test(experiment): ground RTO apparatus in real fixture

This commit is contained in:
2026-08-18 12:39:19 +01:00
parent 25a88c5fc3
commit 4a34dcc361
2 changed files with 218 additions and 1 deletions
+42 -1
View File
@@ -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;
@@ -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);
});
});