Skip to content

Commit aa03d5c

Browse files
test(e2e): a manual trigger is accepted and the run is listed (CL-9519) (#1036)
* test(e2e): a manual trigger is accepted and the run is listed (CL-9519) * test(e2e): wait for Myra's deployment before triggering (CL-9519) * test(e2e): log the deployment listing when none appears (CL-9519)
1 parent 5009c73 commit aa03d5c

3 files changed

Lines changed: 118 additions & 48 deletions

File tree

‎e2e/browser/first-run.test.ts‎

Lines changed: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,11 @@
11
// A fresh user signs up in a real browser, the client sets up its workspace
22
// over stock routes, connects a placeholder provider, and creates a first
33
// workbench that shows up in the sidebar. Asserts on visible UI only.
4-
import { expect, test } from "bun:test";
5-
import type { Page } from "puppeteer-core";
4+
import { test } from "bun:test";
65
import { bootBrowserApp, browserGate } from "../lib/browser";
6+
import { runFirstRunFlow } from "../lib/first-run";
77

88
const describeBrowser = browserGate(import.meta.path);
9-
const STEP_TIMEOUT = 60_000;
10-
const FIRST_WORKBENCH = "Summarize what shipped this week";
11-
12-
// Page scripts are strings: this tsconfig has no DOM lib.
13-
async function waitForText(page: Page, label: string): Promise<void> {
14-
await page.waitForFunction(`document.body.innerText.includes(${JSON.stringify(label)})`, {
15-
timeout: STEP_TIMEOUT,
16-
});
17-
}
18-
19-
async function clickText(page: Page, selector: string, label: string): Promise<void> {
20-
const script = `Array.from(document.querySelectorAll(${JSON.stringify(selector)})).find((e) => e.textContent.includes(${JSON.stringify(label)}))`;
21-
await page.waitForFunction(script, { timeout: STEP_TIMEOUT });
22-
await page.evaluate(`(${script}).click()`);
23-
}
24-
25-
async function runFlow(page: Page, origin: string): Promise<void> {
26-
await page.goto(origin, { waitUntil: "networkidle0" });
27-
28-
await clickText(page, "button", "Create an account");
29-
await waitForText(page, "Create your account");
30-
await page.type("input[type=email]", `alice+${String(Date.now())}@example.com`);
31-
await page.type("input[type=password]", "correct-horse-battery-staple");
32-
await page.click("button[type=submit]");
33-
34-
// Provider connect is the only onboarding step; a placeholder key is
35-
// enough because connecting stores a credential without calling the provider.
36-
await waitForText(page, "Connect a brain");
37-
await clickText(page, "label", "Anthropic");
38-
await page.waitForSelector("input[type=password]");
39-
await page.type("input[type=password]", "sk-ant-placeholder");
40-
await clickText(page, "button", "Connect");
41-
42-
// Connecting installs Myra, then the ready screen hands off to the
43-
// new-workbench prompt (a workspace with no workbenches).
44-
await clickText(page, "button", "Start your first workbench");
45-
await page.waitForSelector("textarea", { timeout: STEP_TIMEOUT });
46-
await page.type("textarea", FIRST_WORKBENCH);
47-
await page.click("button[aria-label='Start this workbench']");
48-
49-
// Creating the workbench navigates to its own page, which shows the prompt.
50-
await page.waitForFunction(`location.pathname.startsWith("/w/")`, { timeout: STEP_TIMEOUT });
51-
await waitForText(page, FIRST_WORKBENCH);
52-
expect(new URL(page.url()).pathname).toStartWith("/w/");
53-
}
549

5510
// The test database outlives a run, so each run signs up a distinct alice.
5611
describeBrowser("first run", () => {
@@ -59,7 +14,7 @@ describeBrowser("first run", () => {
5914
test("sign up, connect a provider, create a first workbench", async () => {
6015
const { page, errors } = await app().newPage();
6116
try {
62-
await runFlow(page, app().origin);
17+
await runFirstRunFlow(page, app().origin);
6318
} catch (cause) {
6419
const body = await page.evaluate("document.body.innerText");
6520
process.stderr.write(

‎e2e/browser/run-now.test.ts‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// A manual trigger through the stock trigger route (what the Routines page's
2+
// Run now calls) answers 202 and the run shows up in the stock runs listing.
3+
// The harness has no scripted inference and no deployed non-agent workflow,
4+
// so Myra's own deployment is the only thing triggerable and the brief +
5+
// artifact half is not asserted.
6+
import { expect, test } from "bun:test";
7+
import { bootBrowserApp, browserGate } from "../lib/browser";
8+
import { runFirstRunFlow } from "../lib/first-run";
9+
10+
const describeBrowser = browserGate(import.meta.path);
11+
12+
// Runs in the page so the signed-in session cookie applies.
13+
const TRIGGER_SCRIPT = `(async () => {
14+
const bench = decodeURIComponent(location.pathname.split("/")[2]);
15+
const json = async (r) => ({ status: r.status, body: await r.json().catch(() => null) });
16+
const benchTenant = await json(await fetch("/api/tenants/" + bench));
17+
const tenants = [bench, benchTenant.body?.parentId].filter(Boolean);
18+
// Myra deploys asynchronously after setup, so poll for the deployment.
19+
let lastListing = null;
20+
for (let attempt = 0; attempt < 60; attempt++) {
21+
for (const tenantId of tenants) {
22+
const listed = await json(await fetch("/api/tenants/" + tenantId + "/workflows/deployments"));
23+
const deployment = (Array.isArray(listed.body) ? listed.body : []).find((d) => d.status === "deployed");
24+
lastListing = { tenantId, status: listed.status, body: listed.body };
25+
if (!deployment) continue;
26+
const base = "/api/tenants/" + tenantId + "/workflows/";
27+
const trigger = await json(await fetch(base + encodeURIComponent(deployment.id) + "/mail", {
28+
method: "POST",
29+
headers: { "content-type": "application/json" },
30+
body: JSON.stringify({ content: "Run now" }),
31+
}));
32+
const runs = await json(await fetch(base + "runs?limit=50"));
33+
return { tenantId, deploymentId: deployment.id, trigger, runs };
34+
}
35+
await new Promise((r) => setTimeout(r, 1000));
36+
}
37+
return { tenants, lastListing };
38+
})()`;
39+
40+
describeBrowser("run now", () => {
41+
const app = bootBrowserApp();
42+
43+
test("a manual trigger is accepted and the run is listed", async () => {
44+
const { page, errors } = await app().newPage();
45+
try {
46+
await runFirstRunFlow(page, app().origin);
47+
const result = (await page.evaluate(TRIGGER_SCRIPT)) as {
48+
deploymentId?: string;
49+
trigger?: { status: number; body: { runId?: string } | null };
50+
runs?: { body: unknown };
51+
};
52+
process.stderr.write(`${JSON.stringify(result)}\n`);
53+
if (result.deploymentId === undefined)
54+
process.stderr.write(`NO-DEPLOYMENT ${JSON.stringify(result)}\n`);
55+
expect(result.deploymentId).toBeDefined();
56+
expect(result.trigger?.status).toBe(202);
57+
expect(typeof result.trigger?.body?.runId).toBe("string");
58+
expect(JSON.stringify(result.runs?.body)).toContain(result.trigger?.body?.runId ?? "");
59+
} catch (cause) {
60+
process.stderr.write(`run-now failed at ${page.url()}\nconsole: ${errors.join(" | ")}\n`);
61+
throw cause;
62+
}
63+
await page.close();
64+
}, 240_000);
65+
});

‎e2e/lib/first-run.ts‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Shared browser steps for e2e: sign up, connect a placeholder provider, and
2+
// create a first workbench.
3+
import { expect } from "bun:test";
4+
import type { Page } from "puppeteer-core";
5+
6+
export const STEP_TIMEOUT = 60_000;
7+
export const FIRST_WORKBENCH = "Summarize what shipped this week";
8+
9+
// Page scripts are strings: this tsconfig has no DOM lib.
10+
export async function waitForText(page: Page, label: string): Promise<void> {
11+
await page.waitForFunction(`document.body.innerText.includes(${JSON.stringify(label)})`, {
12+
timeout: STEP_TIMEOUT,
13+
});
14+
}
15+
16+
export async function clickText(page: Page, selector: string, label: string): Promise<void> {
17+
const script = `Array.from(document.querySelectorAll(${JSON.stringify(selector)})).find((e) => e.textContent.includes(${JSON.stringify(label)}))`;
18+
await page.waitForFunction(script, { timeout: STEP_TIMEOUT });
19+
await page.evaluate(`(${script}).click()`);
20+
}
21+
22+
export async function runFirstRunFlow(page: Page, origin: string): Promise<void> {
23+
await page.goto(origin, { waitUntil: "networkidle0" });
24+
25+
await clickText(page, "button", "Create an account");
26+
await waitForText(page, "Create your account");
27+
await page.type("input[type=email]", `alice+${String(Date.now())}@example.com`);
28+
await page.type("input[type=password]", "correct-horse-battery-staple");
29+
await page.click("button[type=submit]");
30+
31+
// Provider connect is the only onboarding step; a placeholder key is
32+
// enough because connecting stores a credential without calling the provider.
33+
await waitForText(page, "Connect a brain");
34+
await clickText(page, "label", "Anthropic");
35+
await page.waitForSelector("input[type=password]");
36+
await page.type("input[type=password]", "sk-ant-placeholder");
37+
await clickText(page, "button", "Connect");
38+
39+
// Connecting installs Myra, then the ready screen hands off to the
40+
// new-workbench prompt (a workspace with no workbenches).
41+
await clickText(page, "button", "Start your first workbench");
42+
await page.waitForSelector("textarea", { timeout: STEP_TIMEOUT });
43+
await page.type("textarea", FIRST_WORKBENCH);
44+
await page.click("button[aria-label='Start this workbench']");
45+
46+
// Creating the workbench navigates to its own page, which shows the prompt.
47+
await page.waitForFunction(`location.pathname.startsWith("/w/")`, { timeout: STEP_TIMEOUT });
48+
await waitForText(page, FIRST_WORKBENCH);
49+
expect(new URL(page.url()).pathname).toStartWith("/w/");
50+
}

0 commit comments

Comments
 (0)