feat: integrate evaluation closing and questions

This commit is contained in:
lda
2026-07-10 21:26:21 +07:00 Verified
parent 0a02c23be2
commit 5f79ee455b
5 changed files with 153 additions and 29 deletions
+66
View File
@@ -0,0 +1,66 @@
# Task 4 Report
## Status
- DONE
## Files
- Modified `web/apps/console/src/presentation/SceneBody.tsx`
- Modified `web/apps/console/src/presentation/SceneBody.test.tsx`
- Modified `web/apps/console/src/presentation/PresentationRoute.test.tsx`
- Modified `web/apps/console/src/presentation/presentation-state.test.ts`
## RED
Command:
```powershell
pnpm --dir web --filter @lda/console test -- src/presentation/SceneBody.test.tsx src/presentation/PresentationRoute.test.tsx src/presentation/presentation-state.test.ts
```
Observed result:
- `SceneBody.test.tsx`: 3 failing tests
- `PresentationRoute.test.tsx`: 2 failing tests
- `presentation-state.test.ts`: passed
- Failure cause matched the brief: `SceneBody` still rendered the local evaluation/conclusion content and the Questions beat still showed the generic discussion rail instead of the dedicated discussion index.
## GREEN
Command:
```powershell
pnpm --dir web --filter @lda/console test -- src/presentation/SceneBody.test.tsx src/presentation/PresentationRoute.test.tsx src/presentation/presentation-state.test.ts
```
Observed result:
- `3` test files passed
- `74` tests passed
- `0` failures
## Verification
- Replaced the old local evaluation scene in `SceneBody` with `EvaluationEvidenceScene`
- Routed non-Questions conclusion beats to `ConclusionScene`
- Routed `#scene/conclusion/questions` to `DefenseDiscussionIndex`
- Suppressed the generic scene discussion rail on the Questions beat to avoid duplicate discussion controls
- Added route-level coverage that opening a Questions discussion returns to `#scene/conclusion/questions`
- Added reducer coverage that `discussionReturn` preserves the Questions beat without introducing new state branches
## Deviations
- The brief's example assertion used `getByRole("group", { name: /thesis contribution boundary/i })`.
- The committed `ConclusionScene` already exposes that surface as an accessible `region`, so the test asserts `role="region"` instead of changing another task's interface.
## Concerns
- No functional concerns from Task 4 changes.
- Existing Git line-ending warnings remain (`LF` to `CRLF` on checkout); no content changes were made to address that.
## Self-Review
- TDD was followed with an observed RED before the `SceneBody` routing change.
- Only Task 4 files were edited, plus this report file.
- Reducer code was intentionally left unchanged because the new regression proved existing return semantics already handled the Questions beat correctly.
@@ -114,6 +114,30 @@ describe("PresentationRoute", () => {
expect(window.location.hash).toBe("#scene/positioning/landscape"); expect(window.location.hash).toBe("#scene/positioning/landscape");
}); });
it("returns to the questions beat after closing a discussion opened from the index", async () => {
const user = userEvent.setup();
window.location.hash = "#scene/conclusion/questions";
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
await user.click(screen.getByRole("button", { name: /where is the ai agent/i }));
expect(window.location.hash).toBe("#discuss/where-is-ai-agent");
await user.click(screen.getByRole("button", { name: /return to thesis/i }));
expect(window.location.hash).toBe("#scene/conclusion/questions");
});
it("advances from the conclusion beat to the questions beat", async () => {
window.location.hash = "#scene/conclusion/conclusion";
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
await userEvent.keyboard("{ArrowRight}");
expect(window.location.hash).toBe("#scene/conclusion/questions");
expect(await screen.findByRole("navigation", { name: /defense discussion index/i })).toBeInTheDocument();
});
it("renders stable chat, primary, progress, and transient evidence surfaces", async () => { it("renders stable chat, primary, progress, and transient evidence surfaces", async () => {
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
@@ -39,6 +39,19 @@ const demo: DemoTimelineController = {
afterEach(() => cleanup()); afterEach(() => cleanup());
const renderSceneBodyAtMainLocation = (sceneId: PresentationLocation["sceneId"], beatId: string, openDiscussion = noop) => render(
<SceneBody
location={{ kind: "main", sceneId, beatId, focusPath: [] }}
demo={demo}
selectedNodeId={null}
selectNode={noop}
openEvidence={noop}
openDiscussion={openDiscussion}
onFocusPathChange={noop}
motionDisabled={false}
/>,
);
describe("SceneBody", () => { describe("SceneBody", () => {
it("renders Scene 1 as an opening decomposition visual", () => { it("renders Scene 1 as an opening decomposition visual", () => {
const location: PresentationLocation = { kind: "main", sceneId: "thesis", beatId: "substrate", focusPath: [] }; const location: PresentationLocation = { kind: "main", sceneId: "thesis", beatId: "substrate", focusPath: [] };
@@ -382,6 +395,31 @@ describe("SceneBody", () => {
expect(openDiscussion).toHaveBeenCalledWith("hosted-automation"); expect(openDiscussion).toHaveBeenCalledWith("hosted-automation");
}); });
it("routes the evaluation scene through the dedicated evidence board", () => {
renderSceneBodyAtMainLocation("evaluation", "cohort");
expect(screen.getByRole("group", { name: /evaluation evidence board/i })).toBeInTheDocument();
});
it("routes conclusion beats through the dedicated conclusion scene", () => {
renderSceneBodyAtMainLocation("conclusion", "conclusion");
expect(screen.getByRole("region", { name: /thesis contribution boundary/i })).toBeInTheDocument();
});
it("renders the defense discussion index on the questions beat and opens its branches", async () => {
const user = userEvent.setup();
const openDiscussion = vi.fn();
renderSceneBodyAtMainLocation("conclusion", "questions", openDiscussion);
expect(screen.getByRole("navigation", { name: /defense discussion index/i })).toBeInTheDocument();
expect(screen.queryByLabelText("defense discussion topics")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /where is the ai agent/i }));
expect(openDiscussion).toHaveBeenCalledWith("where-is-ai-agent");
});
it("renders evidence before discussion links so the chip lane cannot cover evidence text", () => { it("renders evidence before discussion links so the chip lane cannot cover evidence text", () => {
const location: PresentationLocation = { kind: "main", sceneId: "positioning", beatId: "landscape", focusPath: [] }; const location: PresentationLocation = { kind: "main", sceneId: "positioning", beatId: "landscape", focusPath: [] };
const { container } = render( const { container } = render(
+11 -29
View File
@@ -11,6 +11,9 @@ import {
import { DemoLifecycleScene } from "./DemoLifecycleScene.js"; import { DemoLifecycleScene } from "./DemoLifecycleScene.js";
import { DemoWorkflowScene } from "./DemoWorkflowScene.js"; import { DemoWorkflowScene } from "./DemoWorkflowScene.js";
import { StageCaption } from "./StageCaption.js"; import { StageCaption } from "./StageCaption.js";
import { ConclusionScene } from "./conclusion/ConclusionScene.js";
import { DefenseDiscussionIndex } from "./discussion/DefenseDiscussionIndex.js";
import { EvaluationEvidenceScene } from "./evaluation/EvaluationEvidenceScene.js";
import { ArchitectureScene } from "./scenes/ArchitectureScene.js"; import { ArchitectureScene } from "./scenes/ArchitectureScene.js";
import { OpeningThesisScene } from "./opening/OpeningThesisScene.js"; import { OpeningThesisScene } from "./opening/OpeningThesisScene.js";
import { ProblemLoopScene } from "./opening/ProblemLoopScene.js"; import { ProblemLoopScene } from "./opening/ProblemLoopScene.js";
@@ -257,32 +260,6 @@ const AuthoringScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBe
</> </>
); );
const evaluationStats = [
{ id: "cohort", value: "36", label: "trials" },
{ id: "validity", value: "2", label: "challenges" },
{ id: "findings", value: "3", label: "waves" },
];
const EvaluationScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
<>
<StageCaption eyebrow={`Act I · ${scene.claimClass}`} title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div className="scene-body__evaluation">
{evaluationStats.map((stat) => (
<div
key={stat.id}
className={`scene-body__evaluation-stat${beat.id === stat.id ? " scene-body__evaluation-stat--active" : ""}`}
>
<strong>{stat.value}</strong>
<span>{stat.label}</span>
</div>
))}
</div>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
);
const AgentHandoffScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => ( const AgentHandoffScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
<> <>
<StageCaption eyebrow="Agent handoff" title={scene.title}> <StageCaption eyebrow="Agent handoff" title={scene.title}>
@@ -302,7 +279,10 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvid
const scene = findScene(sceneId) ?? findScene("thesis")!; const scene = findScene(sceneId) ?? findScene("thesis")!;
const beat = findBeat(sceneId, beatId) ?? scene.beats[0]!; const beat = findBeat(sceneId, beatId) ?? scene.beats[0]!;
const discussionLinks = <DiscussionLinks sceneId={scene.id} openDiscussion={openDiscussion} />; // The questions beat renders its own grouped discussion index. Suppress the
// generic per-scene rail there so the same actions do not appear twice.
const showDiscussionRail = !(scene.id === "conclusion" && beat.id === "questions");
const discussionLinks = showDiscussionRail ? <DiscussionLinks sceneId={scene.id} openDiscussion={openDiscussion} /> : null;
const content = (() => { const content = (() => {
switch (scene.view) { switch (scene.view) {
case "narrative": case "narrative":
@@ -345,9 +325,11 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvid
/> />
); );
case "evaluation": case "evaluation":
return <EvaluationScene scene={scene} beat={beat} />; return <EvaluationEvidenceScene scene={scene} beat={beat} />;
case "conclusion": case "conclusion":
return <NarrativeScene scene={scene} beat={beat} />; return beat.id === "questions"
? <DefenseDiscussionIndex openDiscussion={openDiscussion} />
: <ConclusionScene scene={scene} beat={beat} />;
default: default:
return assertNever(scene.view); return assertNever(scene.view);
} }
@@ -220,6 +220,20 @@ describe("presentationReducer", () => {
.toEqual(deepRuntimeState.location); .toEqual(deepRuntimeState.location);
}); });
it("preserves the questions beat as the discussion return location", () => {
const atQuestions = presentationReducer(initialPresentationState, {
type: "jump",
location: { kind: "main", sceneId: "conclusion", beatId: "questions", focusPath: [] },
});
const opened = presentationReducer(atQuestions, {
type: "open_discussion",
branchId: "where-is-ai-agent",
});
expect(opened.discussionReturn).toEqual(atQuestions.location);
expect(presentationReducer(opened, { type: "close_discussion" }).location).toEqual(atQuestions.location);
});
it("derives a receipt from beat metadata without opening an inspector", () => { it("derives a receipt from beat metadata without opening an inspector", () => {
const state = presentationReducer(initialPresentationState, { const state = presentationReducer(initialPresentationState, {
type: "jump", type: "jump",