Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 3 additions & 48 deletions e2e/browser/first-run.test.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,11 @@
// A fresh user signs up in a real browser, the client sets up its workspace
// over stock routes, connects a placeholder provider, and creates a first
// workbench that shows up in the sidebar. Asserts on visible UI only.
import { expect, test } from "bun:test";
import type { Page } from "puppeteer-core";
import { test } from "bun:test";
import { bootBrowserApp, browserGate } from "../lib/browser";
import { runFirstRunFlow } from "../lib/first-run";

const describeBrowser = browserGate(import.meta.path);
const STEP_TIMEOUT = 60_000;
const FIRST_WORKBENCH = "Summarize what shipped this week";

// Page scripts are strings: this tsconfig has no DOM lib.
async function waitForText(page: Page, label: string): Promise<void> {
await page.waitForFunction(`document.body.innerText.includes(${JSON.stringify(label)})`, {
timeout: STEP_TIMEOUT,
});
}

async function clickText(page: Page, selector: string, label: string): Promise<void> {
const script = `Array.from(document.querySelectorAll(${JSON.stringify(selector)})).find((e) => e.textContent.includes(${JSON.stringify(label)}))`;
await page.waitForFunction(script, { timeout: STEP_TIMEOUT });
await page.evaluate(`(${script}).click()`);
}

async function runFlow(page: Page, origin: string): Promise<void> {
await page.goto(origin, { waitUntil: "networkidle0" });

await clickText(page, "button", "Create an account");
await waitForText(page, "Create your account");
await page.type("input[type=email]", `alice+${String(Date.now())}@example.com`);
await page.type("input[type=password]", "correct-horse-battery-staple");
await page.click("button[type=submit]");

// Provider connect is the only onboarding step; a placeholder key is
// enough because connecting stores a credential without calling the provider.
await waitForText(page, "Connect a brain");
await clickText(page, "label", "Anthropic");
await page.waitForSelector("input[type=password]");
await page.type("input[type=password]", "sk-ant-placeholder");
await clickText(page, "button", "Connect");

// Connecting installs Myra, then the ready screen hands off to the
// new-workbench prompt (a workspace with no workbenches).
await clickText(page, "button", "Start your first workbench");
await page.waitForSelector("textarea", { timeout: STEP_TIMEOUT });
await page.type("textarea", FIRST_WORKBENCH);
await page.click("button[aria-label='Start this workbench']");

// Creating the workbench navigates to its own page, which shows the prompt.
await page.waitForFunction(`location.pathname.startsWith("/w/")`, { timeout: STEP_TIMEOUT });
await waitForText(page, FIRST_WORKBENCH);
expect(new URL(page.url()).pathname).toStartWith("/w/");
}

