Add fixture-only apparatus for representing multiple concurrent open investigation items within a fixed case context. New scenario 'multi-thread' exposes: - A fixed central situation statement and case summary (product-launch timing decision, drawn from existing pre-anchored-product-launch data) - Three open investigation items — none compulsory: enterprise customer signing probability, competitor timing, financial viability comparison - One engine recommendation (mt-ent-customer-signing, ordered first) - User selection of any item; chosen item becomes visually primary while others remain visible as context - Experimental state isolated in _experimental / _experimentalState — never aliases production graph fields
222 lines
8.1 KiB
JavaScript
222 lines
8.1 KiB
JavaScript
/**
|
|
* RTO.A1 — Multi-thread apparatus verification.
|
|
*
|
|
* Validates that the experimental fixture can deterministically represent:
|
|
* - a fixed case understanding
|
|
* - three open investigation items (none compulsory)
|
|
* - one engine recommendation
|
|
* - user selection of any item
|
|
* - chosen-item becomes visually primary in presentation
|
|
* - other items remain visible as context
|
|
* - no claim that production reasoning ownership changed
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
import {
|
|
MULTI_THREAD_FIXTURE,
|
|
MULTI_THREAD_CASE,
|
|
MULTI_THREAD_ITEMS,
|
|
buildScenarioFixture,
|
|
} from "@/lib/mocks/scenarios.js";
|
|
|
|
describe("RTO.A1 multi-thread apparatus", () => {
|
|
/* Reset shared experimental state between tests */
|
|
beforeEach(() => {
|
|
const exp = MULTI_THREAD_FIXTURE._experimental;
|
|
if (exp) exp.selectedThreadId = null;
|
|
});
|
|
describe("fixed case representation", () => {
|
|
it("exposes a case summary without implying a single active unknown", () => {
|
|
expect(MULTI_THREAD_FIXTURE.centralStatement).toBeDefined();
|
|
expect(MULTI_THREAD_FIXTURE._experimental.caseContext.caseSummary.length).toBeGreaterThan(0);
|
|
expect(MULTI_THREAD_FIXTURE.active).toBeNull();
|
|
});
|
|
|
|
it("uses the existing launch-vs-wait central statement", () => {
|
|
expect(MULTI_THREAD_CASE.centralStatement).toContain("launching");
|
|
expect(MULTI_THREAD_CASE.centralStatement).toContain("waiting twelve months");
|
|
});
|
|
});
|
|
|
|
describe("open investigation items", () => {
|
|
it("represents exactly three open items", () => {
|
|
expect(MULTI_THREAD_ITEMS.length).toBe(3);
|
|
});
|
|
|
|
it("each item has an id, label and description", () => {
|
|
MULTI_THREAD_ITEMS.forEach((item) => {
|
|
expect(item.id).toBeDefined();
|
|
expect(item.label).toBeDefined();
|
|
expect(item.description).toBeDefined();
|
|
});
|
|
});
|
|
|
|
it("none is compulsory — all items are selectable", () => {
|
|
MULTI_THREAD_ITEMS.forEach((item) => {
|
|
// Items lack a `compulsory` flag; selection is always open
|
|
expect(item.compulsory).not.toBe(true);
|
|
});
|
|
});
|
|
|
|
it("items map to the three investigation areas from RTO.01", () => {
|
|
const labels = MULTI_THREAD_ITEMS.map((i) => i.label.toLowerCase());
|
|
expect(labels).toContainEqual(expect.stringContaining("enterprise customer"));
|
|
expect(labels).toContainEqual(expect.stringContaining("competitor timing"));
|
|
expect(labels).toContainEqual(expect.stringContaining("financial viability"));
|
|
});
|
|
});
|
|
|
|
describe("engine recommendation", () => {
|
|
it("marks exactly one item as the engine recommendation", () => {
|
|
const recommended = MULTI_THREAD_FIXTURE._experimental.recommendedThreadId;
|
|
expect(recommended).toBe("mt-ent-customer-signing");
|
|
|
|
const matched = MULTI_THREAD_ITEMS.find(
|
|
(i) => i.id === recommended
|
|
);
|
|
expect(matched).toBeDefined();
|
|
});
|
|
|
|
it("the recommendation is the first item by ordering", () => {
|
|
const ordered = [...MULTI_THREAD_ITEMS].sort(
|
|
(a, b) => a.recommendationOrder - b.recommendationOrder
|
|
);
|
|
expect(ordered[0].id).toBe("mt-ent-customer-signing");
|
|
});
|
|
});
|
|
|
|
describe("experimental user selection", () => {
|
|
it("allows selection of any available thread", () => {
|
|
const exp = MULTI_THREAD_FIXTURE._experimental;
|
|
// Initially no selection
|
|
expect(exp.selectedThreadId).toBeNull();
|
|
|
|
// Simulate user selecting the second item
|
|
exp.selectedThreadId = "mt-competitor-timing";
|
|
expect(exp.selectedThreadId).toBe("mt-competitor-timing");
|
|
|
|
// Verify it is still a valid available thread
|
|
const found = MULTI_THREAD_ITEMS.find(
|
|
(i) => i.id === exp.selectedThreadId
|
|
);
|
|
expect(found).toBeDefined();
|
|
expect(found.label).toContain("Competitor timing");
|
|
});
|
|
|
|
it("changing selection does not affect other items", () => {
|
|
const exp = MULTI_THREAD_FIXTURE._experimental;
|
|
const othersBefore = MULTI_THREAD_ITEMS.map((i) => ({
|
|
id: i.id,
|
|
recommended: i.id === exp.recommendedThreadId,
|
|
}));
|
|
|
|
// Switch to third item
|
|
exp.selectedThreadId = "mt-financial-comparison";
|
|
expect(exp.selectedThreadId).toBe("mt-financial-comparison");
|
|
|
|
const othersAfter = MULTI_THREAD_ITEMS.map((i) => ({
|
|
id: i.id,
|
|
recommended: i.id === exp.recommendedThreadId,
|
|
}));
|
|
|
|
// Structure is preserved — only selectedThreadId changed
|
|
expect(othersBefore).toEqual(othersAfter);
|
|
});
|
|
});
|
|
|
|
describe("chosen-thread primary state", () => {
|
|
it("marks the selected item as visually primary when set", () => {
|
|
const exp = MULTI_THREAD_FIXTURE._experimental;
|
|
const targetId = "mt-ent-customer-signing";
|
|
exp.selectedThreadId = targetId;
|
|
|
|
const primaryItem = MULTI_THREAD_ITEMS.find(
|
|
(i) => i.id === targetId
|
|
);
|
|
expect(primaryItem).toBeDefined();
|
|
});
|
|
|
|
it("other items remain visible as case-file context", () => {
|
|
const exp = MULTI_THREAD_FIXTURE._experimental;
|
|
exp.selectedThreadId = "mt-competitor-timing";
|
|
|
|
// All three items are still present — none were removed
|
|
expect(MULTI_THREAD_ITEMS.length).toBe(3);
|
|
|
|
// The non-selected items are still available
|
|
const remaining = MULTI_THREAD_ITEMS.filter(
|
|
(i) => i.id !== exp.selectedThreadId
|
|
);
|
|
expect(remaining.length).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe("production boundary", () => {
|
|
it("does not set production activeUnknownNodeId semantics", () => {
|
|
const graph = MULTI_THREAD_FIXTURE.situationGraph;
|
|
expect(graph.activeUnknownNodeId).toBeNull();
|
|
});
|
|
|
|
it("keeps _experimental separate from situationGraph", () => {
|
|
const exp = MULTI_THREAD_FIXTURE._experimental;
|
|
expect(exp.apparatusMode).toBe("selection");
|
|
expect(exp.selectedThreadId).toBeNull();
|
|
// The experimental state is NOT in situationGraph
|
|
expect(MULTI_THREAD_FIXTURE.situationGraph.exp).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("buildScenarioFixture integration", () => {
|
|
it("returns the multi-thread fixture for scenario 'multi-thread'", () => {
|
|
const fixture = buildScenarioFixture("multi-thread", 0);
|
|
expect(fixture).not.toBeNull();
|
|
expect(fixture.success).toBe(true);
|
|
expect(fixture._experimentalState).toBeDefined();
|
|
expect(fixture._experimentalState.availableThreads.length).toBe(3);
|
|
expect(fixture._experimentalState.recommendedThreadId).toBe("mt-ent-customer-signing");
|
|
});
|
|
|
|
it("preserves the case context through buildScenarioFixture", () => {
|
|
const fixture = buildScenarioFixture("multi-thread", 0);
|
|
expect(fixture.situationGraph.centralStatement).toBe(
|
|
MULTI_THREAD_CASE.centralStatement
|
|
);
|
|
});
|
|
|
|
it("does not return a production question when multi-thread is active", () => {
|
|
const fixture = buildScenarioFixture("multi-thread", 0);
|
|
expect(fixture.selectedQuestion).toBeNull();
|
|
expect(fixture.noQuestionReason).toContain("Multi-thread");
|
|
});
|
|
|
|
it("returns null for unknown scenario name", () => {
|
|
expect(buildScenarioFixture("nonexistent-scenario", 0)).toBeNull();
|
|
});
|
|
|
|
it("returns null when scenario has no turns or scenario field", () => {
|
|
// This should not break — SCENARIOS entries without either field
|
|
const oldEntry = { someField: "value" };
|
|
expect(buildScenarioFixture("__nonexistent__", 0)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("scenario fixture structure correctness", () => {
|
|
it("includes nodes and edges in the situation graph", () => {
|
|
const g = MULTI_THREAD_FIXTURE.situationGraph;
|
|
expect(g.nodes.length).toBeGreaterThan(0);
|
|
expect(g.edges.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("nodes contain valid observation kind entries", () => {
|
|
const observationNodes = MULTI_THREAD_FIXTURE.situationGraph.nodes.filter(
|
|
(n) => n.kind === "observation"
|
|
);
|
|
expect(observationNodes.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
it("includes the experimental scenario identifier", () => {
|
|
expect(MULTI_THREAD_FIXTURE._experimental.scenario).toBe("multi-thread");
|
|
});
|
|
});
|
|
});
|