import { describe, test, expect } from "bun:test";
import { applyTransition, type OrchestratorState } from "../lib/state-transition-core";
const MAX_STAGE = 7;
function makeState(overrides: Partial<OrchestratorState> = {}): OrchestratorState {
const stageStatus: Record<string, string> = {};
const retryCount: Record<string, number> = {};
for (let i = 1; i <= MAX_STAGE; i++) {
stageStatus[String(i)] = "pending";
retryCount[String(i)] = 0;
}
return {
operator_name: "test_op",
schema_version: "2.0",
max_stage: MAX_STAGE,
current_stage: 1,
stage_status: stageStatus,
stage_retry_count: retryCount,
...overrides,
};
}
describe("init", () => {
test("transitions from all-pending to stage 1 in_progress", () => {
const state = makeState();
const next = applyTransition(state, { action: "init" });
expect(next.current_stage).toBe(1);
expect(next.stage_status["1"]).toBe("in_progress");
expect(next.schema_version).toBe("2.0");
});
test("rejects when a stage is already in_progress", () => {
const state = makeState({
stage_status: { ...makeState().stage_status, "1": "in_progress" },
});
expect(() => applyTransition(state, { action: "init" })).toThrow("already in_progress");
});
test("rejects target stage other than 1", () => {
const state = makeState();
expect(() => applyTransition(state, { action: "init", stage: 2 })).toThrow("stage 1");
});
});
describe("complete_stage", () => {
test("advances to next stage", () => {
const state = makeState({
current_stage: 2,
stage_status: { ...makeState().stage_status, "1": "completed", "2": "in_progress" },
});
const next = applyTransition(state, { action: "complete_stage", stage: 2 });
expect(next.stage_status["2"]).toBe("completed");
expect(next.stage_status["3"]).toBe("in_progress");
expect(next.current_stage).toBe(3);
});
test("rejects wrong stage", () => {
const state = makeState({
current_stage: 2,
stage_status: { ...makeState().stage_status, "1": "completed", "2": "in_progress" },
});
expect(() =>
applyTransition(state, { action: "complete_stage", stage: 1 }),
).toThrow("current_stage");
});
test("rejects non-in_progress stage", () => {
const state = makeState({
current_stage: 2,
stage_status: { ...makeState().stage_status, "1": "completed" },
});
expect(() =>
applyTransition(state, { action: "complete_stage", stage: 2 }),
).toThrow("in_progress");
});
test("does not advance past max_stage", () => {
const last = MAX_STAGE;
const stageStatus: Record<string, string> = {};
for (let i = 1; i <= MAX_STAGE; i++) {
stageStatus[String(i)] = i < last ? "completed" : "in_progress";
}
const state = makeState({ current_stage: last, stage_status: stageStatus });
const next = applyTransition(state, { action: "complete_stage", stage: last });
expect(next.stage_status[String(last)]).toBe("completed");
expect(next.current_stage).toBe(last);
});
test("rejects complete_stage(5) when a phase is not yet verified", () => {
const state = makeState({
current_stage: 5,
stage_status: {
...makeState().stage_status,
"1": "completed", "2": "completed", "3": "completed", "4": "completed", "5": "in_progress",
},
stage5_phases: {
active_phase: "M2",
max_cycles_per_phase: 10,
phase_status: {
M1: { status: "verified", cycles: 1 },
M2: { status: "in_debug", cycles: 1, failure_category: "precision" },
},
},
});
expect(() =>
applyTransition(state, { action: "complete_stage", stage: 5 }),
).toThrow("phase M2");
});
test("allows complete_stage(5) when all phases are verified", () => {
const state = makeState({
current_stage: 5,
stage_status: {
...makeState().stage_status,
"1": "completed", "2": "completed", "3": "completed", "4": "completed", "5": "in_progress",
},
stage5_phases: {
active_phase: null,
max_cycles_per_phase: 10,
phase_status: {
M1: { status: "verified", cycles: 1 },
M2: { status: "verified", cycles: 2 },
},
},
});
const next = applyTransition(state, { action: "complete_stage", stage: 5 });
expect(next.stage_status["5"]).toBe("completed");
expect(next.stage_status["6"]).toBe("in_progress");
});
});
describe("start_stage", () => {
test("starts a failed stage for retry", () => {
const state = makeState({
current_stage: 2,
stage_status: { ...makeState().stage_status, "1": "completed", "2": "failed" },
stage_retry_count: { ...makeState().stage_retry_count, "2": 1 },
});
const next = applyTransition(state, { action: "start_stage", stage: 2 });
expect(next.stage_status["2"]).toBe("in_progress");
expect(next.current_stage).toBe(2);
});
test("rejects if previous stage not completed", () => {
const state = makeState({ current_stage: 2 });
expect(() =>
applyTransition(state, { action: "start_stage", stage: 2 }),
).toThrow();
});
test("rejects starting a completed stage", () => {
const state = makeState({
stage_status: { ...makeState().stage_status, "1": "completed", "2": "completed" },
});
expect(() =>
applyTransition(state, { action: "start_stage", stage: 2 }),
).toThrow("already completed");
});
});
describe("fail_stage", () => {
test("marks stage as failed and increments retry", () => {
const state = makeState({
current_stage: 2,
stage_status: { ...makeState().stage_status, "1": "completed", "2": "in_progress" },
});
const result = applyTransition(state, { action: "fail_stage", stage: 2 });
expect(result.stage_status["2"]).toBe("failed");
expect(result.stage_retry_count["2"]).toBe(1);
});
});
describe("Stage 5 phase loop", () => {
function stage5Ready(): OrchestratorState {
return makeState({
current_stage: 5,
stage_status: {
...makeState().stage_status,
"1": "completed", "2": "completed", "3": "completed", "4": "completed", "5": "in_progress",
},
});
}
test("start_phase initialises the phase entry", () => {
const next = applyTransition(stage5Ready(), { action: "start_phase", stage: 5, phase: "M1" });
expect(next.stage5_phases?.active_phase).toBe("M1");
expect(next.stage5_phases?.phase_status.M1.status).toBe("in_progress");
expect(next.stage5_phases?.phase_status.M1.cycles).toBe(0);
});
test("fail_phase increments cycles and stores failure_category", () => {
let s = applyTransition(stage5Ready(), { action: "start_phase", stage: 5, phase: "M1" });
s = applyTransition(s, {
action: "fail_phase",
stage: 5,
phase: "M1",
failure_category: "precision",
last_error: "max_diff=3.2e-4 vs atol=1e-6",
});
expect(s.stage5_phases?.phase_status.M1.status).toBe("in_debug");
expect(s.stage5_phases?.phase_status.M1.cycles).toBe(1);
expect(s.stage5_phases?.phase_status.M1.failure_category).toBe("precision");
expect(s.stage5_phases?.phase_status.M1.last_error).toContain("max_diff");
});
test("10 consecutive fail_phase calls mark the phase blocked", () => {
let s = applyTransition(stage5Ready(), { action: "start_phase", stage: 5, phase: "M1" });
for (let i = 0; i < 10; i++) {
s = applyTransition(s, {
action: "fail_phase",
stage: 5,
phase: "M1",
failure_category: "precision",
});
}
expect(s.stage5_phases?.phase_status.M1.status).toBe("blocked");
expect(s.stage5_phases?.phase_status.M1.cycles).toBe(10);
});
test("complete_phase marks phase verified and clears active_phase", () => {
let s = applyTransition(stage5Ready(), { action: "start_phase", stage: 5, phase: "M1" });
s = applyTransition(s, { action: "complete_phase", stage: 5, phase: "M1" });
expect(s.stage5_phases?.phase_status.M1.status).toBe("verified");
expect(s.stage5_phases?.phase_status.M1.verified_at).toBeDefined();
expect(s.stage5_phases?.active_phase).toBeNull();
});
test("rejects starting a second phase while another is in_progress", () => {
let s = applyTransition(stage5Ready(), { action: "start_phase", stage: 5, phase: "M1" });
expect(() =>
applyTransition(s, { action: "start_phase", stage: 5, phase: "M2" }),
).toThrow("already in_progress");
});
test("rejects starting an already-verified phase", () => {
let s = applyTransition(stage5Ready(), { action: "start_phase", stage: 5, phase: "M1" });
s = applyTransition(s, { action: "complete_phase", stage: 5, phase: "M1" });
expect(() =>
applyTransition(s, { action: "start_phase", stage: 5, phase: "M1" }),
).toThrow("already verified");
});
test("phase actions are rejected outside stage 5", () => {
expect(() =>
applyTransition(makeState(), { action: "start_phase" as never, stage: 4 as never, phase: "M1" }),
).toThrow();
});
});
describe("record_artifact_hash", () => {
test("stores the named hash", () => {
const next = applyTransition(makeState(), {
action: "record_artifact_hash",
name: "spec_md",
hash: "abcdef1234",
});
expect(next.artifact_hashes?.spec_md).toBe("abcdef1234");
});
test("rejects empty name or hash", () => {
expect(() =>
applyTransition(makeState(), {
action: "record_artifact_hash",
name: "",
hash: "x",
}),
).toThrow("name");
expect(() =>
applyTransition(makeState(), {
action: "record_artifact_hash",
name: "spec_md",
hash: "",
}),
).toThrow("hash");
});
});
describe("rollback_to_stage", () => {
function stage5InDebug(): OrchestratorState {
return makeState({
current_stage: 5,
stage_status: {
...makeState().stage_status,
"1": "completed",
"2": "completed",
"3": "completed",
"4": "completed",
"5": "in_progress",
},
stage_retry_count: { ...makeState().stage_retry_count, "3": 0 },
artifact_hashes: {
spec_md: "spec-hash",
design_md: "design-hash",
module_interfaces_yaml: "yaml-hash",
},
stage5_phases: {
active_phase: "M2",
max_cycles_per_phase: 10,
phase_status: {
M1: { status: "verified", cycles: 1 },
M2: { status: "in_debug", cycles: 3, failure_category: "fp32_unstable" },
},
},
});
}
test("resets every stage after target to pending", () => {
const next = applyTransition(stage5InDebug(), {
action: "rollback_to_stage",
target_stage: 3,
reason: "fp32_unstable: log-sum-exp shift missing",
failure_category: "fp32_unstable",
failed_phase: "M2",
});
expect(next.current_stage).toBe(3);
expect(next.stage_status["3"]).toBe("in_progress");
expect(next.stage_status["4"]).toBe("pending");
expect(next.stage_status["5"]).toBe("pending");
expect(next.stage_status["6"]).toBe("pending");
});
test("increments retry_count on target stage", () => {
const next = applyTransition(stage5InDebug(), {
action: "rollback_to_stage",
target_stage: 3,
reason: "fp32_unstable",
});
expect(next.stage_retry_count["3"]).toBe(1);
});
test("wipes stage5_phases when rolling back to stage < 5", () => {
const next = applyTransition(stage5InDebug(), {
action: "rollback_to_stage",
target_stage: 3,
reason: "fp32_unstable",
});
expect(next.stage5_phases?.phase_status).toEqual({});
expect(next.stage5_phases?.active_phase).toBeNull();
});
test("drops artifact hashes for stages after target", () => {
const next = applyTransition(stage5InDebug(), {
action: "rollback_to_stage",
target_stage: 3,
reason: "design rework",
});
expect(next.artifact_hashes?.spec_md).toBe("spec-hash");
expect(next.artifact_hashes?.module_interfaces_yaml).toBeUndefined();
expect(next.artifact_hashes?.design_md).toBe("design-hash");
});
test("appends an entry to rollback_history", () => {
const next = applyTransition(stage5InDebug(), {
action: "rollback_to_stage",
target_stage: 3,
reason: "fp32_unstable in M2",
failure_category: "fp32_unstable",
failed_phase: "M2",
});
expect(next.rollback_history?.length).toBe(1);
const entry = next.rollback_history![0];
expect(entry.from_stage).toBe(5);
expect(entry.from_phase).toBe("M2");
expect(entry.to_stage).toBe(3);
expect(entry.failure_category).toBe("fp32_unstable");
expect(entry.reason).toContain("fp32_unstable");
expect(entry.timestamp).toBeDefined();
});
test("rejects rollback to current or future stage", () => {
const state = stage5InDebug();
expect(() =>
applyTransition(state, {
action: "rollback_to_stage",
target_stage: 5,
reason: "x",
}),
).toThrow("strictly less than");
expect(() =>
applyTransition(state, {
action: "rollback_to_stage",
target_stage: 6,
reason: "x",
}),
).toThrow();
});
test("rejects rollback without reason", () => {
const state = stage5InDebug();
expect(() =>
applyTransition(state, {
action: "rollback_to_stage",
target_stage: 3,
reason: "",
}),
).toThrow("reason");
});
test("rollback within stage 5 (target=5 invalid since current_stage=5)", () => {
expect(() =>
applyTransition(stage5InDebug(), {
action: "rollback_to_stage",
target_stage: 5,
reason: "x",
}),
).toThrow();
});
test("rollback from stage 7 to stage 5 keeps stage5_phases (since target>=5)", () => {
const stage7: OrchestratorState = makeState({
current_stage: 7,
stage_status: {
...makeState().stage_status,
"1": "completed", "2": "completed", "3": "completed", "4": "completed",
"5": "completed", "6": "completed", "7": "in_progress",
},
stage5_phases: {
active_phase: null,
max_cycles_per_phase: 10,
phase_status: {
M1: { status: "verified", cycles: 1 },
M2: { status: "verified", cycles: 1 },
},
},
});
const next = applyTransition(stage7, {
action: "rollback_to_stage",
target_stage: 5,
reason: "perf regression",
});
expect(next.current_stage).toBe(5);
expect(next.stage_status["5"]).toBe("in_progress");
expect(next.stage_status["7"]).toBe("pending");
expect(next.stage5_phases?.phase_status.M1.status).toBe("verified");
});
});
describe("dynamic max_stage", () => {
test("max_stage of 10 allows complete_stage(9) and (10)", () => {
const stageStatus: Record<string, string> = {};
const retryCount: Record<string, number> = {};
for (let i = 1; i <= 10; i++) {
stageStatus[String(i)] = i < 9 ? "completed" : i === 9 ? "in_progress" : "pending";
retryCount[String(i)] = 0;
}
const state: OrchestratorState = {
operator_name: "extended_op",
schema_version: "2.0",
max_stage: 10,
current_stage: 9,
stage_status: stageStatus,
stage_retry_count: retryCount,
};
const after9 = applyTransition(state, { action: "complete_stage", stage: 9 });
expect(after9.current_stage).toBe(10);
expect(after9.stage_status["10"]).toBe("in_progress");
const after10 = applyTransition(after9, { action: "complete_stage", stage: 10 });
expect(after10.stage_status["10"]).toBe("completed");
expect(after10.current_stage).toBe(10);
});
test("rejects stage > max_stage", () => {
expect(() =>
applyTransition(makeState(), { action: "fail_stage", stage: 8 }),
).toThrow("must be 1..7");
});
});