feat: show prepared workflow authoring lifecycle
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { findBeat, findScene } from "../storyboard.js";
|
||||
import { PreparedAuthoringLifecycleScene } from "./PreparedAuthoringLifecycleScene.js";
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
const renderBeat = (beatId: string) => {
|
||||
const scene = findScene("prepared-lifecycle");
|
||||
const beat = findBeat("prepared-lifecycle", beatId);
|
||||
if (!scene || !beat) throw new Error(`missing prepared-lifecycle/${beatId}`);
|
||||
return render(<PreparedAuthoringLifecycleScene scene={scene} beat={beat} />);
|
||||
};
|
||||
|
||||
describe("PreparedAuthoringLifecycleScene", () => {
|
||||
it("discover shows sources, capabilities, and schema", () => {
|
||||
renderBeat("discover");
|
||||
expect(screen.getByText("Discover")).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/sources|capabilities|schema/i).length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("draft shows graph or routes", () => {
|
||||
renderBeat("draft");
|
||||
expect(screen.getByText("Draft")).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/graph|routes/i).length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("validate shows diagnosis and repair", () => {
|
||||
renderBeat("validate");
|
||||
expect(screen.getByText("Validate")).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/diagnos|repair/i).length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("artifact shows immutable ID and version", () => {
|
||||
renderBeat("artifact");
|
||||
expect(screen.getByText("Artifact")).toBeInTheDocument();
|
||||
expect(screen.getByText(/art_x9y8z7/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("deployment shows bindings and validation", () => {
|
||||
renderBeat("deployment");
|
||||
expect(screen.getByText("Deployment")).toBeInTheDocument();
|
||||
expect(screen.getByText(/dep_m4n5p6/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders an Agent trace trigger", () => {
|
||||
renderBeat("discover");
|
||||
expect(screen.getByRole("button", { name: "Agent trace" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("trace is initially closed", () => {
|
||||
renderBeat("discover");
|
||||
expect(screen.queryByRole("dialog", { name: "Authoring trace" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the trace panel when Agent trace is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderBeat("draft");
|
||||
await user.click(screen.getByRole("button", { name: "Agent trace" }));
|
||||
expect(screen.getByRole("dialog", { name: "Authoring trace" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a compact orientation rail", () => {
|
||||
renderBeat("artifact");
|
||||
const rail = screen.getByLabelText("authoring phase rail");
|
||||
expect(rail).toBeInTheDocument();
|
||||
expect(rail.children.length).toBeGreaterThanOrEqual(5);
|
||||
});
|
||||
|
||||
it("highlights the active phase in the rail", () => {
|
||||
renderBeat("deployment");
|
||||
const rail = screen.getByLabelText("authoring phase rail");
|
||||
const active = rail.querySelector("[data-active='true']");
|
||||
expect(active).toBeInTheDocument();
|
||||
expect(active).toHaveTextContent("Deployment");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useState } from "react";
|
||||
import { projectPreparedAuthoringPhase } from "./authoring-projection.js";
|
||||
import { AuthoringTracePanel } from "./AuthoringTracePanel.js";
|
||||
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
||||
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
|
||||
import { StageCaption } from "../StageCaption.js";
|
||||
|
||||
type PreparedAuthoringLifecycleSceneProps = {
|
||||
readonly scene: SceneDefinition;
|
||||
readonly beat: SceneBeatDefinition;
|
||||
};
|
||||
|
||||
const phases: readonly { readonly id: AuthoringPhaseId; readonly label: string }[] = [
|
||||
{ id: "discover", label: "Discover" },
|
||||
{ id: "draft", label: "Draft" },
|
||||
{ id: "validate", label: "Validate" },
|
||||
{ id: "artifact", label: "Artifact" },
|
||||
{ id: "deployment", label: "Deployment" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Scene 9 — Prepared workflow authoring lifecycle.
|
||||
*
|
||||
* Each beat shows a compact orientation rail and one dominant phase projection
|
||||
* sourced from the prepared authoring recording. A persistent "Agent trace"
|
||||
* trigger opens the AuthoringTracePanel overlay.
|
||||
*
|
||||
* The receipt bridges Scene 8's full conversation and Scene 9's separate
|
||||
* trace panel without morphing runtime-owned components.
|
||||
*/
|
||||
export const PreparedAuthoringLifecycleScene = ({ scene, beat }: PreparedAuthoringLifecycleSceneProps) => {
|
||||
const beatId = beat.id as AuthoringPhaseId;
|
||||
const [traceOpen, setTraceOpen] = useState(false);
|
||||
const projection = projectPreparedAuthoringPhase(beatId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StageCaption eyebrow="Prepared workflow" title={scene.title}>
|
||||
<p>{beat.caption}</p>
|
||||
</StageCaption>
|
||||
<section
|
||||
className="prepared-lifecycle-scene"
|
||||
aria-label="prepared workflow authoring lifecycle"
|
||||
data-active-phase={beatId}
|
||||
>
|
||||
<ol className="prepared-lifecycle-scene__rail" aria-label="authoring phase rail">
|
||||
{phases.map((phase) => (
|
||||
<li key={phase.id} data-active={phase.id === beatId ? "true" : "false"}>
|
||||
<strong>{phase.label}</strong>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<article className="prepared-lifecycle-scene__projection">
|
||||
<div className="prepared-lifecycle-scene__summary">
|
||||
<p>{projection.summary}</p>
|
||||
</div>
|
||||
<div className="prepared-lifecycle-scene__commands">
|
||||
{projection.commands.map((cmd, i) => (
|
||||
<div key={i} className="prepared-lifecycle-scene__command">
|
||||
<code>$ {cmd.command}</code>
|
||||
<span className="prepared-lifecycle-scene__command-result" data-result={cmd.result}>
|
||||
{cmd.result === "success" ? "✓" : "⚡"} {cmd.result}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<div className="prepared-lifecycle-scene__receipt">
|
||||
<AuthoringTracePanel
|
||||
phase={beatId}
|
||||
open={traceOpen}
|
||||
onOpen={() => setTraceOpen(true)}
|
||||
onClose={() => setTraceOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user