// The test database outlives a run, so each run signs up a distinct alice.
describeBrowser("first run", () => {
Expand All @@ -59,7 +14,7 @@ describeBrowser("first run", () => {
test("sign up, connect a provider, create a first workbench", async () => {
const { page, errors } = await app().newPage();
try {
await runFlow(page, app().origin);
await runFirstRunFlow(page, app().origin);
} catch (cause) {
const body = await page.evaluate("document.body.innerText");
process.stderr.write(
Expand Down
65 changes: 65 additions & 0 deletions e2e/browser/run-now.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// A manual trigger through the stock trigger route (what the Routines page's
// Run now calls) answers 202 and the run shows up in the stock runs listing.
// The harness has no scripted inference and no deployed non-agent workflow,
// so Myra's own deployment is the only thing triggerable and the brief +
// artifact half is not asserted.
import { expect, test } from "bun:test";
import { bootBrowserApp, browserGate } from "../lib/browser";
import { runFirstRunFlow } from "../lib/first-run";

const describeBrowser = browserGate(import.meta.path);

// Runs in the page so the signed-in session cookie applies.
const TRIGGER_SCRIPT = `(async () => {
const bench = decodeURIComponent(location.pathname.split("/")[2]);
const json = async (r) => ({ status: r.status, body: await r.json().catch(() => null) });
const benchTenant = await json(await fetch("/api/tenants/" + bench));
const tenants = [bench, benchTenant.body?.parentId].filter(Boolean);
// Myra deploys asynchronously after setup, so poll for the deployment.
let lastListing = null;
for (let attempt = 0; attempt < 60; attempt++) {
for (const tenantId of tenants) {
const listed = await json(await fetch("/api/tenants/" + tenantId + "/workflows/deployments"));
const deployment = (Array.isArray(listed.body) ? listed.body : []).find((d) => d.status === "deployed");
lastListing = { tenantId, status: listed.status, body: listed.body };
if (!deployment) continue;
const base = "/api/tenants/" + tenantId + "/workflows/";
const trigger = await json(await fetch(base + encodeURIComponent(deployment.id) + "/mail", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ content: "Run now" }),
}));
const runs = await json(await fetch(base + "runs?limit=50"));
return { tenantId, deploymentId: deployment.id, trigger, runs };
}
await new Promise((r) => setTimeout(r, 1000));
}
return { tenants, lastListing };
})()`;

describeBrowser("run now", () => {
const app = bootBrowserApp();

test("a manual trigger is accepted and the run is listed", async () => {
const { page, errors } = await app().newPage();
try {
await runFirstRunFlow(page, app().origin);
const result = (await page.evaluate(TRIGGER_SCRIPT)) as {
deploymentId?: string;
trigger?: { status: number; body: { runId?: string } | null };
runs?: { body: unknown };
};
process.stderr.write(`${JSON.stringify(result)}\n`);
if (result.deploymentId === undefined)
process.stderr.write(`NO-DEPLOYMENT ${JSON.stringify(result)}\n`);
expect(result.deploymentId).toBeDefined();
expect(result.trigger?.status).toBe(202);
expect(typeof result.trigger?.body?.runId).toBe("string");
expect(JSON.stringify(result.runs?.body)).toContain(result.trigger?.body?.runId ?? "");
} catch (cause) {
process.stderr.write(`run-now failed at ${page.url()}\nconsole: ${errors.join(" | ")}\n`);
throw cause;
}
await page.close();
}, 240_000);
});
50 changes: 50 additions & 0 deletions e2e/lib/first-run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Shared browser steps for e2e: sign up, connect a placeholder provider, and
// create a first workbench.
import { expect } from "bun:test";
import type { Page } from "puppeteer-core";

export const STEP_TIMEOUT = 60_000;
export const FIRST_WORKBENCH = "Summarize what shipped this week";

// Page scripts are strings: this tsconfig has no DOM lib.
export async function waitForText(page: Page, label: string): Promise<void> {
await page.waitForFunction(`document.body.innerText.includes(${JSON.stringify(label)})`, {
timeout: STEP_TIMEOUT,
});
}

export async function clickText(page: Page, selector: string, label: string): Promise<void> {
const script = `Array.from(document.querySelectorAll(${JSON.stringify(selector)})).find((e) => e.textContent.includes(${JSON.stringify(label)}))`;
await page.waitForFunction(script, { timeout: STEP_TIMEOUT });
await page.evaluate(`(${script}).click()`);
}

export async function runFirstRunFlow(page: Page, origin: string): Promise<void> {
await page.goto(origin, { waitUntil: "networkidle0" });

await clickText(page, "button", "Create an account");
await waitForText(page, "Create your account");
await page.type("input[type=email]", `alice+${String(Date.now())}@example.com`);
await page.type("input[type=password]", "correct-horse-battery-staple");
await page.click("button[type=submit]");

// Provider connect is the only onboarding step; a placeholder key is
// enough because connecting stores a credential without calling the provider.
await waitForText(page, "Connect a brain");
await clickText(page, "label", "Anthropic");
await page.waitForSelector("input[type=password]");
await page.type("input[type=password]", "sk-ant-placeholder");
await clickText(page, "button", "Connect");

// Connecting installs Myra, then the ready screen hands off to the
// new-workbench prompt (a workspace with no workbenches).
await clickText(page, "button", "Start your first workbench");
await page.waitForSelector("textarea", { timeout: STEP_TIMEOUT });
await page.type("textarea", FIRST_WORKBENCH);
await page.click("button[aria-label='Start this workbench']");

// Creating the workbench navigates to its own page, which shows the prompt.
await page.waitForFunction(`location.pathname.startsWith("/w/")`, { timeout: STEP_TIMEOUT });
await waitForText(page, FIRST_WORKBENCH);
expect(new URL(page.url()).pathname).toStartWith("/w/");
}
Loading