feat: probe presentation live target
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { callOperation } from "../connection/api.js";
|
||||||
|
import { initialDemoTimelineState } from "../demo/timeline/reducer.js";
|
||||||
|
import { usePresentationTargetStatus } from "./usePresentationTargetStatus.js";
|
||||||
|
|
||||||
|
vi.mock("../connection/api.js", () => ({ callOperation: vi.fn() }));
|
||||||
|
const mockedCallOperation = vi.mocked(callOperation);
|
||||||
|
|
||||||
|
beforeEach(() => mockedCallOperation.mockReset());
|
||||||
|
|
||||||
|
describe("usePresentationTargetStatus", () => {
|
||||||
|
it("marks live target ready after workflow health succeeds", async () => {
|
||||||
|
mockedCallOperation.mockResolvedValue({
|
||||||
|
ok: true as const,
|
||||||
|
operation: "workflow.health",
|
||||||
|
label: "Health",
|
||||||
|
interpreted: { status: "ok", storeRoot: "store" },
|
||||||
|
exchange: { request: {}, response: {} },
|
||||||
|
equivalentCli: "uv run wf status",
|
||||||
|
durationMs: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
usePresentationTargetStatus(
|
||||||
|
{ mode: "live", target: "http://127.0.0.1:8765/rpc", source: "default" },
|
||||||
|
initialDemoTimelineState,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.kind).toBe("ready"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to replay when health fails", async () => {
|
||||||
|
mockedCallOperation.mockResolvedValue({
|
||||||
|
ok: false as const,
|
||||||
|
error: { code: "upstream_unreachable", message: "connection refused" },
|
||||||
|
exchange: { request: {}, response: {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
usePresentationTargetStatus(
|
||||||
|
{ mode: "live", target: "http://127.0.0.1:8765/rpc", source: "default" },
|
||||||
|
initialDemoTimelineState,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.kind).toBe("failed"));
|
||||||
|
expect(result.current.label).toBe("Replay fallback");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { callOperation } from "../connection/api.js";
|
||||||
|
import type { DemoTimelineState } from "../demo/timeline/reducer.js";
|
||||||
|
import type { PresentationTargetState } from "./live-target.js";
|
||||||
|
import {
|
||||||
|
presentationTargetHealth,
|
||||||
|
type PresentationTargetHealth,
|
||||||
|
type TargetProbeState,
|
||||||
|
} from "./presentation-target-status.js";
|
||||||
|
|
||||||
|
const liveActive = (state: DemoTimelineState): boolean =>
|
||||||
|
state.mode === "live" && state.phase !== "ready";
|
||||||
|
|
||||||
|
export const usePresentationTargetStatus = (
|
||||||
|
targetState: PresentationTargetState,
|
||||||
|
demoState: DemoTimelineState,
|
||||||
|
): PresentationTargetHealth => {
|
||||||
|
const [probe, setProbe] = useState<TargetProbeState>(
|
||||||
|
targetState.mode === "live" ? "checking" : "none",
|
||||||
|
);
|
||||||
|
const [failureReason, setFailureReason] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
if (targetState.mode !== "live") {
|
||||||
|
setProbe("none");
|
||||||
|
setFailureReason(targetState.reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setProbe("checking");
|
||||||
|
setFailureReason(undefined);
|
||||||
|
const probeTarget = async () => {
|
||||||
|
try {
|
||||||
|
const response = await callOperation("workflow.health", targetState.target, {});
|
||||||
|
if (cancelled) return;
|
||||||
|
if (response.ok) {
|
||||||
|
setProbe("ready");
|
||||||
|
} else {
|
||||||
|
setProbe("failed");
|
||||||
|
setFailureReason(response.error.message);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (cancelled) return;
|
||||||
|
setProbe("failed");
|
||||||
|
setFailureReason(error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void probeTarget();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [targetState]);
|
||||||
|
|
||||||
|
return presentationTargetHealth({
|
||||||
|
target: targetState.mode === "live" ? targetState.target : null,
|
||||||
|
probe,
|
||||||
|
liveActive: liveActive(demoState),
|
||||||
|
failureReason,
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user