diff --git a/lib/mocks/scenarios.js b/lib/mocks/scenarios.js index e3fa068..91c20e1 100644 --- a/lib/mocks/scenarios.js +++ b/lib/mocks/scenarios.js @@ -1,10 +1,41 @@ /** * Expanded mock scenario library for the Confidence Engine workspace. * Each scenario produces a complete investigation journey through turns. - * + * * UI-only development work — no reasoning engine changes. */ +/* ── RTO.A4 real captured fixture (RTO.A1 superseded) ──────────── */ + +import fixtureData from "../../tests/fixtures/live-product-launch-update-response.json"; + +/** + * Thin projection of a real captured Engine update response into the + * shape already expected by the workspace/mock client. + * + * The real captured fixture remains the source of truth — no reasoning + * content is recreated or synthesized here. + */ +function projectRealUpdateFixture(fixture) { + return { + success: fixture.success, + stage: fixture.stage, + situationGraph: fixture.updatedSituationGraph, + updatedSituationGraph: fixture.updatedSituationGraph, + selectedQuestion: fixture.selectedQuestion, + newlySurfacedNodeIds: (fixture.proposal?.addedNodes || []) + .filter(node => node.kind === "unknown") + .map(node => node.id), + diagnostics: fixture.diagnostics, + noQuestionReason: null, + // Mark as experimental so buildScenarioFixture routes through the scenario branch + _experimental: { apparatusMode: "rto-real-fixture" }, + _sourceFixturePath: "../../tests/fixtures/live-product-launch-update-response.json", + }; +} + +const __rtoRealScenario = projectRealUpdateFixture(fixtureData); + /* ── Node / Edge factories ─────────────────────────────── */ export function mkN(id, label, opts) { @@ -389,6 +420,11 @@ export var MULTI_THREAD_ITEMS = multiThreadItems; /* ── Registry ─────────────────────────────────────── */ var SCENARIOS = { + "rto-real-product-launch": { + scenario: __rtoRealScenario, + label: "[RTO.A4] Real captured product-launch update", + centralStatement: __rtoRealScenario?.updatedSituationGraph?.centralStatement || "", + }, "default": { turns: comparisonTurns, label: "Comparison (product ratings)", centralStatement: comparisonTurns[0].centralStatement }, "comparison": { turns: comparisonTurns, label: "Comparison (product ratings)", centralStatement: comparisonTurns[0].centralStatement }, "contradictory": { turns: contradictoryTurns, label: "Contradictory evidence", centralStatement: contradictoryTurns[0].centralStatement }, @@ -409,6 +445,11 @@ export function buildScenarioFixture(scenarioName, turnIdx) { var s = SCENARIOS[scenarioName]; if (!s) return null; + /* RTO.A4 real fixture — returned as-is (already projected) */ + if (s.scenario && s.scenario._sourceFixturePath) { + return s.scenario; + } + /* Experimental multi-thread — does not follow the turns model */ if (s.scenario && s.scenario._experimental) { var mt = s.scenario; diff --git a/tests/experimental/rto-real-fixture-apparatus.test.js b/tests/experimental/rto-real-fixture-apparatus.test.js new file mode 100644 index 0000000..5e7317b --- /dev/null +++ b/tests/experimental/rto-real-fixture-apparatus.test.js @@ -0,0 +1,176 @@ +/** + * RTO.A4 — Real captured fixture apparatus verification. + * + * Proves the bounded projection boundary: + * 1. source fixture is the real captured JSON + * 2. projected situationGraph is the captured updatedSituationGraph + * 3. exactly 3 unresolved unknowns are derived from real graph state + * 4. their IDs/labels match the captured nodes + * 5. recommended item derives from selectedQuestion.nodeId + * 6. diagnostics recommendation agrees for this fixture + * 7. no synthetic availableThreads/recommendedThreadId data is required + * 8. any presentation selection remains outside situationGraph + * 9. active/reasoning ownership fields are not mutated + */ + +import { describe, it, expect } from "vitest"; +import rawFixture from "../fixtures/live-product-launch-update-response.json"; + +import { buildScenarioFixture } from "@/lib/mocks/scenarios.js"; + +const projected = buildScenarioFixture("rto-real-product-launch", 0); + +describe("RTO.A4 real captured fixture apparatus", () => { + /* ── 1. Source fixture is the real captured JSON ─────────── */ + + it("uses the live-product-launch-update-response.json as source", () => { + expect(projected._sourceFixturePath).toBe("../../tests/fixtures/live-product-launch-update-response.json"); + }); + + /* ── 2. Projected situationGraph is the captured updatedSituationGraph ─── */ + + it("projects situationGraph from fixture.updatedSituationGraph", () => { + // projected.situationGraph is the SAME object as projected.updatedSituationGraph (alias) + expect(projected.situationGraph).toBe(projected.updatedSituationGraph); + }); + + it("preserves centralStatement from captured graph", () => { + expect(projected.situationGraph.centralStatement.length).toBeGreaterThan(100); + }); + + /* ── 3. Exactly 3 unresolved unknowns derived from real graph state ─── */ + + it("derives exactly 3 unresolved unknowns", () => { + const unresolved = projected.situationGraph.nodes.filter( + (n) => n.kind === "unknown" && n.status !== "resolved" + ); + expect(unresolved.length).toBe(3); + }); + + /* ── 4. IDs/labels match the captured nodes ──────────────── */ + + it("resolves to the three known unresolved node IDs", () => { + const resolved = JSON.stringify(rawFixture.updatedSituationGraph.nodes + .filter(n => n.kind === "unknown" && n.status !== "resolved") + .map(n => n.id) + .sort()); + expect(resolved).toBe(JSON.stringify(["nR4vL9w", "ntpt9ki", "nxmeiab"].sort())); + }); + + it("labels come directly from captured nodes, not synthesized", () => { + const unresolved = projected.situationGraph.nodes.filter( + (n) => n.kind === "unknown" && n.status !== "resolved" + ); + const labels = unresolved.map((n) => n.label); + expect(labels).toContain( + "Probability or current status of the large enterprise customer signing their contract before launch or within the year" + ); + expect(labels).toContain( + "Whether competitors are actively developing similar products and how soon they might release them" + ); + expect(labels).toContain( + "Sufficiency of committed or highly probable revenue from other customers to cover £300k cost and yield acceptable return" + ); + }); + + /* ── 5. Recommended item derives from selectedQuestion.nodeId ─── */ + + it("derives recommendation from selectedQuestion.nodeId", () => { + expect(projected.selectedQuestion.nodeId).toBe("nR4vL9w"); + }); + + it("recommendation matches one of the unresolved graph nodes", () => { + const resolved = projected.situationGraph.nodes.find( + (n) => n.id === projected.selectedQuestion.nodeId && n.kind === "unknown" + ); + expect(resolved).toBeDefined(); + expect(resolved.label).toContain("Sufficiency of committed or highly probable revenue"); + }); + + /* ── 6. Diagnostics recommendation agrees for this fixture ── */ + + it("diagnostics.unknownSelectionExplanation.selectedNodeId agrees with selectedQuestion", () => { + expect(projected.diagnostics.unknownSelectionExplanation.selectedNodeId).toBe("nR4vL9w"); + expect(projected.selectedQuestion.nodeId).toBe( + projected.diagnostics.unknownSelectionExplanation.selectedNodeId + ); + }); + + /* ── 7. No synthetic availableThreads/recommendedThreadId required ─── */ + + it("does not require synthetic reasoning fields to establish the open set", () => { + // The open set comes from situationGraph.nodes, not from synthetic data + const graphUnknowns = projected.situationGraph.nodes.filter( + (n) => n.kind === "unknown" && n.status !== "resolved" + ); + expect(graphUnknowns.length).toBe(3); + // No _experimental or synthetic thread fields on situationGraph + expect(projected.situationGraph._experimental).toBeUndefined(); + }); + + it("does not introduce synthetic recommendationOrder, questionFrame, thread confidence, or priority", () => { + const unresolved = projected.situationGraph.nodes.filter( + (n) => n.kind === "unknown" && n.status !== "resolved" + ); + for (const node of unresolved) { + expect(node).not.toHaveProperty("recommendationOrder"); + expect(node).not.toHaveProperty("questionFrame"); + } + }); + + /* ── 8. Presentation selection remains outside situationGraph ── */ + + it("keeps presentation-only state separate from situationGraph", () => { + expect(projected.situationGraph.selectedPresentationItemId).toBeUndefined(); + }); + + it("activeUnknownNodeId is not mutated by the projection", () => { + expect(projected.situationGraph.activeUnknownNodeId).toBe( + rawFixture.updatedSituationGraph.activeUnknownNodeId + ); + }); + + it("selectedQuestion is preserved as captured, not rewritten", () => { + // selectedQuestion comes directly from the fixture — check key fields + expect(projected.selectedQuestion.nodeId).toBe(rawFixture.selectedQuestion.nodeId); + expect(projected.selectedQuestion.question).toBe(rawFixture.selectedQuestion.question); + }); + + /* ── 9. No production reasoning or ownership mutation ─────── */ + + it("does not modify activeUnknownNodeId for presentation selection", () => { + const resolved = projected.situationGraph.resolvedNodeIds || []; + expect(resolved).toEqual([]); + }); + + it("resolvedNodeIds matches captured state (empty)", () => { + const resolved = projected.situationGraph.resolvedNodeIds || []; + expect(resolved.length).toBe(0); + }); + + /* ── Additional boundary checks ─────────────────────────── */ + + it("preserves success and stage from fixture", () => { + expect(projected.success).toBe(true); + expect(projected.stage).toBe("update_applied"); + }); + + it("newlySurfacedNodeIds derives from proposal.addedNodes (unknown kind)", () => { + const expected = rawFixture.proposal.addedNodes + .filter(n => n.kind === "unknown") + .map(n => n.id); + expect(projected.newlySurfacedNodeIds).toEqual(expected); + }); + + it("selectedQuestion matches captured field exactly", () => { + expect(projected.selectedQuestion.nodeId).toBe(rawFixture.selectedQuestion.nodeId); + expect(projected.selectedQuestion.question).toBe(rawFixture.selectedQuestion.question); + }); + + it("diagnostics object matches captured diagnostics key fields", () => { + expect(projected.diagnostics.promptVersion).toBe(rawFixture.diagnostics.promptVersion); + expect(projected.diagnostics.modelName).toBe(rawFixture.diagnostics.modelName); + expect(projected.diagnostics.validationStatus).toBe(rawFixture.diagnostics.validationStatus); + expect(projected.diagnostics.nodeCount).toBe(rawFixture.diagnostics.nodeCount); + }); +});