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
1 change: 0 additions & 1 deletion scripts/language-policy-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"src/ui/tui/api.js",
"src/ui/tui/blocks.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",
"src/ui/tui/testing/scenario-runner.js",
"src/ui/tui/types.js",
Expand Down
2 changes: 1 addition & 1 deletion src/ui/tui/golden-scenarios/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { loadPlanWorkflowScenarios } from "./load-plan-workflow.ts";
import { loadPlanEpicWorkflowScenarios } from "./load-plan-epic-workflow.ts";
import { plannedChangeWorkflowScenarios } from "./planned-change-workflow.js";
import { presentationAndTerminalScenarios } from "./presentation-and-terminal.ts";
import { projectWorkflowScenarios } from "./project-workflow.js";
import { projectWorkflowScenarios } from "./project-workflow.ts";
import { projectRoutingWorkflowScenarios } from "./project-routing-workflow.ts";
import { roleJourneyScenarios } from "./role-journeys.ts";
import { sessionResumeWorkflowScenarios } from "./session-resume-workflow.ts";
Expand Down
4 changes: 2 additions & 2 deletions src/ui/tui/golden-scenarios/project-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
projectPlanReviewScenario,
projectWorkflowScenarios,
twoChildProjectContinuationScenario,
} from "./project-workflow.js";
} from "./project-workflow.ts";

