fix(confidence-engine): show focused investigation activity

This commit is contained in:
2026-08-29 15:15:28 +01:00
parent 0f4dfcbb17
commit b9c0b6f6f7
3 changed files with 212 additions and 12 deletions
@@ -1246,3 +1246,150 @@ describe("getHistoricalPropositions: Previous Learning uses canonical Findings",
expect(result).toEqual([]);
});
});
// ── v0.49: INVESTIGATING cue on normal Open Questions buttons (actual render path) ───
describe("INVESTIGATING cue on Open Question buttons (normal render path)", () => {
// This mirrors the exact filter logic in ThreadContributionsBadge.jsx
// that determines whether INVESTIGATING is visible on each Open Question card/button.
function threadContribsForNode(nodeId, focusedContributions) {
return (focusedContributions || []).filter(
(c) => c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId,
);
}
// Simulates the ThreadContributionsBadge component's render decision:
// returns null when no threadContribs (no INVESTIGATING rendered),
// or { investigating: true, count, contributions } when there are.
function renderBadge(nodeId, focusedContributions) {
const tc = threadContribsForNode(nodeId, focusedContributions);
if (!tc.length) return null;
return { investigating: true, count: tc.length };
}
// Test fixture: an Open Question (originating) with multi-turn focused history
const questionA = { id: "oq-originating", label: "Which step of the onboarding funnel has the highest abandonment rate?", kind: "unknown" };
const questionB = { id: "question-b", label: "What is the primary revenue driver?", kind: "unknown" };
// Contribution history for question A:
// - Turn 1: direct to origin node
// - Turn 2: follow-up targeting a different intermediate node but linking back via originatingTargetNodeId
const focusedContributions = [
{ id: "contrib-001", targetNodeId: "oq-originating", question: "First turn", observations: ["Fact 1"] },
{ id: "contrib-002", targetNodeId: "follow_up_intermediate", originatingTargetNodeId: "oq-originating", question: "Follow-up turn", observations: ["Fact 2"] },
{ id: "contrib-003", targetNodeId: "follow_up_intermediate_2", originatingTargetNodeId: "oq-originating", question: "Second follow-up", observations: ["Fact 3"] },
];
// ── Case 1 — untouched question (no focused Contributions) ───
it("untouched question shows Unclear only — INVESTIGATING absent", () => {
const badge = renderBadge(questionB.id, focusedContributions);
expect(badge).toBeNull(); // no badge → no INVESTIGATING
// The epistemic state "Unclear" is still visible (it's independent of activity)
});
// ── Case 2 — direct focused contribution matches via targetNodeId ───
it("direct contribution to question A shows INVESTIGATING alongside Unclear", () => {
const directContribs = [
{ id: "contrib-direct", targetNodeId: "oq-originating", observations: ["Direct fact"] },
];
const badge = renderBadge("oq-originating", directContribs);
expect(badge).not.toBeNull();
expect(badge.investigating).toBe(true);
expect(badge.count).toBe(1);
});
// ── Case 3 — follow-up/origin contribution matches via originatingTargetNodeId ───
it("follow-up contribution with different targetNodeId still shows INVESTIGATING via originatingTargetNodeId", () => {
const followUpOnly = [
{ id: "contrib-followup", targetNodeId: "different_node_id", originatingTargetNodeId: "oq-originating", observations: ["Follow-up fact"] },
];
const badge = renderBadge("oq-originating", followUpOnly);
expect(badge).not.toBeNull();
expect(badge.investigating).toBe(true);
expect(badge.count).toBe(1);
});
// ── Case 4 — question isolation (A has history, B does not) ───
it("only investigated question shows INVESTIGATING; untouched question remains Unclear without cue", () => {
const badgeA = renderBadge("oq-originating", focusedContributions);
const badgeB = renderBadge("question-b", focusedContributions);
// Question A (originating) — has multi-turn history
expect(badgeA).not.toBeNull();
expect(badgeA.investigating).toBe(true);
expect(badgeA.count).toBe(3); // all three turns match the thread
// Question B (untouched) — zero contributions for its thread
expect(badgeB).toBeNull();
});
// ── Critical: origin matching vs direct matching are OR, not AND ───
it("originatingTargetNodeId-only contribution matches even when targetNodeId differs", () => {
const onlyOriginMatch = [
{ id: "contrib-origin-only", originatingTargetNodeId: "oq-originating" },
];
expect(renderBadge("oq-originating", onlyOriginMatch)).not.toBeNull();
});
it("targetNodeId-only contribution matches even when originatingTargetNodeId is undefined", () => {
const onlyTargetMatch = [
{ id: "contrib-target-only", targetNodeId: "oq-originating" },
];
expect(renderBadge("oq-originating", onlyTargetMatch)).not.toBeNull();
});
it("contribution with neither field does NOT match any node", () => {
const noFields = [{ id: "contrib-no-fields" }];
expect(renderBadge("oq-originating", noFields)).toBeNull();
expect(renderBadge("any-other-node", noFields)).toBeNull();
});
it("null contributions produce no INVESTIGATING on any question", () => {
const badgeA = renderBadge("oq-originating", null);
const badgeB = renderBadge("question-b", null);
expect(badgeA).toBeNull();
expect(badgeB).toBeNull();
});
it("empty contributions array produces no INVESTIGATING on any question", () => {
const badgeA = renderBadge("oq-originating", []);
const badgeB = renderBadge("question-b", []);
expect(badgeA).toBeNull();
expect(badgeB).toBeNull();
});
it("investigated + untouched coexist on same surface — user can distinguish via INVESTIGATING cue", () => {
// Simulates the full Open Questions render: both questions visible simultaneously
const badgeA = renderBadge("oq-originating", focusedContributions);
const badgeB = renderBadge("question-b", focusedContributions);
// Both epistemic states remain "Unclear" — they are independent of activity
// Only question A has the additional activity cue
// User sees on A: Unclear + INVESTIGATING (badge exists)
expect(badgeA).not.toBeNull();
expect(badgeA.investigating).toBe(true);
// User sees on B: Unclear only (no badge)
expect(badgeB).toBeNull();
// The two questions are visually distinguishable without developer diagnostics
});
it("multi-turn recovery after only follow-up survives cold return via originatingTargetNodeId", () => {
// Simulates the cold-return edge case: only Turn 2 (follow-up) persists
const coldReturnContribs = [
{ id: "contrib-002", targetNodeId: "follow_up_intermediate", originatingTargetNodeId: "oq-originating", observations: ["Cold return fact"] },
];
// The repaired filter still recovers the INVESTIGATING cue
const badge = renderBadge("oq-originating", coldReturnContribs);
expect(badge).not.toBeNull();
expect(badge.investigating).toBe(true);
expect(badge.count).toBe(1);
// Untouched question still has no cue
const badgeUntouched = renderBadge("question-b", coldReturnContribs);
expect(badgeUntouched).toBeNull();
});
});