diff --git a/tests/fixtures/pre-anchored-product-launch-customer-signing.json b/tests/fixtures/pre-anchored-product-launch-customer-signing.json new file mode 100644 index 0000000..6d0710b --- /dev/null +++ b/tests/fixtures/pre-anchored-product-launch-customer-signing.json @@ -0,0 +1,126 @@ +{ + "description": "Deterministic pre-anchored product-launch customer-signing follow-up fixture — represents the confirmed state immediately before the material customer-signing answer.", + "scenario": "We are evaluating two product-launch timing options: launching the new software product this year or waiting twelve months.", + "unresolved_question": "Which option leaves us better off overall?", + "selectedQuestion": { + "nodeId": "n_enterprise_customer_signing", + "question": "What evidence would clarify whether one prospective enterprise customer will sign if we launch this year?", + "reason": "decision" + }, + "graph": { + "centralStatement": "We are evaluating two product-launch timing options: launching the new software product this year or waiting twelve months.", + "nodes": [ + { + "id": "n_product_launch_state", + "label": "Product launch timing consideration", + "description": "Current state: the organisation is evaluating whether to launch the new software product this year or wait twelve months.", + "kind": "state", + "status": "provisional", + "confidence": "high", + "value": null, + "unit": null, + "evidenceIds": [], + "dependsOn": [], + "affects": [], + "parentId": null, + "childIds": [] + }, + { + "id": "opt_launch_this_year", + "label": "Launch this year", + "description": "Launch the new software product this year. Consequences: £1.2M/year expected additional recurring revenue, £300k one-off launch/support cost.", + "kind": "option", + "status": "known", + "confidence": "high", + "value": null, + "unit": null, + "evidenceIds": [], + "dependsOn": [], + "affects": [], + "parentId": null, + "childIds": [] + }, + { + "id": "opt_wait_twelve_months", + "label": "Wait twelve months", + "description": "Wait twelve months before launching the product. Consequences: avoids the £300k immediate launch/support cost, forgoes approximately £1.2M of first-year revenue.", + "kind": "option", + "status": "known", + "confidence": "high", + "value": null, + "unit": null, + "evidenceIds": [], + "dependsOn": [], + "affects": [], + "parentId": null, + "childIds": [] + }, + { + "id": "n_product_launch_decision", + "label": "Which option leaves us better off overall?", + "description": "Uncertainty about which of the two product-launch timing options — launch this year or wait twelve months — provides superior net value for the organisation.", + "kind": "unknown", + "status": "unknown", + "confidence": "medium", + "value": null, + "unit": null, + "evidenceIds": [], + "dependsOn": [], + "affects": [], + "parentId": null, + "childIds": [] + }, + { + "id": "n_enterprise_customer_signing", + "label": "Prospective enterprise customer signing status", + "description": "Unknown whether one prospective enterprise customer will sign if we launch this year, because they account for approximately £700,000 of the £1.2 million expected annual revenue.", + "kind": "unknown", + "status": "unknown", + "confidence": "medium", + "value": null, + "unit": null, + "evidenceIds": [], + "dependsOn": [], + "affects": [], + "parentId": null, + "childIds": [] + } + ], + "edges": [ + { + "id": "e-opt-launch-to-dec", + "fromNodeId": "opt_launch_this_year", + "toNodeId": "n_product_launch_decision", + "relationship": "contained_in", + "confidence": "high", + "description": "Launch this year option is a candidate for the product launch decision" + }, + { + "id": "e-opt-wait-to-dec", + "fromNodeId": "opt_wait_twelve_months", + "toNodeId": "n_product_launch_decision", + "relationship": "contained_in", + "confidence": "high", + "description": "Wait twelve months option is a candidate for the product launch decision" + }, + { + "id": "e-customer-signing-to-launch-option", + "fromNodeId": "n_enterprise_customer_signing", + "toNodeId": "opt_launch_this_year", + "relationship": "contained_in", + "confidence": "high", + "description": "Prospective enterprise customer signing status is material to the launch this year option" + } + ], + "activeUnknownNodeId": "n_enterprise_customer_signing", + "resolvedNodeIds": [], + "currentSummary": "Two product-launch timing options evaluated; net-value comparison unresolved; prospective enterprise customer signing remains the active material uncertainty.", + "reasoningState": { + "comparabilityStatus": null, + "relationshipStatus": null, + "relationshipAssessed": false, + "contradictionReasoningAllowed": true, + "reasoningStages": [] + } + } +} \ No newline at end of file diff --git a/tests/reproduce-multi-turn-investigation.harness.test.js b/tests/reproduce-multi-turn-investigation.harness.test.js index bcedf2c..0c05431 100644 --- a/tests/reproduce-multi-turn-investigation.harness.test.js +++ b/tests/reproduce-multi-turn-investigation.harness.test.js @@ -2,6 +2,11 @@ import { describe, it, expect } from "vitest"; import fs from "fs"; import { fileURLToPath } from "url"; import path from "path"; +import { situationGraphSchema } from "@/lib/graph/schema.js"; +import { + validateGraphReferences, + detectDuplicateNodeIds, +} from "@/lib/graph/utils.js"; // ── Load committed decision-options fixture directly (60A.7) ───────────── const DECISION_OPTIONS_FIXTURE_PATH = path.resolve( @@ -10,6 +15,14 @@ const DECISION_OPTIONS_FIXTURE_PATH = path.resolve( ); const DECISION_OPTIONS_FIXTURE = JSON.parse(fs.readFileSync(DECISION_OPTIONS_FIXTURE_PATH, "utf-8")); +const PRODUCT_LAUNCH_CUSTOMER_SIGNING_FIXTURE_PATH = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "fixtures/pre-anchored-product-launch-customer-signing.json", +); +const PRODUCT_LAUNCH_CUSTOMER_SIGNING_FIXTURE = JSON.parse( + fs.readFileSync(PRODUCT_LAUNCH_CUSTOMER_SIGNING_FIXTURE_PATH, "utf-8"), +); + /** * Synchronous simulator of the pre-anchored update-only mode with a custom fixture. * Mirrors what reproduce-multi-turn-investigation.mjs does when FIXTURE_MODE is set @@ -1372,6 +1385,62 @@ describe("reproduce-multi-turn-investigation harness: one-shot semantics", () => }); }); +describe("pre-anchored product-launch customer-signing fixture", () => { + it("parses, validates, preserves identities, and exposes the customer unknown as the active target", () => { + const fixture = PRODUCT_LAUNCH_CUSTOMER_SIGNING_FIXTURE; + const graph = situationGraphSchema.parse(fixture.graph); + const graphReferenceValidation = validateGraphReferences(graph); + const duplicateNodeIds = detectDuplicateNodeIds(graph.nodes); + + const decisionNodes = graph.nodes.filter((node) => node.id === "n_product_launch_decision"); + const launchOptionNodes = graph.nodes.filter((node) => node.id === "opt_launch_this_year"); + const waitOptionNodes = graph.nodes.filter((node) => node.id === "opt_wait_twelve_months"); + const customerUnknownNodes = graph.nodes.filter( + (node) => node.id === "n_enterprise_customer_signing", + ); + const additionalUnknownIds = graph.nodes + .filter( + (node) => + node.kind === "unknown" && + ![ + "n_product_launch_decision", + "n_enterprise_customer_signing", + ].includes(node.id), + ) + .map((node) => node.id); + + expect(fixture.selectedQuestion?.question).toBe( + "What evidence would clarify whether one prospective enterprise customer will sign if we launch this year?", + ); + expect(fixture.selectedQuestion?.nodeId).toBe("n_enterprise_customer_signing"); + + expect(decisionNodes).toHaveLength(1); + expect(decisionNodes[0].status).toBe("unknown"); + + expect(launchOptionNodes).toHaveLength(1); + expect(waitOptionNodes).toHaveLength(1); + + expect(customerUnknownNodes).toHaveLength(1); + expect(customerUnknownNodes[0].status).toBe("unknown"); + expect(customerUnknownNodes[0].kind).toBe("unknown"); + + expect(additionalUnknownIds).toEqual([]); + expect(graphReferenceValidation.valid).toBe(true); + expect(duplicateNodeIds).toEqual([]); + + expect(graph.activeUnknownNodeId).toBe("n_enterprise_customer_signing"); + expect(customerUnknownNodes[0].id).toBe(graph.activeUnknownNodeId); + + const customerLinkage = graph.edges.filter( + (edge) => + edge.fromNodeId === "n_enterprise_customer_signing" && + edge.toNodeId === "opt_launch_this_year" && + edge.relationship === "contained_in", + ); + expect(customerLinkage).toHaveLength(1); + }); +}); + // ── Pre-anchored update-only mode (57J.74) ───────────── /**