fix(reasoning): preserve investigation ownership across selection and question rejection
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js";
|
||||
import { determineGraphBackedQuestion } from "@/lib/graph/apply-proposal.js";
|
||||
import { validateGraphReferences } from "@/lib/graph/utils.js";
|
||||
import { makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||
import liveProductLaunchStartResponse from "@/tests/fixtures/live-product-launch-start-response.json";
|
||||
|
||||
const mockAnalyseScenario = vi.fn();
|
||||
const MOCK_CONFIG = { OLLAMA_MODEL: "configured" };
|
||||
@@ -588,6 +590,46 @@ describe("lib/graph/orchestrator startCase", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("retains ownership when the strongest target's formulated question is rejected", () => {
|
||||
const situationGraph = structuredClone(
|
||||
liveProductLaunchStartResponse.situationGraph,
|
||||
);
|
||||
|
||||
const result = determineGraphBackedQuestion({ situationGraph });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.deterministicSelection?.nodeId ?? null).toBe("ntpt9ki");
|
||||
expect(result.updatedSituationGraph.activeUnknownNodeId).toBe("ntpt9ki");
|
||||
expect(result.selectedQuestion).toBeNull();
|
||||
expect(result.noQuestionReason).toBe(
|
||||
"The selected investigation target remains active, but its current graph-backed question formulation was rejected as too complex.",
|
||||
);
|
||||
expect(result.selectedChildUnknown).toBe("ntpt9ki");
|
||||
expect(result.selectedUnknownAfter).toBe("ntpt9ki");
|
||||
expect(result.selectedQuestion?.nodeId ?? null).toBe(null);
|
||||
});
|
||||
|
||||
it("replays the captured live product-launch start graph through deterministic graph-backed question selection", () => {
|
||||
const situationGraph = liveProductLaunchStartResponse.situationGraph;
|
||||
|
||||
const result = determineGraphBackedQuestion({ situationGraph });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.updatedSituationGraph.activeUnknownNodeId).toBe("ntpt9ki");
|
||||
expect(result.deterministicSelection?.nodeId ?? null).toBe("ntpt9ki");
|
||||
expect(result.answerabilityAssessment?.independentlyAnswerable).toBe(false);
|
||||
expect(result.answerabilityAssessment?.decompositionRequired).toBe(true);
|
||||
expect(result.decompositionAttempted).toBe(true);
|
||||
expect(result.decompositionPerformed).toBe(false);
|
||||
expect(result.selectedChildUnknown).toBe("ntpt9ki");
|
||||
expect(result.selectedUnknownAfter).toBe("ntpt9ki");
|
||||
expect(result.selectedQuestion).toBeNull();
|
||||
expect(result.noQuestionReason).toBe(
|
||||
"The selected investigation target remains active, but its current graph-backed question formulation was rejected as too complex.",
|
||||
);
|
||||
expect(result.deterministicSelection?.nodeId).not.toBe("nxmeiab");
|
||||
});
|
||||
|
||||
it("includes compatibility diagnostics when provided by analysis", async () => {
|
||||
mockAnalyseScenario.mockResolvedValue(
|
||||
makeAnalysisResult({
|
||||
|
||||
+131
-11
@@ -621,7 +621,7 @@ describe("selectActiveUnknownCandidate", () => {
|
||||
expect(parentScore.score).toBeGreaterThan(childScore.score);
|
||||
});
|
||||
|
||||
it("returns ambiguous for a complete unresolved tie instead of label-based winner", () => {
|
||||
it("returns a deterministic winner for a complete unresolved tie", () => {
|
||||
const unknownA = makeNode({
|
||||
id: "tie-a",
|
||||
label: "Magnitude and nature of cash outflows",
|
||||
@@ -651,16 +651,12 @@ describe("selectActiveUnknownCandidate", () => {
|
||||
});
|
||||
|
||||
const result = selectActiveUnknownCandidate(graph, []);
|
||||
expect(result).toMatchObject({
|
||||
selectedNode: null,
|
||||
status: "ambiguous",
|
||||
tieType: "complete_unresolved_tie",
|
||||
tiedCandidateIds: ["tie-a", "tie-b"],
|
||||
});
|
||||
expect(result.nodeId).toBeUndefined();
|
||||
expect(result.status).toBe("selected");
|
||||
expect(result.tieType).toBe("complete_unresolved_tie");
|
||||
expect(result.nodeId).toBeTruthy();
|
||||
});
|
||||
|
||||
it("alphabetical renaming does not resolve a complete tie", () => {
|
||||
it("stable ordering resolves a complete tie deterministically", () => {
|
||||
const unknownA = makeNode({
|
||||
id: "tie-a",
|
||||
label: "Unknown B",
|
||||
@@ -683,8 +679,132 @@ describe("selectActiveUnknownCandidate", () => {
|
||||
});
|
||||
|
||||
const result = selectActiveUnknownCandidate(graph, []);
|
||||
expect(result.status).toBe("ambiguous");
|
||||
expect(result.tiedCandidateIds.sort()).toEqual(["tie-a", "tie-b"]);
|
||||
expect(result.status).toBe("selected");
|
||||
expect(result.nodeId).toBeTruthy();
|
||||
});
|
||||
|
||||
it("preserves the active candidate when it remains eligible and substantively tied", () => {
|
||||
const unknownA = makeNode({
|
||||
id: "tie-a",
|
||||
label: "Unknown B",
|
||||
description: "Unknown factor one.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
});
|
||||
const unknownB = makeNode({
|
||||
id: "tie-b",
|
||||
label: "Unknown A",
|
||||
description: "Unknown factor two.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
});
|
||||
const graph = makeGraph({
|
||||
centralStatement: "Two conflicting signals remain unresolved.",
|
||||
nodes: [unknownA, unknownB],
|
||||
edges: [],
|
||||
activeUnknownNodeId: "tie-a",
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "Tie case",
|
||||
});
|
||||
|
||||
const result = selectActiveUnknownCandidate(graph, []);
|
||||
expect(result.status).toBe("selected");
|
||||
expect(result.nodeId).toBe("tie-a");
|
||||
});
|
||||
|
||||
it("transfers ownership when the active candidate substantively loses on score", () => {
|
||||
const loser = makeNode({
|
||||
id: "active-loser",
|
||||
label: "Unknown branch",
|
||||
description: "Speculative future branch.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
});
|
||||
const winner = makeNode({
|
||||
id: "clear-winner",
|
||||
label: "Customer value definition",
|
||||
description: "Need customer value definition.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
});
|
||||
const graph = makeGraph({
|
||||
centralStatement: "Decision context",
|
||||
nodes: [loser, winner],
|
||||
edges: [],
|
||||
activeUnknownNodeId: loser.id,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "Score loss case",
|
||||
});
|
||||
|
||||
const result = selectActiveUnknownCandidate(graph, []);
|
||||
expect(result.status).toBe("selected");
|
||||
expect(result.nodeId).toBe(winner.id);
|
||||
});
|
||||
|
||||
it("transfers ownership when the active candidate is resolved or ineligible", () => {
|
||||
const resolvedActive = makeNode({
|
||||
id: "resolved-active",
|
||||
label: "Resolved unknown",
|
||||
description: "Resolved unknown.",
|
||||
kind: "unknown",
|
||||
status: "resolved",
|
||||
confidence: "high",
|
||||
});
|
||||
const remaining = makeNode({
|
||||
id: "remaining-unknown",
|
||||
label: "Customer value definition",
|
||||
description: "Need customer value definition.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
});
|
||||
const graph = makeGraph({
|
||||
centralStatement: "Decision context",
|
||||
nodes: [resolvedActive, remaining],
|
||||
edges: [],
|
||||
activeUnknownNodeId: resolvedActive.id,
|
||||
resolvedNodeIds: [resolvedActive.id],
|
||||
currentSummary: "Resolved active case",
|
||||
});
|
||||
|
||||
const result = selectActiveUnknownCandidate(graph, graph.resolvedNodeIds);
|
||||
expect(result.status).toBe("selected");
|
||||
expect(result.nodeId).toBe(remaining.id);
|
||||
});
|
||||
|
||||
it("keeps existing deterministic fallback when there is no active candidate", () => {
|
||||
const unknownA = makeNode({
|
||||
id: "fallback-a",
|
||||
label: "Unknown B",
|
||||
description: "Unknown factor one.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
});
|
||||
const unknownB = makeNode({
|
||||
id: "fallback-b",
|
||||
label: "Unknown A",
|
||||
description: "Unknown factor two.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
});
|
||||
const graph = makeGraph({
|
||||
centralStatement: "Two conflicting signals remain unresolved.",
|
||||
nodes: [unknownA, unknownB],
|
||||
edges: [],
|
||||
activeUnknownNodeId: null,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "Tie case",
|
||||
});
|
||||
|
||||
const result = selectActiveUnknownCandidate(graph, []);
|
||||
expect(result.status).toBe("selected");
|
||||
expect(result.nodeId).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user