fix: use replay fallback for direct demo hashes

This commit is contained in:
lda
2026-07-12 03:15:19 +07:00 Verified
parent 2e15322cc9
commit 179cd0119e
6 changed files with 54 additions and 13 deletions
+6
View File
@@ -134,6 +134,12 @@ sessionStorage.removeItem("lda.workflowConsole.target");
location.reload(); location.reload();
``` ```
When a stateful demo deep link is opened before this browser session has started
a live run, the presentation selects the reviewed recording for that beat even
if the loopback health probe succeeds. Start from the Scene 8 handoff action to
run live; otherwise the typed interrupt, output, and trace links remain useful
replay-backed entry points.
## Useful Deep Links ## Useful Deep Links
- Title: `http://127.0.0.1:5173/present#scene/thesis/title` - Title: `http://127.0.0.1:5173/present#scene/thesis/title`
@@ -231,15 +231,15 @@ describe("PresentationRoute", () => {
expect(await screen.findByRole("button", { name: /run prepared workflow/i })).toBeInTheDocument(); expect(await screen.findByRole("button", { name: /run prepared workflow/i })).toBeInTheDocument();
}); });
it("does not prime replay evidence on a live direct hash", async () => { it("uses replay fallback for a live direct hash before a run starts", async () => {
window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8765/rpc"); window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8765/rpc");
window.location.hash = "#scene/typed-human-boundary/approval"; window.location.hash = "#scene/typed-human-boundary/approval";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
expect(await screen.findByText(/Live target is ready/i)).toBeInTheDocument(); expect(await screen.findByText(/Replay evidence is active/i)).toBeInTheDocument();
expect(screen.queryByText("Workflow input")).not.toBeInTheDocument(); expect(await screen.findByText("Workflow input")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Submit" })).not.toBeInTheDocument(); expect(await screen.findByRole("button", { name: "Submit" })).toBeInTheDocument();
}); });
it("switches to replay evidence when a configured target fails health", async () => { it("switches to replay evidence when a configured target fails health", async () => {
@@ -58,6 +58,9 @@ export const PresentationRoute = () => {
mode: presentationTarget.mode === "live" ? "live" : "replay", mode: presentationTarget.mode === "live" ? "live" : "replay",
status: targetStatus, status: targetStatus,
}); });
const requiredDemoStage = state.location.kind === "main"
? requirementForDemoBeat(state.location.sceneId, state.location.beatId).requiredStage
: null;
useEffect(() => { useEffect(() => {
const hash = hashForLocation(state.location); const hash = hashForLocation(state.location);
@@ -96,13 +99,21 @@ export const PresentationRoute = () => {
useEffect(() => { useEffect(() => {
if (demo.state.phase !== "ready") return; if (demo.state.phase !== "ready") return;
// The resolved target owns the initial mode: a healthy loopback target must // A direct evidence hash has no live run context to inspect. Use the
// remain live, while an invalid or unreachable target starts from the // reviewed recording until the handoff action creates a live timeline.
// offline recording. The health hook is the point where an HTTP target const directHashNeedsEvidence = requiredDemoStage !== null;
// becomes known to be unreachable; URL shape alone is not enough. const shouldUseReplay = targetStatus.kind === "failed"
const desiredMode = targetStatus.kind === "failed" ? "replay" : presentationTarget.mode; || (targetStatus.kind === "ready" && directHashNeedsEvidence);
const desiredMode = shouldUseReplay ? "replay" : presentationTarget.mode;
if (demo.state.mode !== desiredMode) demo.setMode(desiredMode); if (demo.state.mode !== desiredMode) demo.setMode(desiredMode);
}, [demo.setMode, demo.state.mode, demo.state.phase, presentationTarget.mode, targetStatus.kind]); }, [
demo.setMode,
demo.state.mode,
demo.state.phase,
presentationTarget.mode,
requiredDemoStage,
targetStatus.kind,
]);
useEffect(() => { useEffect(() => {
if (demo.state.phase === "ready" && demo.state.mode === "replay") { if (demo.state.phase === "ready" && demo.state.mode === "replay") {
@@ -18,12 +18,25 @@ describe("presentationTargetHealth", () => {
target: "http://127.0.0.1:8765/rpc", target: "http://127.0.0.1:8765/rpc",
probe: "ready", probe: "ready",
liveActive: false, liveActive: false,
replayActive: false,
})).toMatchObject({ })).toMatchObject({
kind: "ready", kind: "ready",
label: "Live target ready", label: "Live target ready",
}); });
}); });
it("labels a reviewed replay selected alongside a healthy target", () => {
expect(presentationTargetHealth({
target: "http://127.0.0.1:8765/rpc",
probe: "ready",
liveActive: false,
replayActive: true,
})).toMatchObject({
kind: "replay",
label: "Replay evidence",
});
});
it("marks live active only after live timeline starts", () => { it("marks live active only after live timeline starts", () => {
expect(presentationTargetHealth({ expect(presentationTargetHealth({
target: "http://127.0.0.1:8765/rpc", target: "http://127.0.0.1:8765/rpc",
@@ -20,11 +20,13 @@ export const presentationTargetHealth = ({
target, target,
probe, probe,
liveActive, liveActive,
replayActive = false,
failureReason, failureReason,
}: { }: {
readonly target: string | null; readonly target: string | null;
readonly probe: TargetProbeState; readonly probe: TargetProbeState;
readonly liveActive: boolean; readonly liveActive: boolean;
readonly replayActive?: boolean;
readonly failureReason?: string | undefined; readonly failureReason?: string | undefined;
}): PresentationTargetHealth => { }): PresentationTargetHealth => {
if (!target) { if (!target) {
@@ -35,6 +37,14 @@ export const presentationTargetHealth = ({
}; };
} }
if (replayActive && probe === "ready") {
return {
kind: "replay",
label: "Replay evidence",
detail: "reviewed recording",
};
}
if (liveActive && probe === "ready") { if (liveActive && probe === "ready") {
return { return {
kind: "active", kind: "active",
@@ -56,6 +56,7 @@ export const usePresentationTargetStatus = (
target: targetState.mode === "live" ? targetState.target : null, target: targetState.mode === "live" ? targetState.target : null,
probe, probe,
liveActive: liveActive(demoState), liveActive: liveActive(demoState),
replayActive: demoState.mode === "replay",
failureReason, failureReason,
}); });
}; };