feat: add user-focused reasoning workspace

This commit is contained in:
2026-08-03 15:18:35 +01:00
parent 0fe11b93a0
commit ed32d585bb
5 changed files with 835 additions and 99 deletions
+310
View File
@@ -4,6 +4,11 @@ import { renderToStaticMarkup } from "react-dom/server";
import DiagnosticsView from "@/components/diagnostics-view.jsx";
import GraphUpdateView from "@/components/graph-update-view.jsx";
import SituationGraphView from "@/components/situation-graph-view.jsx";
import ReasoningWorkspace, {
useLoadingStatus,
INITIAL_MESSAGES,
UPDATE_MESSAGES,
} from "@/components/reasoning-workspace.jsx";
import {
ScenarioResultPanels,
UpdateErrorPanel,
@@ -838,4 +843,309 @@ describe("graph-backed UI rendering", () => {
expect(html).toContain("New active unknown");
expect(html).not.toContain("No next question selected yet.");
});
});
// ── ReasoningWorkspace tests ────────────────────────────────
describe("ReasoningWorkspace UI", () => {
function makeWorkspaceResult(overrides = {}) {
return {
success: true,
situationGraph: makeGraphResult().situationGraph,
selectedQuestion: { question: "What denominator is being used for the complaint rate?" },
newlySurfacedNodeIds: [],
diagnostics: {
modelName: "test",
responseDurationMs: 1234,
validationStatus: "valid",
nodeCount: 3,
edgeCount: 2,
graphReferenceValidation: { valid: true, errors: [] },
},
...overrides,
};
}
function makeWorkspaceProps(overrides = {}) {
return {
status: "success",
updateStatus: "idle",
result: makeWorkspaceResult(),
answer: "",
setAnswer: vi.fn(),
onAnswerSubmit: vi.fn(),
...overrides,
};
}
it("renders by default with a successful start result", () => {
const html = renderToStaticMarkup(<ReasoningWorkspace {...makeWorkspaceProps()} />);
expect(html).toContain("Your situation");
expect(html).toContain("Complaints increased while production increased.");
});
it("shows current understanding section", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
{...makeWorkspaceProps({
result: makeWorkspaceResult({
situationGraph: {
...makeWorkspaceResult().situationGraph,
currentSummary: "Nodes: 2 observation, 1 unknown",
},
}),
})}
/>,
);
expect(html).toContain("Current understanding");
expect(html).toContain("Nodes: 2 observation, 1 unknown");
});
it("shows current focus section with the active unknown", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace {...makeWorkspaceProps()} />,
);
expect(html).toContain("What we are working out");
expect(html).toContain("Complaint rate denominator");
});
it("prominently displays the next question", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace {...makeWorkspaceProps()} />,
);
expect(html).toContain("Next question");
expect(html).toContain("What denominator is being used for the complaint rate?");
});
it("shows progress summary with resolved and remaining counts", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
{...makeWorkspaceProps({
result: makeWorkspaceResult({
situationGraph: {
...makeWorkspaceResult().situationGraph,
resolvedNodeIds: ["n-1"],
},
}),
})}
/>,
);
expect(html).toContain("resolved");
expect(html).toContain("remaining");
});
it("renders the answer form when a question is available", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace {...makeWorkspaceProps()} />,
);
expect(html).toContain("Your answer");
expect(html).toContain("Update situation");
});
it("renders developer details section (collapsed)", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace {...makeWorkspaceProps()} />,
);
expect(html).toContain("Developer details");
});
it("preserves the full situation graph in developer details", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace {...makeWorkspaceProps()} />,
);
expect(html).toContain("Situation Graph");
expect(html).toContain("Central statement");
});
it("preserves diagnostics in developer details", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace {...makeWorkspaceProps()} />,
);
expect(html).toContain("Diagnostics");
expect(html).toContain("test");
expect(html).toContain("1234ms");
});
it("user-facing sections show labels, not raw node IDs", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace {...makeWorkspaceProps()} />,
);
// Labels and human-readable text should be present
expect(html).toContain("Your situation");
expect(html).toContain("Current understanding");
expect(html).toContain("What we are working out");
expect(html).toContain("Next question");
expect(html).toContain("Complaint rate denominator");
// Raw node IDs only appear in developer details (collapsed), not in user-facing sections
expect(html).toContain("Developer details");
});
it("error state remains visible", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="error"
updateStatus="idle"
result={{ error: "Invalid start-case request" }}
answer=""
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
expect(html).toContain("Error: Invalid start-case request");
});
it("no-question state is handled clearly", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="success"
updateStatus="idle"
result={makeWorkspaceResult({
selectedQuestion: null,
diagnostics: { ...makeWorkspaceResult().diagnostics, noQuestionReason: "All unknowns resolved" },
})}
answer=""
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
expect(html).toContain("There is no next question at the moment.");
});
it("initial loading state appears with spinner heading", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="loading"
updateStatus="idle"
result={null}
answer=""
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
expect(html).toContain("Working through your situation");
expect(html).toContain("Reading your situation");
});
it("update loading state appears with spinner heading", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="success"
updateStatus="loading"
result={makeWorkspaceResult()}
answer=""
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
expect(html).toContain("Working through your situation");
});
it("elapsed time text appears during loading", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="loading"
updateStatus="idle"
result={null}
answer=""
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
expect(html).toContain("This has been running for");
});
it("empty state shows guidance when idle", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="idle"
updateStatus="idle"
result={null}
answer=""
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
// Should not show any result panels or workspace
expect(html).not.toContain("Your situation");
expect(html).not.toContain("Next question");
});
it("update error is visible", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="success"
updateStatus="error"
result={{
situationGraph: makeWorkspaceResult().situationGraph,
selectedQuestion: null,
updateError: { error: "Invalid graph update proposal" },
}}
answer=""
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
expect(html).toContain("Update error: Invalid graph update proposal");
});
it("message pools are exported for external use", () => {
expect(INITIAL_MESSAGES.length).toBeGreaterThan(0);
expect(UPDATE_MESSAGES.length).toBeGreaterThan(0);
expect(INITIAL_MESSAGES[0].min).toBe(0);
expect(UPDATE_MESSAGES[0].min).toBe(0);
});
it("no answer form shown when there is no question", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="success"
updateStatus="idle"
result={makeWorkspaceResult({ selectedQuestion: null })}
answer=""
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
expect(html).not.toContain("Your answer");
expect(html).not.toContain("Update situation");
});
it("update result merges into workspace correctly", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="success"
updateStatus="success"
result={makeWorkspaceResult({
situationGraph: makeUpdateSuccess().updatedSituationGraph,
selectedQuestion: makeUpdateSuccess().selectedQuestion,
})}
answer=""
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
expect(html).toContain("Updated summary");
expect(html).toContain(
"What evidence would clarify how the two observations were measured?",
);
});
});