type ProjectWorkflowScenario = (typeof projectWorkflowScenarios)[number] &
Partial<Pick<typeof twoChildProjectContinuationScenario, "timeoutMs">>;
Expand All @@ -22,7 +22,7 @@ for (const scenario of scenarios) {
test(`golden PROJECT workflow: ${scenario.name}`, async () => {
const { runGoldenScenarioChildProcess } = await import("../testing/child-protocol.js");
const result = await runGoldenScenarioChildProcess({
scenarioModule: "src/ui/tui/golden-scenarios/project-workflow.js",
scenarioModule: "src/ui/tui/golden-scenarios/project-workflow.ts",
exportName: scenarioExportNames.get(scenario) || "",
timeoutMs: scenario.timeoutMs || 30000,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,83 @@ import { ok as assert } from "node:assert/strict";
import { assertEventIncludes, assertScreenIncludes } from "../testing/scenario-runner.js";
import { assertRuntimeEvent, assertsGoldenCoverage } from "../testing/portfolio-assertions.ts";

/** @typedef {import('../testing/scenario-runner.js').GoldenScenarioResult} GoldenScenarioResult */
/** @typedef {{ status?: string, path?: string }} WorkRecordStatus */
/** @typedef {{ status?: string, epicCompletionMode?: string, validatedCommit?: string, workRecord?: WorkRecordStatus }} EpicCompletionAttrs */
/** @typedef {{ attrs?: EpicCompletionAttrs }} EpicPlanState */
/** @typedef {{ plans?: EpicPlanState[] }} EpicProjectState */
import type { GoldenScenario, GoldenScenarioResult } from "../testing/mod.ts";
import type { WorktreeRegistryEntry } from "../../../shared/worktree-registry.js";

/** @param {GoldenScenarioResult} result @param {string | undefined} capability */
function assertProjectPlanReviewJourney(result, capability) {
interface WorkRecordStatus {
status?: string;
path?: string;
}
interface EpicCompletionAttrs {
status?: string;
epicCompletionMode?: string;
validatedCommit?: string;
workRecord?: WorkRecordStatus;
}
interface ProjectPlanAttrs extends EpicCompletionAttrs {
classification?: string;
}
interface EpicPlanState {
name?: string;
attrs?: ProjectPlanAttrs | null;
}
interface EpicProjectState {
plans?: EpicPlanState[];
}
interface ProjectPlanReview {
attrs?: ProjectPlanAttrs;
plan?: string;
}
interface ProjectChildPlan extends ProjectPlanAttrs {
name?: string;
order?: number;
parentPlan?: string;
}
interface ReplacedSession {
previousSessionId?: string;
currentSessionId?: string;
reason?: string;
childPlanName?: string;
}
interface ProjectPlans {
parent?: ProjectPlanAttrs;
firstChild?: ProjectPlanAttrs;
secondChild?: ProjectPlanAttrs;
}
interface ProjectDurability {
trackedFiles?: string;
primaryTrackedFiles?: string;
epicBranch?: string;
deliveryLog?: string;
liveRegistryEntries?: string[];
registryEntryCount?: number;
}
interface CapturedProjectState extends EpicProjectState {
registryEntries?: WorktreeRegistryEntry[];
nonTerminalRegistryEntries?: WorktreeRegistryEntry[];
workRecordNames?: string[];
}
interface CapturedWorkRecord extends WorkRecordStatus {
error?: string;
recordNames?: string[];
}
type LabeledPlanAttrs = [label: string, attrs: ProjectPlanAttrs | undefined];

function assertProjectPlanReviewJourney(result: GoldenScenarioResult, capability: string | undefined) {
assert(typeof capability === "string" && capability.length > 0, "Expected capability-specific PROJECT assertion.");
assertEventIncludes(result, "interaction:PLAN_REVIEW:approved");
assertEventIncludes(result, "review_approved");
assertEventIncludes(result, "runtime:tool:start:plan_written");
assertScreenIncludes(result, 'Plan "plan" approved');
const planReview = /** @type {{ attrs?: Record<string, unknown>, plan?: string } | undefined} */ (
result.state.planReview
);
const planReview = result.state.planReview as ProjectPlanReview | undefined;
assert(planReview?.attrs?.classification === "PROJECT", "Expected reviewed Plan to use PROJECT classification.");
assert(
String(planReview.plan || "").includes("Golden PROJECT revised content."),
"Expected reviewed PROJECT content to persist.",
);
}

/** @param {GoldenScenarioResult} result @param {string | undefined} capability */
function assertRuntimeSessionReplacementAndEpicEvidence(result, capability) {
function assertRuntimeSessionReplacementAndEpicEvidence(result: GoldenScenarioResult, capability: string | undefined) {
assert(typeof capability === "string" && capability.length > 0, "Expected capability-specific Epic assertion.");
assertEventIncludes(result, "project:epic:status:ready_for_decomposition");
assertEventIncludes(result, "runtime:tool:start:slicer_finalize_decomposition");
Expand All @@ -42,10 +94,7 @@ function assertRuntimeSessionReplacementAndEpicEvidence(result, capability) {

// The Slicer materialized these through the real decomposition transaction, so
// their shape is the product's output rather than fixture setup.
const children =
/** @type {Array<{ name?: string, status?: string, classification?: string, order?: unknown, parentPlan?: string }> | undefined} */ (
result.state.projectChildren
);
const children = result.state.projectChildren as ProjectChildPlan[] | undefined;
assert(children?.length === 2, `Expected two materialized child Plans; got ${JSON.stringify(children)}`);
assert(
children[0].name === "epic/child-one" && children[1].name === "epic/child-two",
Expand All @@ -61,10 +110,7 @@ function assertRuntimeSessionReplacementAndEpicEvidence(result, capability) {
`Expected the Slicer to materialize child drafts; got ${JSON.stringify(children)}`,
);

const replacement =
/** @type {{ previousSessionId?: string, currentSessionId?: string, reason?: string, childPlanName?: string } | undefined} */ (
result.state.replacedSession
);
const replacement = result.state.replacedSession as ReplacedSession | undefined;
assert(replacement?.previousSessionId && replacement?.currentSessionId, "Expected previous/current Session IDs.");
assert(
replacement.previousSessionId !== replacement.currentSessionId,
Expand All @@ -76,18 +122,15 @@ function assertRuntimeSessionReplacementAndEpicEvidence(result, capability) {
`Expected continuation into the second child ${children[1].name}; got ${replacement.childPlanName}`,
);

const plans =
/** @type {{ parent?: Record<string, unknown>, firstChild?: Record<string, unknown>, secondChild?: Record<string, unknown> } | undefined} */ (
result.state.projectPlans
);
const plans = result.state.projectPlans as ProjectPlans | undefined;
assert(plans?.parent?.classification === "PROJECT", "Expected parent Epic PROJECT metadata.");
// Both children reached a terminal verified state through their own real
// execution and Workflow Validation, not a harness status write.
for (const [label, attrs] of [
["first", plans?.firstChild],
["second", plans?.secondChild],
]) {
const childAttrs = /** @type {Record<string, unknown> | undefined} */ (attrs);
] satisfies LabeledPlanAttrs[]) {
const childAttrs = attrs;
assert(
childAttrs?.classification === "PLANNED_CHANGE",
`Expected ${label} child canonical PLANNED_CHANGE metadata.`,
Expand All @@ -98,10 +141,7 @@ function assertRuntimeSessionReplacementAndEpicEvidence(result, capability) {
);
}

const durability =
/** @type {{ trackedFiles?: string, primaryTrackedFiles?: string, epicBranch?: string, deliveryLog?: string, liveRegistryEntries?: string[], registryEntryCount?: number } | undefined} */ (
result.state.projectDurability
);
const durability = result.state.projectDurability as ProjectDurability | undefined;
// Each child ran a real Engineer turn in a real worktree and a real Direct
// Delivery merge to the Epic branch, so its file has to be tracked there and
// the primary branch must not have received partial Epic work.
Expand Down Expand Up @@ -162,14 +202,14 @@ export const projectPlanReviewScenario = {
),
assertsGoldenCoverage("durable:plan-lifecycle", (result) => {
assertEventIncludes(result, "review_approved");
const planReview = /** @type {{ attrs?: Record<string, unknown> } | undefined} */ (result.state.planReview);
const planReview = result.state.planReview as ProjectPlanReview | undefined;
assert(
["approved", "ready_for_decomposition"].includes(String(planReview?.attrs?.status || "")),
"Expected PROJECT Plan Review approval lifecycle state.",
);
}),
],
};
} satisfies GoldenScenario;

export const twoChildProjectContinuationScenario = {
// This scenario isolates automated delivery; human policy gates have dedicated journeys.
Expand Down Expand Up @@ -525,9 +565,7 @@ export const twoChildProjectContinuationScenario = {
assertions: [
assertsGoldenCoverage("durable:session-replaced", (result) => {
assertRuntimeEvent("durable:session-replaced", "runtime:session-replaced:epic_continuation")(result);
const replacement = /** @type {{ previousSessionId?: string, currentSessionId?: string } | undefined} */ (
result.state.replacedSession
);
const replacement = result.state.replacedSession as ReplacedSession | undefined;
assert(
replacement?.previousSessionId !== replacement?.currentSessionId,
"Expected typed Session replacement identity.",
Expand All @@ -538,14 +576,9 @@ export const twoChildProjectContinuationScenario = {
assertRuntimeSessionReplacementAndEpicEvidence(result, "durable:epic-evidence");
}),
assertsGoldenCoverage("durable:epic-completion", (result) => {
const projectState =
/** @type {{ plans?: Array<{ name?: string, attrs?: Record<string, unknown> | null }>, registryEntries?: unknown[], nonTerminalRegistryEntries?: unknown[], workRecordNames?: string[] } | undefined} */ (
result.state.projectState
);
const parent = /** @type {EpicCompletionAttrs | undefined} */ (
projectState?.plans?.find((plan) => plan.name === "epic")?.attrs || undefined
);
const reviewedState = /** @type {EpicProjectState} */ (result.state.reviewedEpicState);
const projectState = result.state.projectState as CapturedProjectState | undefined;
const parent = projectState?.plans?.find((plan) => plan.name === "epic")?.attrs || undefined;
const reviewedState = result.state.reviewedEpicState as EpicProjectState;
const reviewed = reviewedState.plans?.[0]?.attrs;
assert(
reviewed?.status === "reviewed",
Expand Down Expand Up @@ -589,10 +622,7 @@ export const twoChildProjectContinuationScenario = {
);
}),
assertsGoldenCoverage("durable:work-record", (result) => {
const workRecord =
/** @type {{ status?: string, path?: string, error?: string, recordNames?: string[] } | undefined} */ (
result.state.workRecord
);
const workRecord = result.state.workRecord as CapturedWorkRecord | undefined;
// Loading the externally delivered Epic writes its Work Record in the primary checkout.
assert(
workRecord?.status === "local" || workRecord?.status === "published",
Expand All @@ -608,7 +638,7 @@ export const twoChildProjectContinuationScenario = {
);
}),
],
};
} satisfies GoldenScenario;

/**
* Reuse a turn from the two-child Epic script by name.
Expand All @@ -619,7 +649,7 @@ export const twoChildProjectContinuationScenario = {
*
* @param {string} id
*/
function epicTurn(id) {
function epicTurn(id: string) {
const turn = twoChildProjectContinuationScenario.script.find((entry) => entry.id === id);
if (!turn) throw new Error(`Golden PROJECT script has no turn "${id}".`);
return turn;
Expand Down Expand Up @@ -704,14 +734,12 @@ export const projectChildCiFailureStopScenario = {
assertEventIncludes(result, "runtime:interaction_resolved");
}),
assertsGoldenCoverage("durable:epic-child-halted", (result) => {
const children = /** @type {Array<{ name?: string, status?: string }> | undefined} */ (
result.state.projectChildren
);
const children = result.state.projectChildren as ProjectChildPlan[] | undefined;
const second = children?.find((child) => String(child.name || "").includes("child-two"));
assert(second && !["verified", "user_verified"].includes(String(second.status || "")));
}),
],
};
} satisfies GoldenScenario;

export const projectWorkflowScenarios = [
projectPlanReviewScenario,
Expand Down
Loading