feat(confidence-engine): support finding corrections
This commit is contained in:
@@ -170,10 +170,35 @@ function FocusedQuestionBody({
|
|||||||
focusedContributions,
|
focusedContributions,
|
||||||
currentFindings,
|
currentFindings,
|
||||||
onUpdateFindingDisposition,
|
onUpdateFindingDisposition,
|
||||||
|
onUpdateFindingProposition,
|
||||||
}) {
|
}) {
|
||||||
const hasContent = focused?.question?.trim() || formulationStep === "active" || processingStep === "active" || focused?.error;
|
const hasContent = focused?.question?.trim() || formulationStep === "active" || processingStep === "active" || focused?.error;
|
||||||
const hasResult = Boolean(focused?.result);
|
const hasResult = Boolean(focused?.result);
|
||||||
|
|
||||||
|
// ── Local correction state (FQB-owned, not propagated upward) ─
|
||||||
|
const [editingFindingId, setEditingFindingId] = useState(null);
|
||||||
|
const [draft, setDraft] = useState("");
|
||||||
|
|
||||||
|
function startEditing(id, proposition) {
|
||||||
|
setEditingFindingId(id);
|
||||||
|
setDraft(proposition ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEditing() {
|
||||||
|
setEditingFindingId(null);
|
||||||
|
setDraft("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEditing() {
|
||||||
|
const trimmed = (draft ?? "").trim();
|
||||||
|
if (!trimmed || !editingFindingId) {
|
||||||
|
cancelEditing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onUpdateFindingProposition?.(editingFindingId, trimmed);
|
||||||
|
cancelEditing();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isFocused && hasContent && (
|
{isFocused && hasContent && (
|
||||||
@@ -206,14 +231,40 @@ function FocusedQuestionBody({
|
|||||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">What this tells us</h3><ul className="list-disc pl-5 space-y-2">{(currentFindings?.length ? currentFindings : (focused.result.observations || [])).map((item, i) => {
|
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">What this tells us</h3><ul className="list-disc pl-5 space-y-2">{(currentFindings?.length ? currentFindings : (focused.result.observations || [])).map((item, i) => {
|
||||||
const isFinding = typeof item === "object" && item !== null && "id" in item;
|
const isFinding = typeof item === "object" && item !== null && "id" in item;
|
||||||
const disposition = isFinding ? item.userDisposition : null;
|
const disposition = isFinding ? item.userDisposition : null;
|
||||||
|
const isEditing = isFinding && editingFindingId === item.id;
|
||||||
|
if (!isFinding) {
|
||||||
|
return (
|
||||||
|
<li key={i} className="text-sm leading-relaxed text-gray-700">{item}</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<li key={i} className="text-sm leading-relaxed text-gray-700 flex items-start gap-2">
|
||||||
|
<textarea
|
||||||
|
value={draft}
|
||||||
|
onChange={(e) => setDraft(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
data-testid="proposition-editor"
|
||||||
|
className="flex-1 rounded border border-blue-300 bg-blue-50/40 px-2 py-1 text-sm focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-300"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-1 shrink-0 mt-[2px]">
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); saveEditing(); }} data-testid="proposition-save" className="text-[10px] font-medium text-blue-600 underline shrink-0 hover:text-blue-700">Save</button>
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); cancelEditing(); }} data-testid="proposition-cancel" className="text-[10px] font-medium text-gray-400 underline shrink-0 hover:text-gray-500">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<li key={i} className="text-sm leading-relaxed text-gray-700 flex items-start gap-2">
|
<li key={i} className="text-sm leading-relaxed text-gray-700 flex items-start gap-2">
|
||||||
<span className="flex-1">{currentFindings?.length ? item.proposition : item}</span>
|
<span className="flex-1">{item.proposition}</span>
|
||||||
|
{onUpdateFindingProposition && (
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); startEditing(item.id, item.proposition); }} data-testid={`not-quite-${item.id}`} className="mt-[2px] text-[10px] font-medium text-amber-500 underline shrink-0 hover:text-amber-600">Not quite</button>
|
||||||
|
)}
|
||||||
{isFinding && onUpdateFindingDisposition && (
|
{isFinding && onUpdateFindingDisposition && (
|
||||||
disposition === "not_relevant" ? (
|
disposition === "not_relevant" ? (
|
||||||
<button onClick={(e) => { e.stopPropagation(); onUpdateFindingDisposition(item.id, null); }} className="mt-[2px] text-[10px] font-medium text-teal-600 underline shrink-0 hover:text-teal-700" title="Restore to understanding">restore</button>
|
<button onClick={(e) => { e.stopPropagation(); onUpdateFindingDisposition(item.id, null); }} data-testid={`restore-${item.id}`} className="mt-[2px] text-[10px] font-medium text-teal-600 underline shrink-0 hover:text-teal-700" title="Restore to understanding">restore</button>
|
||||||
) : (
|
) : (
|
||||||
<button onClick={(e) => { e.stopPropagation(); onUpdateFindingDisposition(item.id, "not_relevant"); }} className="mt-[2px] text-[10px] font-medium text-gray-400 underline shrink-0 hover:text-red-500" title="Remove from understanding">not relevant</button>
|
<button onClick={(e) => { e.stopPropagation(); onUpdateFindingDisposition(item.id, "not_relevant"); }} data-testid={`not-relevant-${item.id}`} className="mt-[2px] text-[10px] font-medium text-gray-400 underline shrink-0 hover:text-red-500" title="Remove from understanding">not relevant</button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
@@ -1041,6 +1092,7 @@ function FocusedInvestigationWorkspace({
|
|||||||
focusedContributions,
|
focusedContributions,
|
||||||
currentFindings,
|
currentFindings,
|
||||||
onUpdateFindingDisposition,
|
onUpdateFindingDisposition,
|
||||||
|
onUpdateFindingProposition,
|
||||||
}) {
|
}) {
|
||||||
const hasResult = Boolean(focused?.result);
|
const hasResult = Boolean(focused?.result);
|
||||||
|
|
||||||
@@ -1070,6 +1122,7 @@ function FocusedInvestigationWorkspace({
|
|||||||
focusedContributions={hasResult ? [] : (focusedContributions || [])}
|
focusedContributions={hasResult ? [] : (focusedContributions || [])}
|
||||||
currentFindings={currentFindings || []}
|
currentFindings={currentFindings || []}
|
||||||
onUpdateFindingDisposition={onUpdateFindingDisposition}
|
onUpdateFindingDisposition={onUpdateFindingDisposition}
|
||||||
|
onUpdateFindingProposition={onUpdateFindingProposition}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1091,6 +1144,7 @@ function OpenQuestionsPanel({
|
|||||||
setFocusedPresentationItemId, setFocusedAnswer, focusedAnswer, setDoneForNowIds,
|
setFocusedPresentationItemId, setFocusedAnswer, focusedAnswer, setDoneForNowIds,
|
||||||
setFollowUpQuestion, focusedContributions, focusedInvestigations, setIsFocusedWorkspaceOpen,
|
setFollowUpQuestion, focusedContributions, focusedInvestigations, setIsFocusedWorkspaceOpen,
|
||||||
onUpdateFindingDisposition,
|
onUpdateFindingDisposition,
|
||||||
|
onUpdateFindingProposition,
|
||||||
}) {
|
}) {
|
||||||
const openNodes = (graph?.nodes || []).filter(
|
const openNodes = (graph?.nodes || []).filter(
|
||||||
(n) => n.kind === "unknown" && n.status !== "resolved" && !doneForNowIds.includes(n.id),
|
(n) => n.kind === "unknown" && n.status !== "resolved" && !doneForNowIds.includes(n.id),
|
||||||
@@ -1169,6 +1223,7 @@ function OpenQuestionsPanel({
|
|||||||
setFollowUpQuestion={setFollowUpQuestion}
|
setFollowUpQuestion={setFollowUpQuestion}
|
||||||
focusedContributions={focusedContributions}
|
focusedContributions={focusedContributions}
|
||||||
onUpdateFindingDisposition={onUpdateFindingDisposition}
|
onUpdateFindingDisposition={onUpdateFindingDisposition}
|
||||||
|
onUpdateFindingProposition={onUpdateFindingProposition}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Thread contributions for this node */}
|
{/* Thread contributions for this node */}
|
||||||
@@ -1212,6 +1267,7 @@ export default function ReasoningWorkspace({
|
|||||||
onFocusedContribution,
|
onFocusedContribution,
|
||||||
findings,
|
findings,
|
||||||
onUpdateFindingDisposition,
|
onUpdateFindingDisposition,
|
||||||
|
onUpdateFindingProposition,
|
||||||
}) {
|
}) {
|
||||||
const [investigationHistory, setInvestigationHistory] = useState([]);
|
const [investigationHistory, setInvestigationHistory] = useState([]);
|
||||||
const turnCounter = useRef(0);
|
const turnCounter = useRef(0);
|
||||||
@@ -1816,6 +1872,7 @@ export default function ReasoningWorkspace({
|
|||||||
focusedContributions={focusedContributions}
|
focusedContributions={focusedContributions}
|
||||||
currentFindings={currentFindings || []}
|
currentFindings={currentFindings || []}
|
||||||
onUpdateFindingDisposition={onUpdateFindingDisposition}
|
onUpdateFindingDisposition={onUpdateFindingDisposition}
|
||||||
|
onUpdateFindingProposition={onUpdateFindingProposition}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -1891,6 +1948,7 @@ export default function ReasoningWorkspace({
|
|||||||
focusedContributions={focusedContributions}
|
focusedContributions={focusedContributions}
|
||||||
focusedInvestigations={focusedInvestigations}
|
focusedInvestigations={focusedInvestigations}
|
||||||
setIsFocusedWorkspaceOpen={setIsFocusedWorkspaceOpen}
|
setIsFocusedWorkspaceOpen={setIsFocusedWorkspaceOpen}
|
||||||
|
onUpdateFindingProposition={onUpdateFindingProposition}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -2066,6 +2124,7 @@ export default function ReasoningWorkspace({
|
|||||||
focusedContributions={focusedContributions}
|
focusedContributions={focusedContributions}
|
||||||
currentFindings={currentFindings || []}
|
currentFindings={currentFindings || []}
|
||||||
onUpdateFindingDisposition={onUpdateFindingDisposition}
|
onUpdateFindingDisposition={onUpdateFindingDisposition}
|
||||||
|
onUpdateFindingProposition={onUpdateFindingProposition}
|
||||||
/>
|
/>
|
||||||
{/* Workspace navigation — hidden during formulation/loading states */}
|
{/* Workspace navigation — hidden during formulation/loading states */}
|
||||||
{formulationStep !== "active" && (
|
{formulationStep !== "active" && (
|
||||||
|
|||||||
@@ -276,6 +276,16 @@ export default function ScenarioForm() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateFindingProposition(findingId, newProposition) {
|
||||||
|
setFindings((prev) =>
|
||||||
|
prev.map((f) =>
|
||||||
|
f.id === findingId
|
||||||
|
? { ...f, proposition: newProposition, userDisposition: null }
|
||||||
|
: f,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function appendFocusedContribution(contribution) {
|
function appendFocusedContribution(contribution) {
|
||||||
// Derive a single stored contribution object and use it for BOTH
|
// Derive a single stored contribution object and use it for BOTH
|
||||||
// contribution storage AND Finding derivation so the same identity
|
// contribution storage AND Finding derivation so the same identity
|
||||||
@@ -630,6 +640,7 @@ export default function ScenarioForm() {
|
|||||||
onFocusedContribution={appendFocusedContribution}
|
onFocusedContribution={appendFocusedContribution}
|
||||||
findings={findings}
|
findings={findings}
|
||||||
onUpdateFindingDisposition={updateFindingDisposition}
|
onUpdateFindingDisposition={updateFindingDisposition}
|
||||||
|
onUpdateFindingProposition={updateFindingProposition}
|
||||||
onRestart={() => {
|
onRestart={() => {
|
||||||
clearSession();
|
clearSession();
|
||||||
setStatus("idle");
|
setStatus("idle");
|
||||||
|
|||||||
@@ -812,3 +812,222 @@ describe("applyFindingsToSummary — disposition-aware", () => {
|
|||||||
expect(content).toContain("notQuiteTexts");
|
expect(content).toContain("notQuiteTexts");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── updateFindingProposition mutation seam (ScenarioForm) ───
|
||||||
|
|
||||||
|
describe("updateFindingProposition — ScenarioForm mutation seam", () => {
|
||||||
|
function simulateUpdateFindingProposition(findings, findingId, newProposition) {
|
||||||
|
return findings.map((f) =>
|
||||||
|
f.id === findingId
|
||||||
|
? { ...f, proposition: newProposition, userDisposition: null }
|
||||||
|
: f,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseFindings = [
|
||||||
|
{
|
||||||
|
id: "f1",
|
||||||
|
proposition: "Original text A",
|
||||||
|
userDisposition: null,
|
||||||
|
sourceObservation: "obs-001",
|
||||||
|
contributionId: "contrib-0001",
|
||||||
|
originatingTargetNodeId: "node-A",
|
||||||
|
createdAt: "2024-01-01T00:00:00Z",
|
||||||
|
status: "confirmed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "f2",
|
||||||
|
proposition: "Original text B",
|
||||||
|
userDisposition: null,
|
||||||
|
sourceObservation: "obs-002",
|
||||||
|
contributionId: "contrib-0001",
|
||||||
|
originatingTargetNodeId: "node-B",
|
||||||
|
createdAt: "2024-01-01T00:01:00Z",
|
||||||
|
status: "confirmed",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it("target finding.id is unchanged", () => {
|
||||||
|
const result = simulateUpdateFindingProposition(baseFindings, "f1", "New text A");
|
||||||
|
expect(result[0].id).toBe("f1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("target proposition changes to new value", () => {
|
||||||
|
const result = simulateUpdateFindingProposition(baseFindings, "f1", "Corrected text A");
|
||||||
|
expect(result[0].proposition).toBe("Corrected text A");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("target userDisposition resets to null", () => {
|
||||||
|
const findingsWithNull = baseFindings.map((f) => f.id === "f2" ? { ...f, userDisposition: "not_relevant" } : f);
|
||||||
|
const result = simulateUpdateFindingProposition(findingsWithNull, "f2", "Corrected text B");
|
||||||
|
expect(result[1].userDisposition).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sourceObservation remains unchanged on target", () => {
|
||||||
|
const result = simulateUpdateFindingProposition(baseFindings, "f1", "New text A");
|
||||||
|
expect(result[0].sourceObservation).toBe("obs-001");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("contributionId remains unchanged on target", () => {
|
||||||
|
const result = simulateUpdateFindingProposition(baseFindings, "f1", "New text A");
|
||||||
|
expect(result[0].contributionId).toBe("contrib-0001");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finding.id remains unchanged on target (reconfirmed)", () => {
|
||||||
|
const result = simulateUpdateFindingProposition(baseFindings, "f1", "New text A");
|
||||||
|
expect(result[0].id).toBe("f1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("another Finding (non-target) remains the exact existing object", () => {
|
||||||
|
const before = baseFindings.find((f) => f.id === "f2");
|
||||||
|
const result = simulateUpdateFindingProposition(baseFindings, "f1", "New text A");
|
||||||
|
const after = result.find((f) => f.id === "f2");
|
||||||
|
expect(after).toBe(before); // same object reference
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no Finding is added or removed — count unchanged", () => {
|
||||||
|
const beforeCount = baseFindings.length;
|
||||||
|
const result = simulateUpdateFindingProposition(baseFindings, "f1", "New text A");
|
||||||
|
expect(result.length).toBe(beforeCount);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── onUpdateFindingProposition prop chain verification ───────────
|
||||||
|
|
||||||
|
describe("onUpdateFindingProposition — prop chain verification", () => {
|
||||||
|
it("ScenarioForm exposes updateFindingProposition callback with correct arity", async () => {
|
||||||
|
const testFindings = [{ id: "f1", proposition: "P", userDisposition: null }];
|
||||||
|
|
||||||
|
const updateFindingProposition = (findingId, newProposition) => {
|
||||||
|
return testFindings.map((f) =>
|
||||||
|
f.id === findingId ? { ...f, proposition: newProposition, userDisposition: null } : f,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updated = updateFindingProposition("f1", "Corrected");
|
||||||
|
expect(updated[0].proposition).toBe("Corrected");
|
||||||
|
expect(updated[0].userDisposition).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ReasoningWorkspace accepts onUpdateFindingProposition as prop", async () => {
|
||||||
|
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "components", "reasoning-workspace.jsx");
|
||||||
|
const content = await fs.readFile(path, "utf-8");
|
||||||
|
|
||||||
|
expect(content).toContain("onUpdateFindingProposition");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ReasoningWorkspace passes onUpdateFindingProposition to FocusedInvestigationWorkspace", async () => {
|
||||||
|
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "components", "reasoning-workspace.jsx");
|
||||||
|
const content = await fs.readFile(path, "utf-8");
|
||||||
|
|
||||||
|
const fiwsCall = content.match(/<FocusedInvestigationWorkspace[\s\S]*?\/>/g);
|
||||||
|
expect(fiwsCall).not.toBeNull();
|
||||||
|
expect(fiwsCall[0]).toContain("onUpdateFindingProposition");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ScenarioForm passes onUpdateFindingProposition to ReasoningWorkspace", async () => {
|
||||||
|
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "components", "scenario-form.jsx");
|
||||||
|
const content = await fs.readFile(path, "utf-8");
|
||||||
|
expect(content).toContain("onUpdateFindingProposition={updateFindingProposition}");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no correction state introduced in ReasoningWorkspace — only FQB owns editingFindingId + draft", async () => {
|
||||||
|
const rwPath = join(dirname(fileURLToPath(import.meta.url)), "..", "components", "reasoning-workspace.jsx");
|
||||||
|
const rwContent = await fs.readFile(rwPath, "utf-8");
|
||||||
|
|
||||||
|
// ReasoningWorkspace should NOT declare editingFindingId or draft as its own state
|
||||||
|
// (they are only declared inside FocusedQuestionBody)
|
||||||
|
const reasonFuncMatch = rwContent.match(/export default function ReasoningWorkspace\([\s\S]*?return \(/);
|
||||||
|
expect(reasonFuncMatch).not.toBeNull();
|
||||||
|
const funcBody = reasonFuncMatch[0];
|
||||||
|
expect(funcBody).not.toMatch(/editingFindingId/);
|
||||||
|
expect(funcBody).not.toMatch(/setEditingFindingId/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Not quite invokes correction for exact Finding.id", async () => {
|
||||||
|
// Simulate: user clicks "Not quite" → startEditing(id, proposition)
|
||||||
|
let state = { editingFindingId: null, draft: "" };
|
||||||
|
const startEditing = (id, proposition) => {
|
||||||
|
state.editingFindingId = id;
|
||||||
|
state.draft = proposition ?? "";
|
||||||
|
};
|
||||||
|
|
||||||
|
startEditing("f1", "Original text A");
|
||||||
|
|
||||||
|
expect(state.editingFindingId).toBe("f1");
|
||||||
|
expect(state.draft).toBe("Original text A");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cancel does not invoke canonical mutation", async () => {
|
||||||
|
let mutations = [];
|
||||||
|
const originalMutation = (id, val) => { mutations.push({ id, val }); };
|
||||||
|
|
||||||
|
let state = { editingFindingId: "f1", draft: "changed" };
|
||||||
|
|
||||||
|
// Simulate cancel — clears state without calling mutation
|
||||||
|
state.editingFindingId = null;
|
||||||
|
state.draft = "";
|
||||||
|
|
||||||
|
expect(mutations.length).toBe(0);
|
||||||
|
expect(state.editingFindingId).toBeNull();
|
||||||
|
expect(state.draft).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("valid save invokes proposition mutation with exact Finding.id + trimmed text", () => {
|
||||||
|
let mutations = [];
|
||||||
|
const mockMutation = (id, val) => { mutations.push({ id, val }); };
|
||||||
|
|
||||||
|
let state = { editingFindingId: "f1", draft: " corrected text " };
|
||||||
|
|
||||||
|
const saveEditing = () => {
|
||||||
|
const trimmed = (state.draft ?? "").trim();
|
||||||
|
if (!trimmed || !state.editingFindingId) { return; }
|
||||||
|
mockMutation(state.editingFindingId, trimmed);
|
||||||
|
state.editingFindingId = null;
|
||||||
|
state.draft = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
saveEditing();
|
||||||
|
|
||||||
|
expect(mutations).toEqual([{ id: "f1", val: "corrected text" }]);
|
||||||
|
expect(state.editingFindingId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("whitespace-only save does not invoke mutation", () => {
|
||||||
|
let mutations = [];
|
||||||
|
const mockMutation = (id, val) => { mutations.push({ id, val }); };
|
||||||
|
|
||||||
|
let state = { editingFindingId: "f1", draft: " \n\t " };
|
||||||
|
|
||||||
|
const saveEditing = () => {
|
||||||
|
const trimmed = (state.draft ?? "").trim();
|
||||||
|
if (!trimmed || !state.editingFindingId) { return; }
|
||||||
|
mockMutation(state.editingFindingId, trimmed);
|
||||||
|
state.editingFindingId = null;
|
||||||
|
state.draft = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
saveEditing();
|
||||||
|
|
||||||
|
expect(mutations.length).toBe(0);
|
||||||
|
expect(state.editingFindingId).not.toBeNull(); // state preserved when no-op
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Not quite and Not relevant coexist on same Finding", async () => {
|
||||||
|
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "components", "reasoning-workspace.jsx");
|
||||||
|
const content = await fs.readFile(path, "utf-8");
|
||||||
|
|
||||||
|
// Both buttons should appear in the same rendering block for findings
|
||||||
|
const findingLiBlock = content.match(/<ul className="list-disc pl-5 space-y-2"[\s\S]*?<\/ul>/g);
|
||||||
|
expect(findingLiBlock).not.toBeNull();
|
||||||
|
expect(findingLiBlock[0]).toContain("Not quite");
|
||||||
|
expect(findingLiBlock[0]).toContain("not relevant");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restore button still present after Not quite introduced", async () => {
|
||||||
|
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "components", "reasoning-workspace.jsx");
|
||||||
|
const content = await fs.readFile(path, "utf-8");
|
||||||
|
|
||||||
|
expect(content).toContain(">restore<");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user