feat(confidence-engine): support not-relevant findings
This commit is contained in:
@@ -596,3 +596,219 @@ describe("diagnostics prompt version", () => {
|
||||
expect(PROMPT_VERSIONS).toContain("v0.2");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Finding disposition toggle seam ────────────────────────────────
|
||||
|
||||
describe("Finding disposition toggle — state machine", () => {
|
||||
it("default finding has null userDisposition (visible, clickable 'not relevant')", async () => {
|
||||
const testFinding = {
|
||||
id: "find-disp-001",
|
||||
proposition: "Team is overloaded",
|
||||
userDisposition: null,
|
||||
};
|
||||
expect(testFinding.userDisposition).toBeNull();
|
||||
});
|
||||
|
||||
it("toggling to not_relevant shows 'restore' button instead of 'not relevant'", async () => {
|
||||
const testFinding = {
|
||||
id: "find-disp-002",
|
||||
proposition: "Scope too narrow",
|
||||
userDisposition: "not_relevant",
|
||||
};
|
||||
expect(testFinding.userDisposition).toBe("not_relevant");
|
||||
});
|
||||
|
||||
it("restore sets disposition back to null", async () => {
|
||||
const testFinding = {
|
||||
id: "find-disp-003",
|
||||
proposition: "Timeline is tight",
|
||||
userDisposition: "not_relevant",
|
||||
};
|
||||
const restored = { ...testFinding, userDisposition: null };
|
||||
expect(restored.userDisposition).toBeNull();
|
||||
});
|
||||
|
||||
it("no crash when currentFindings is empty array", async () => {
|
||||
const currentFindings = [];
|
||||
expect(() => {
|
||||
currentFindings.forEach((item) => {
|
||||
const isFinding = typeof item === "object" && item !== null && "id" in item;
|
||||
const disposition = isFinding ? item.userDisposition : null;
|
||||
expect(disposition).not.toBe(undefined);
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("no crash when currentFindings contains primitive strings (observations path)", async () => {
|
||||
const observations = ["Factor A confirmed", "Timing unknown"];
|
||||
expect(() => {
|
||||
observations.forEach((item) => {
|
||||
const isFinding = typeof item === "object" && item !== null && "id" in item;
|
||||
const disposition = isFinding ? item.userDisposition : null;
|
||||
expect(isFinding).toBe(false);
|
||||
expect(disposition).toBeNull();
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("finding with 'not_relevant' disposition excluded from confirmed observations", async () => {
|
||||
const findings = [
|
||||
{ id: "f1", proposition: "Valid finding", userDisposition: null },
|
||||
{ id: "f2", proposition: "Not relevant", userDisposition: "not_relevant" },
|
||||
];
|
||||
const approved = findings.filter((f) => f.userDisposition !== "not_relevant");
|
||||
expect(approved).toHaveLength(1);
|
||||
expect(approved[0].id).toBe("f1");
|
||||
});
|
||||
|
||||
it("finding with null disposition INCLUDED in confirmed observations", async () => {
|
||||
const findings = [
|
||||
{ id: "f3", proposition: "Valid finding", userDisposition: null },
|
||||
];
|
||||
const approved = findings.filter((f) => f.userDisposition !== "not_relevant");
|
||||
expect(approved).toHaveLength(1);
|
||||
expect(approved[0].id).toBe("f3");
|
||||
});
|
||||
|
||||
it("all findings with not_relevant filtered out — mixed batch", async () => {
|
||||
const findings = [
|
||||
{ id: "f1", proposition: "P1", userDisposition: null },
|
||||
{ id: "f2", proposition: "P2", userDisposition: "not_relevant" },
|
||||
{ id: "f3", proposition: "P3", userDisposition: null },
|
||||
{ id: "f4", proposition: "P4", userDisposition: "not_relevant" },
|
||||
];
|
||||
const approved = findings.filter((f) => f.userDisposition !== "not_relevant");
|
||||
expect(approved).toHaveLength(2);
|
||||
expect(approved.map((f) => f.id)).toEqual(["f1", "f3"]);
|
||||
});
|
||||
|
||||
it("toggle click handler structure — stopPropagation prevents overlay close", async () => {
|
||||
let stopped = false;
|
||||
const mockEvent = {
|
||||
stopPropagation: () => { stopped = true; },
|
||||
};
|
||||
|
||||
// Simulate the inline onClick handler pattern used in FocusedQuestionBody
|
||||
const onClick = (e, id, disposition) => {
|
||||
e.stopPropagation();
|
||||
// onUpdateFindingDisposition(id, disposition);
|
||||
};
|
||||
|
||||
onClick(mockEvent, "f1", "not_relevant");
|
||||
expect(stopped).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Disposition prop chain verification ────────────────────────
|
||||
|
||||
describe("Disposition prop chain — ScenarioForm → ReasoningWorkspace → FocusedQuestionBody", () => {
|
||||
it("ScenarioForm exposes updateFindingDisposition callback with correct arity", async () => {
|
||||
const testFindings = [{ id: "f1", proposition: "P", userDisposition: null }];
|
||||
|
||||
const updateFindingDisposition = (findingId, newDisposition) => {
|
||||
return testFindings.map((f) =>
|
||||
f.id === findingId ? { ...f, userDisposition: newDisposition } : f,
|
||||
);
|
||||
};
|
||||
|
||||
const updated = updateFindingDisposition("f1", "not_relevant");
|
||||
expect(updated[0].userDisposition).toBe("not_relevant");
|
||||
});
|
||||
|
||||
it("ReasoningWorkspace receives onUpdateFindingDisposition and passes to FocusedInvestigationWorkspace", async () => {
|
||||
// Verify the prop chain exists in source code
|
||||
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "components", "reasoning-workspace.jsx");
|
||||
const content = await fs.readFile(path, "utf-8");
|
||||
|
||||
expect(content).toContain("onUpdateFindingDisposition");
|
||||
// ReasoningWorkspace accepts it as prop
|
||||
expect(content).toContain("export default function ReasoningWorkspace");
|
||||
});
|
||||
|
||||
it("FocusedQuestionBody receives onUpdateFindingDisposition via all three call sites", async () => {
|
||||
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "components", "reasoning-workspace.jsx");
|
||||
const content = await fs.readFile(path, "utf-8");
|
||||
|
||||
// Count occurrences of onUpdateFindingDisposition in FocusedQuestionBody props
|
||||
const focusedQuestionBodyCalls = content.match(/<FocusedQuestionBody[\s\S]*?\/>/g) || [];
|
||||
expect(focusedQuestionBodyCalls.length).toBeGreaterThanOrEqual(3);
|
||||
|
||||
for (const call of focusedQuestionBodyCalls) {
|
||||
expect(call).toContain("onUpdateFindingDisposition");
|
||||
}
|
||||
});
|
||||
|
||||
it("FocusedInvestigationWorkspace also receives onUpdateFindingDisposition", async () => {
|
||||
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "components", "reasoning-workspace.jsx");
|
||||
const content = await fs.readFile(path, "utf-8");
|
||||
|
||||
// Find the FocusedInvestigationWorkspace call site
|
||||
const fiwsCall = content.match(/<FocusedInvestigationWorkspace[\s\S]*?\/>/g);
|
||||
expect(fiwsCall).not.toBeNull();
|
||||
expect(fiwsCall[0]).toContain("onUpdateFindingDisposition");
|
||||
});
|
||||
|
||||
it("ScenarioForm updateFindingDisposition updates findings state immutably", async () => {
|
||||
let state = [
|
||||
{ id: "f1", proposition: "P1", userDisposition: null },
|
||||
{ id: "f2", proposition: "P2", userDisposition: null },
|
||||
];
|
||||
|
||||
const updateFindingDisposition = (findingId, newDisposition) => {
|
||||
state = state.map((f) =>
|
||||
f.id === findingId ? { ...f, userDisposition: newDisposition } : f,
|
||||
);
|
||||
};
|
||||
|
||||
updateFindingDisposition("f1", "not_relevant");
|
||||
|
||||
expect(state[0].userDisposition).toBe("not_relevant");
|
||||
expect(state[1].userDisposition).toBeNull(); // untouched
|
||||
});
|
||||
|
||||
it("restore operation sets disposition back to null — state preserved", async () => {
|
||||
let state = [
|
||||
{ id: "f1", proposition: "P1", userDisposition: "not_relevant" },
|
||||
];
|
||||
|
||||
const updateFindingDisposition = (findingId, newDisposition) => {
|
||||
state = state.map((f) =>
|
||||
f.id === findingId ? { ...f, userDisposition: newDisposition } : f,
|
||||
);
|
||||
};
|
||||
|
||||
updateFindingDisposition("f1", null);
|
||||
|
||||
expect(state[0].userDisposition).toBeNull();
|
||||
expect(state[0].proposition).toBe("P1"); // proposition unchanged
|
||||
});
|
||||
});
|
||||
|
||||
// ── applyFindingsToSummary respects userDisposition ────────────
|
||||
|
||||
describe("applyFindingsToSummary — disposition-aware", () => {
|
||||
it("not_relevant findings do NOT appear in summary text", async () => {
|
||||
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "lib", "graph", "finding-helpers.js");
|
||||
const content = await fs.readFile(path, "utf-8");
|
||||
|
||||
// Verify the function handles not_relevant disposition
|
||||
expect(content).toContain("not_relevant");
|
||||
expect(content).toContain("f.evaluation");
|
||||
});
|
||||
|
||||
it("null disposition findings ARE included in summary text", async () => {
|
||||
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "lib", "graph", "finding-helpers.js");
|
||||
const content = await fs.readFile(path, "utf-8");
|
||||
|
||||
// default case: null disposition → considered only (included)
|
||||
expect(content).toContain("default");
|
||||
});
|
||||
|
||||
it("not_quite findings appear as partial matches in summary", async () => {
|
||||
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "lib", "graph", "finding-helpers.js");
|
||||
const content = await fs.readFile(path, "utf-8");
|
||||
|
||||
expect(content).toContain("not_quite");
|
||||
expect(content).toContain("notQuiteTexts");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user