diff --git a/packages/ui/tests/browser/automation-forms.test.ts b/packages/ui/tests/browser/automation-forms.test.ts index ae4493474..4f1e43514 100644 --- a/packages/ui/tests/browser/automation-forms.test.ts +++ b/packages/ui/tests/browser/automation-forms.test.ts @@ -182,7 +182,7 @@ test("an agent fills the real Mission editor: the title is replaced, not concate await page.addInitScript(desktopWindow) await page.route("**/api/**", route => route.fulfill({ json: {} })) await page.route("**/api/storage/config/ui", route => route.fulfill({ json: { settings: { missionProfileDefaults: [{ template: "all", profiles }] } } })) - // Creation Options embed the project's nesting depth: answer its read like a native runtime. + // Creation embeds the project's nesting depth: answer its read like a native runtime. await page.route("**/subagent-depth*", route => route.fulfill({ json: { location: { directory: "/fixture" }, capability: { minimum: 0 }, effectiveDepth: 1, project: { path: "/fixture/.opencode/opencode.jsonc", depth: null, expectation: "e" } } })) let list: MissionMap[] = [] @@ -212,12 +212,11 @@ test("an agent fills the real Mission editor: the title is replaced, not concate await act({ action: "type", ref: title, text: "First draft" }) assert.deepEqual(await act({ action: "type", ref: title, text: "Seam survey" }), { value: "Seam survey" }, "a second type replaces the first") assert.deepEqual(await act({ action: "type", ref: title, text: " (pilot)", clear: false }), { value: "Seam survey (pilot)" }) - await act({ action: "click", ref: await refFor(/DisclosureTriangle "Options"/, "Options") }) assert.deepEqual(await act({ action: "select", ref: await refFor(/combobox "Playbook"/, "Playbook"), values: ["Exploration"] }), { selected: ["Exploration"] }) assert.match((await run.cdp.inspect(run.identity, { query: "Playbook" })).snapshot, /combobox "Playbook" \[e\d+\] value="Exploration"/) await page.waitForFunction(() => !(document.querySelector('form.mission-editor button[type="submit"]') as HTMLButtonElement)?.disabled) assert.equal(await page.getByLabel("Title", { exact: true }).inputValue(), "Seam survey (pilot)") - // Like an agent, wait for the Options section to finish reading before clicking under it. + // Like an agent, wait for the depth row to finish reading before clicking under it. await act({ action: "wait", text: "Inherit (1)" }) await act({ action: "click", ref: await refFor(/button "Create"(?= \[)/, "Create") }) await page.waitForFunction(() => document.querySelector("form.mission-editor") === null) @@ -227,7 +226,7 @@ test("an agent fills the real Mission editor: the title is replaced, not concate } finally { await run.close() } }) -// The controller aims once, then dispatches: Options' first depth result, or a mission map +// The controller aims once, then dispatches: creation's first depth result, or a mission map // refresh adding a panel banner while the form is open, may land in between. const existing = { ...createdMission({ title: "Existing mission", objective: "Existing mission", template: "custom" }), id: "msn_existing" } const map = (extra: Record = {}) => ({ json: { available: true, projectID: "project", missions: panelMissions([existing]), generatedAt: 1, discardedEvents: 0, ...extra } }) @@ -283,8 +282,7 @@ for (const outcome of HELD_OUTCOMES) test(`Create keeps its place when ${outcome const { page, act, refFor } = run await act({ action: "click", ref: await refFor(/button "Create mission"/, "Create mission") }) await act({ action: "type", ref: await refFor(/textbox "What should the mission do\?"/, "What should the mission do?"), text: "Hold a read" }) - if (outcome.held === "depth") await act({ action: "click", ref: await refFor(/DisclosureTriangle "Options"/, "Options") }) - else { holdMap = true; await page.evaluate(() => window.missionEditorLifetime.invalidate()) } + if (outcome.held !== "depth") { holdMap = true; await page.evaluate(() => window.missionEditorLifetime.invalidate()) } await requested.promise await page.waitForFunction(() => !(document.querySelector('form.mission-editor button[type="submit"]') as HTMLButtonElement)?.disabled) const create = page.locator('form.mission-editor button[type="submit"]'), before = await create.boundingBox() diff --git a/packages/ui/tests/browser/fixtures/mission-creation-start.tsx b/packages/ui/tests/browser/fixtures/mission-creation-start.tsx index 92845342d..9c51beac9 100644 --- a/packages/ui/tests/browser/fixtures/mission-creation-start.tsx +++ b/packages/ui/tests/browser/fixtures/mission-creation-start.tsx @@ -80,9 +80,11 @@ function RealEditorFixture() { true} onCancel={() => setEditing(false)} onSaved={async (_mission, id) => { if (!id) throw new Error("Expected calendar-owned creation") + // Match MissionControl: retire the editor synchronously before reading its created document. + setEditing(false) const read = await serverApi.fetchMissionSchedule("fixture", id, { directory: "/fixture" }) if (!read.document) throw new Error("Created Mission unavailable") - setDoc(read.document); setEditing(false); setSaved(value => value + 1) + setDoc(read.document); setSaved(value => value + 1) }} /> diff --git a/packages/ui/tests/browser/mission-creation-start.test.ts b/packages/ui/tests/browser/mission-creation-start.test.ts index f4690ed92..0a9266ad7 100644 --- a/packages/ui/tests/browser/mission-creation-start.test.ts +++ b/packages/ui/tests/browser/mission-creation-start.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict" import { after, before, test } from "node:test" import { createServer, type Server } from "node:http" -import { chromium, type Browser, type Page } from "playwright" +import { chromium, type Browser, type Page, type Request } from "playwright" import { borrowFixture } from "./fixture-borrow" import { armFixtureNavigationBudget } from "./mission-panel-route" import { NativeMissionDocumentStore } from "../../../server/src/missions/mission-document-store" @@ -101,9 +101,24 @@ async function setup(page: Page, options: { enabled?: boolean; lose?: "create" | await page.route("**/api/workspaces/fixture/missions", route => route.fulfill({ json: { available: true, version: 1, projectID: "project", missions: [], cleanups: [], generatedAt: 1, discardedEvents: 0 } })) } + const kinds = ["agent", "model", "provider"], completed: Request[] = [] + const matches = (request: Request, kind: string) => request.method() === "GET" + && new URL(request.url()).pathname === `/workspaces/fixture/instance/api/${kind}` + // Capture body-completion receipts without starting action deadlines during navigation. + const remember = (request: Request) => { if (kinds.some(kind => matches(request, kind))) completed.push(request) } + page.on("requestfinished", remember) + const waitForCatalog = () => { + page.off("requestfinished", remember) + const early = completed.splice(0) // Initial receipts are consumed once; later waves require new events. + return Promise.all(kinds.map(async kind => { + if (!early.some(request => matches(request, kind))) await page.waitForEvent("requestfinished", { predicate: request => matches(request, kind) }) + })) + } await page.goto(options.realEditor ? `${url}?realEditor=1` : url) await page.waitForFunction(realEditor => realEditor ? (window as any).realCreationStartFixture : (window as any).creationStartFixture, !!options.realEditor) - return { posts, reads, failedRequests, errors, get revision() { return doc.revision }, set activationGate(value: Promise) { activationGate = value } } + if (options.realEditor) await waitForCatalog() + else page.off("requestfinished", remember) + return { posts, reads, failedRequests, errors, waitForCatalog, get revision() { return doc.revision }, set activationGate(value: Promise) { activationGate = value } } } const save = (page: Page, requested = true) => page.evaluate(({ configuration, requested }) => (window as any).creationStartFixture.save(configuration, requested), { configuration, requested }) @@ -194,13 +209,17 @@ test("real editor adopts discovered project before creation; lost Play remains r assert.ok(originalForm) assert.equal(await page.locator("[data-project]").textContent(), "undiscovered") await form.getByLabel("What should the mission do?", { exact: true }).fill("Inspect changes after project discovery") - await page.evaluate(() => (window as any).realCreationStartFixture.discoverProject()) + await Promise.all([ + f.waitForCatalog(), + page.evaluate(() => (window as any).realCreationStartFixture.discoverProject()), + ]) await page.getByText("project", { exact: true }).waitFor() assert.equal(await originalForm.evaluate(element => element.isConnected && element === document.querySelector("form.mission-editor")), true, "project discovery must update the already-mounted MissionEditor") assert.equal(await form.getByLabel("What should the mission do?", { exact: true }).inputValue(), "Inspect changes after project discovery") await form.getByLabel("Criterion 1 type", { exact: true }).selectOption("daily") await form.getByRole("checkbox", { name: "Start on creation", exact: true }).check() + await form.getByLabel("Coordinator · Model", { exact: true }).locator("option:checked").filter({ hasText: "Fixture model" }).waitFor({ state: "attached" }) await form.getByRole("button", { name: "Create", exact: true }).click() await form.waitFor({ state: "detached" }); await saved(page) diff --git a/packages/ui/tests/browser/mission-navigation.test.ts b/packages/ui/tests/browser/mission-navigation.test.ts index d86d603cd..ccc40115a 100644 --- a/packages/ui/tests/browser/mission-navigation.test.ts +++ b/packages/ui/tests/browser/mission-navigation.test.ts @@ -124,6 +124,8 @@ async function setup(missing = false, holdSchedule = false) { const pathname = new URL(route.request().url()).pathname requests.push(`${route.request().method()} ${pathname}`) if (pathname === "/api/events") return route.fulfill({ contentType: "text/event-stream", body: ": fixture\n\n" }) + if (pathname.endsWith("/subagent-depth")) return route.fulfill({ json: { location: { directory: "/fixture" }, + capability: { minimum: 0 }, effectiveDepth: 1, project: { path: "/fixture/.opencode/opencode.jsonc", depth: null, expectation: "e" } } }) if (pathname.endsWith("/missions/scheduled")) return route.fulfill({ json: { version: 1, projectID: "project", schedules: [] } }) if (pathname.endsWith("/missions/A/schedule") && holdSchedule) { scheduleReached.release(); await scheduleHeld.promise diff --git a/packages/ui/tests/browser/mission-recurrence-retry-order.test.ts b/packages/ui/tests/browser/mission-recurrence-retry-order.test.ts index 27db85c0e..ae5a11bb6 100644 --- a/packages/ui/tests/browser/mission-recurrence-retry-order.test.ts +++ b/packages/ui/tests/browser/mission-recurrence-retry-order.test.ts @@ -6,7 +6,7 @@ import { borrowFixture } from "./fixture-borrow" import { createMissionFailureDiagnostics } from "./mission-failure-diagnostics" import { chromium, type Browser, type Page, type Route } from "playwright" import type {} from "./fixtures/mission-editor-lifetime" -import { clickMissionAction, missionRefresh, missionToolbarAction, selectMission } from "./mission-actions" +import { clickMissionAction, missionRefresh, missionRow, missionToolbarAction, selectMission } from "./mission-actions" import type { RecurrenceSchedule } from "../../src/stores/mission-recurrence" import { recurrenceSnapshotSchema, recurrenceControlHttpSchema, recurrenceControlStatusSchema } from "../../../server/src/missions/recurrence-control-contract" @@ -110,6 +110,8 @@ async function holdPartial(page: Page, action: "pause" | "stop", title = TITLE) await missionToolbarAction(page, CHECK(title)).waitFor() await page.locator(".mission-picker-selection-actions .mission-action-feedback [role=status]").getByText(UNCONFIRMED, { exact: true }).waitFor() assert.equal(await missionRefresh(page).getAttribute("aria-label"), RESEND) + // The partial result schedules a row read; feedback can precede its publication. + await missionRow(page, title).locator(".sr-only", { hasText: action === "pause" ? /^Daily · Paused/ : /^Daily · Stopped/ }).waitFor({ state: "attached" }) } /** Starts an event-driven row read that stays in flight until released. */ async function rowReadInFlight(page: Page, state: Awaited>["state"]) {