Skip to content
Open
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
10 changes: 4 additions & 6 deletions packages/ui/tests/browser/automation-forms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = []
Expand Down Expand Up @@ -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)
Expand All @@ -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<string, unknown> = {}) => ({ json: { available: true, projectID: "project", missions: panelMissions([existing]), generatedAt: 1, discardedEvents: 0, ...extra } })
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@ function RealEditorFixture() {
<Show when={editing()}><MissionEditor instanceId="fixture" directory="/fixture" viewDirectory="/fixture" projectID={projectID()}
action={action} active={() => 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)
}} /></Show>
<Show when={!editing() && doc() && controlsMounted()}><Controls /></Show>
<AlertDialog />
Expand Down
25 changes: 22 additions & 3 deletions packages/ui/tests/browser/mission-creation-start.test.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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<void>) { 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<void>) { activationGate = value } }
}
const save = (page: Page, requested = true) => page.evaluate(({ configuration, requested }) =>
(window as any).creationStartFixture.save(configuration, requested), { configuration, requested })
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions packages/ui/tests/browser/mission-navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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<ReturnType<typeof openPage>>["state"]) {
Expand Down
Loading