feat: integrate evaluation closing and questions
This commit is contained in:
@@ -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,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",
|
||||||
|
|||||||
Reference in New Issue
Block a user