diff --git a/scripts/language-policy-baseline.json b/scripts/language-policy-baseline.json index 8d065cbe3..7b7698729 100644 --- a/scripts/language-policy-baseline.json +++ b/scripts/language-policy-baseline.json @@ -33,7 +33,6 @@ "src/ui/design-system/sidebar-motion.js", "src/ui/tui/api.js", "src/ui/tui/blocks.js", - "src/ui/tui/golden-scenarios/initial-scenarios.js", "src/ui/tui/golden-scenarios/planned-change-workflow.js", "src/ui/tui/golden-scenarios/project-workflow.js", "src/ui/tui/testing/child-protocol.js", diff --git a/src/ui/tui/golden-scenarios/catalog.ts b/src/ui/tui/golden-scenarios/catalog.ts index 1d7a34a31..75b4b0f16 100644 --- a/src/ui/tui/golden-scenarios/catalog.ts +++ b/src/ui/tui/golden-scenarios/catalog.ts @@ -4,7 +4,7 @@ */ import { concurrentWorkflowScenarios } from "./concurrent-workflow.ts"; -import { initialGoldenScenarios } from "./initial-scenarios.js"; +import { initialGoldenScenarios } from "./initial-scenarios.ts"; import { loadPlanWorkflowScenarios } from "./load-plan-workflow.ts"; import { loadPlanEpicWorkflowScenarios } from "./load-plan-epic-workflow.ts"; import { plannedChangeWorkflowScenarios } from "./planned-change-workflow.js"; diff --git a/src/ui/tui/golden-scenarios/initial-scenarios.test.ts b/src/ui/tui/golden-scenarios/initial-scenarios.test.ts index 4831d9f3a..fa16b996a 100644 --- a/src/ui/tui/golden-scenarios/initial-scenarios.test.ts +++ b/src/ui/tui/golden-scenarios/initial-scenarios.test.ts @@ -23,7 +23,7 @@ import { runtimeInteractionContractScenario, startupNoProvidersOpensLoginScenario, startupProviderWithoutModelsOpensModelScenario, -} from "./initial-scenarios.js"; +} from "./initial-scenarios.ts"; import { GoldenScenarioActor, runGoldenScenario } from "../testing/mod.ts"; import { NO_OPEN_BROWSER_PORT } from "../../../shared/browser-port.ts"; @@ -72,7 +72,7 @@ for (const scenario of initialGoldenScenarios.filter((candidate) => candidate != test(`golden scenario: ${scenario.name}`, async () => { const { runGoldenScenarioChildProcess } = await import("../testing/child-protocol.js"); const result = await runGoldenScenarioChildProcess({ - scenarioModule: "src/ui/tui/golden-scenarios/initial-scenarios.js", + scenarioModule: "src/ui/tui/golden-scenarios/initial-scenarios.ts", exportName: scenarioExportNames.get(scenario) || "", timeoutMs: "timeoutMs" in scenario && typeof scenario.timeoutMs === "number" ? scenario.timeoutMs : 30000, }); @@ -147,7 +147,7 @@ test("golden actor fails when required tools are unavailable", () => { test("golden scenario child process runs with isolated environment before scenario import", async () => { const { runGoldenScenarioChildProcess } = await import("../testing/child-protocol.js"); const result = await runGoldenScenarioChildProcess({ - scenarioModule: "src/ui/tui/golden-scenarios/initial-scenarios.js", + scenarioModule: "src/ui/tui/golden-scenarios/initial-scenarios.ts", exportName: "helpSlashCommandScenario", timeoutMs: 30000, }); @@ -206,7 +206,7 @@ test("golden scenario child process failure reports retained diagnostics artifac const error = await assertRejects( () => runGoldenScenarioChildProcess({ - scenarioModule: "src/ui/tui/golden-scenarios/initial-scenarios.js", + scenarioModule: "src/ui/tui/golden-scenarios/initial-scenarios.ts", exportName: "missingGoldenScenario", timeoutMs: 3000, }), @@ -229,7 +229,7 @@ test("golden scenario timeout retains child heartbeat diagnostics", async () => const error = await assertRejects( () => runGoldenScenarioChildProcess({ - scenarioModule: "src/ui/tui/golden-scenarios/initial-scenarios.js", + scenarioModule: "src/ui/tui/golden-scenarios/initial-scenarios.ts", exportName: "timeoutDiagnosticScenario", timeoutMs: 2000, }), @@ -263,7 +263,7 @@ test("golden scenario child crash surfaces substantive scenario diagnostics", as const error = await assertRejects( () => runGoldenScenarioChildProcess({ - scenarioModule: "src/ui/tui/golden-scenarios/initial-scenarios.js", + scenarioModule: "src/ui/tui/golden-scenarios/initial-scenarios.ts", exportName: "diagnosticArtifactFailureScenario", timeoutMs: 10000, }), diff --git a/src/ui/tui/golden-scenarios/initial-scenarios.js b/src/ui/tui/golden-scenarios/initial-scenarios.ts similarity index 87% rename from src/ui/tui/golden-scenarios/initial-scenarios.js rename to src/ui/tui/golden-scenarios/initial-scenarios.ts index 96eea5b3f..3af173cfb 100644 --- a/src/ui/tui/golden-scenarios/initial-scenarios.js +++ b/src/ui/tui/golden-scenarios/initial-scenarios.ts @@ -6,10 +6,33 @@ import { ok as assert } from "node:assert/strict"; import { assertEventIncludes, assertScreenIncludes } from "../testing/scenario-runner.js"; -/** @typedef {import('../testing/scenario-runner.js').GoldenScenarioResult} GoldenScenarioResult */ +import type { + ConsumedScriptedInteraction, + ConsumedScriptedReview, + GoldenScenario, + GoldenScenarioResult, +} from "../testing/mod.ts"; -/** @param {GoldenScenarioResult} result */ -function assertRuntimeGuideSwitch(result) { +interface PlanReviewLifecycleEvent { + event: string; + status?: string; +} +interface PlanReviewAttributes { + status: string; +} +interface ReviewConversationDocument { + before?: string; + after?: string; +} +interface PlanReviewEvidence { + attrs: PlanReviewAttributes; + lifecycleEvents: PlanReviewLifecycleEvent[]; + consumed: ConsumedScriptedReview[]; + conversationDocuments: ReviewConversationDocument[]; + plan: string; +} + +function assertRuntimeGuideSwitch(result: GoldenScenarioResult) { assertEventIncludes(result, "runtime:agent:guide"); assert( result.state.activeAgent === "guide", @@ -21,43 +44,36 @@ function assertRuntimeGuideSwitch(result) { assertEventIncludes(result, "runtime:assistant:text"); } -/** @param {GoldenScenarioResult} result */ -function assertTerminalInputVisible(result) { +function assertTerminalInputVisible(result: GoldenScenarioResult) { assertEventIncludes(result, "terminal:type:how does routing work?"); } -/** @param {GoldenScenarioResult} result */ -function assertProjectClean(result) { +function assertProjectClean(result: GoldenScenarioResult) { assert( result.state.projectMutation === "clean", `Expected clean Project mutation state; got ${result.state.projectMutation}`, ); } -/** @param {GoldenScenarioResult} result */ -function assertCancellationEvent(result) { +function assertCancellationEvent(result: GoldenScenarioResult) { assertEventIncludes(result, "runtime:cancellation"); } -/** @param {GoldenScenarioResult} result */ -function assertEditorReady(result) { +function assertEditorReady(result: GoldenScenarioResult) { assert(result.state.editorUsable === true, "Expected editor to be usable after cancellation."); assertEventIncludes(result, "terminal:type:benign follow-up after cancel"); assertEventIncludes(result, "runtime:assistant:text"); } -/** @param {GoldenScenarioResult} result */ -function assertHelpSlashVisible(result) { +function assertHelpSlashVisible(result: GoldenScenarioResult) { assertEventIncludes(result, "terminal:type:/help"); } -/** @param {GoldenScenarioResult} result */ -function assertKeyboardHelpVisible(result) { +function assertKeyboardHelpVisible(result: GoldenScenarioResult) { assertScreenIncludes(result, "Global flags:"); } -/** @param {GoldenScenarioResult} result */ -function assertStartupLoginPromptOpened(result) { +function assertStartupLoginPromptOpened(result: GoldenScenarioResult) { const startupScreen = String(result.state.startupScreen || ""); assert( startupScreen.includes("Welcome to RunWield"), @@ -69,8 +85,7 @@ function assertStartupLoginPromptOpened(result) { ); } -/** @param {GoldenScenarioResult} result */ -function assertStartupModelSelectorOpened(result) { +function assertStartupModelSelectorOpened(result: GoldenScenarioResult) { const startupScreen = String(result.state.startupScreen || ""); assert( startupScreen.includes("Only showing models from configured providers"), @@ -82,24 +97,18 @@ function assertStartupModelSelectorOpened(result) { ); } -/** @param {GoldenScenarioResult} result */ -function assertReviewFeedbackEvent(result) { +function assertReviewFeedbackEvent(result: GoldenScenarioResult) { assertEventIncludes(result, "interaction:PLAN_REVIEW:feedback"); assertEventIncludes(result, "review_feedback"); } -/** @param {GoldenScenarioResult} result */ -function assertReviewApprovedEvent(result) { +function assertReviewApprovedEvent(result: GoldenScenarioResult) { assertEventIncludes(result, "interaction:PLAN_REVIEW:approved"); assertEventIncludes(result, "review_approved"); } -/** @param {GoldenScenarioResult} result */ -function assertPlanReviewLifecyclePersisted(result) { - const planReview = - /** @type {{ lifecycleEvents?: Array<{ event: string, status?: unknown }>, attrs?: Record, consumed?: unknown[] } | undefined} */ ( - result.state.planReview - ); +function assertPlanReviewLifecyclePersisted(result: GoldenScenarioResult) { + const planReview = result.state.planReview as Partial | undefined; assert(planReview, "Expected planReview state from production submitPlanForReview transaction."); assert( planReview.lifecycleEvents?.map((event) => `${event.event}:${event.status}`).join(",") === @@ -110,7 +119,7 @@ function assertPlanReviewLifecyclePersisted(result) { ); assert(planReview.consumed?.length === 2, "Expected scripted review surface to be consumed twice."); assert( - String(/** @type {{ plan?: unknown }} */ (planReview).plan || "").includes("Reviewed content persisted."), + String(planReview.plan || "").includes("Reviewed content persisted."), "Expected reviewed Plan body to persist.", ); } @@ -163,7 +172,7 @@ export const routerToGuideInquiryScenario = { { type: "assertProjectUnchanged" }, ], assertions: [assertTerminalInputVisible, assertRuntimeGuideSwitch, assertProjectClean], -}; +} satisfies GoldenScenario; export const escapeCancellationScenario = { name: "escape-cancellation-restores-editor", @@ -203,7 +212,7 @@ export const escapeCancellationScenario = { { type: "waitForIdle" }, ], assertions: [assertCancellationEvent, assertEditorReady], -}; +} satisfies GoldenScenario; export const helpSlashCommandScenario = { name: "help-slash-command", @@ -211,7 +220,7 @@ export const helpSlashCommandScenario = { terminal: { columns: 100, rows: 30 }, actions: [{ type: "type", text: "/help" }, { type: "enter" }, { type: "waitForIdle" }], assertions: [assertHelpSlashVisible, assertKeyboardHelpVisible], -}; +} satisfies GoldenScenario; export const startupNoProvidersOpensLoginScenario = { name: "startup-no-providers-opens-login", @@ -227,7 +236,7 @@ export const startupNoProvidersOpensLoginScenario = { actions: [], assertions: [assertStartupLoginPromptOpened], timeoutMs: 5000, -}; +} satisfies GoldenScenario; export const startupProviderWithoutModelsOpensModelScenario = { name: "startup-provider-without-models-opens-model", @@ -241,7 +250,7 @@ export const startupProviderWithoutModelsOpensModelScenario = { actions: [], assertions: [assertStartupModelSelectorOpened], timeoutMs: 15000, -}; +} satisfies GoldenScenario; export const planReviewTransactionContractScenario = { name: "plan-review-transaction-contract", @@ -294,10 +303,9 @@ export const planReviewTransactionContractScenario = { { type: "waitForIdle" }, ], assertions: [assertReviewFeedbackEvent, assertReviewApprovedEvent, assertPlanReviewLifecyclePersisted], -}; +} satisfies GoldenScenario; -/** @param {GoldenScenarioResult} result */ -function assertReplyOnlyReviewApproval(result) { +function assertReplyOnlyReviewApproval(result: GoldenScenarioResult) { assertEventIncludes(result, "interaction:PLAN_REVIEW:conversation"); assertEventIncludes(result, "interaction:PLAN_REVIEW:approved"); assertEventIncludes(result, "runtime:assistant:text"); @@ -305,10 +313,7 @@ function assertReplyOnlyReviewApproval(result) { result.events.filter((event) => event === "runtime:tool:start:plan_written").length === 1, "The reply-only turn must not call plan_written again.", ); - const review = - /** @type {{ attrs: { status: string }, conversationDocuments: Array<{ before?: string, after?: string }>, lifecycleEvents: Array<{ event: string }>, consumed: Array<{ decision: { approved: boolean } }>, plan: string }} */ ( - result.state.planReview - ); + const review = result.state.planReview as PlanReviewEvidence; assert(review.attrs.status === "ready_for_work", "Approval through the re-armed review must pass readiness."); assert(review.consumed.length === 2, "The same live page must accept chat then approval."); assert( @@ -357,7 +362,7 @@ export const planReviewReplyOnlyConversationScenario = { { type: "waitForIdle" }, ], assertions: [assertReplyOnlyReviewApproval], -}; +} satisfies GoldenScenario; export const fauxProviderProtocolScenario = { name: "faux-provider-protocol-contract", @@ -392,19 +397,16 @@ export const fauxProviderProtocolScenario = { ], actions: [{ type: "type", text: "protocol check" }, { type: "enter" }, { type: "waitForIdle" }], assertions: [ - (/** @type {GoldenScenarioResult} */ result) => - assertEventIncludes(result, "model:faux-provider:router:triage"), - (/** @type {GoldenScenarioResult} */ result) => - assertEventIncludes(result, "model:faux-provider:guide:inquiry"), + (result: GoldenScenarioResult) => assertEventIncludes(result, "model:faux-provider:router:triage"), + (result: GoldenScenarioResult) => assertEventIncludes(result, "model:faux-provider:guide:inquiry"), ], -}; +} satisfies GoldenScenario; -/** @param {GoldenScenarioResult} result */ -function assertScriptedRuntimeInteractions(result) { +function assertScriptedRuntimeInteractions(result: GoldenScenarioResult) { assertEventIncludes(result, "interaction:select:selected"); assertEventIncludes(result, "interaction:text:text"); assertEventIncludes(result, "interaction:approval:accepted"); - const interactions = /** @type {unknown[]} */ (result.state.scriptedInteractions || []); + const interactions = (result.state.scriptedInteractions || []) as ConsumedScriptedInteraction[]; assert(interactions.length === 3, `Expected three scripted Runtime interactions; got ${interactions.length}`); } @@ -446,7 +448,7 @@ export const runtimeInteractionContractScenario = { }, ], assertions: [assertScriptedRuntimeInteractions], -}; +} satisfies GoldenScenario; // Deliberately hangs so the parent's scenario timeout fires mid-sleep. The sleep // is far longer than that timeout so the kill window cannot be outrun. @@ -456,7 +458,7 @@ export const timeoutDiagnosticScenario = { terminal: { columns: 80, rows: 20 }, script: [{ id: "timeout-unused-turn", agent: "router", phase: "triage", text: "unused" }], actions: [{ type: "sleep", ms: 120_000 }], -}; +} satisfies GoldenScenario; export const diagnosticArtifactFailureScenario = { name: "diagnostic-artifact-failure", @@ -464,7 +466,7 @@ export const diagnosticArtifactFailureScenario = { terminal: { columns: 80, rows: 20 }, script: [{ id: "unused-router-turn", agent: "router", phase: "triage", text: "unused" }], actions: [{ type: "unknown-composed-action" }], -}; +} satisfies GoldenScenario; export const initialGoldenScenarios = [ routerToGuideInquiryScenario,