fix: keep live cancellation terminal
This commit is contained in:
@@ -168,6 +168,22 @@ describe("useTimelineAgent", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("live cancel does not call next", async () => {
|
||||
const cancelReview = vi.fn(async () => {});
|
||||
const next = vi.fn(async () => {});
|
||||
const demo = demoController({
|
||||
state: { ...initialDemoTimelineState, mode: "live", phase: "review" },
|
||||
cancelReview,
|
||||
next,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useTimelineAgent(demo, { mode: "live", status: readyStatus }));
|
||||
await act(async () => result.current.cancelReview());
|
||||
|
||||
expect(cancelReview).toHaveBeenCalledWith("Cancelled by operator.");
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses replay label when live target failed", () => {
|
||||
const demo = demoController();
|
||||
const { result } = renderHook(() =>
|
||||
|
||||
@@ -122,11 +122,8 @@ export const useTimelineAgent = (
|
||||
|
||||
const cancelReview = useCallback(async () => {
|
||||
await demo.cancelReview("Cancelled by operator.");
|
||||
// In replay the canonical recording only contains the submitted branch.
|
||||
// Do not call next() or the UI would falsely advance into submitted evidence.
|
||||
if (demo.state.mode === "live") {
|
||||
await demo.next();
|
||||
}
|
||||
// Cancellation is terminal in presentation mode. Do not call next() or the UI
|
||||
// would falsely advance into submitted/resume evidence.
|
||||
setMessages((current) => appendToolMessage(
|
||||
current,
|
||||
"timeline-agent-cancel",
|
||||
|
||||
@@ -451,4 +451,82 @@ describe("useDemoTimeline", () => {
|
||||
expect(result.current.state.events).toEqual([]);
|
||||
expect(result.current.interruptPayload).toBeNull();
|
||||
});
|
||||
|
||||
it("live cancellation is terminal and does not advance", async () => {
|
||||
vi.useFakeTimers();
|
||||
mockedCallOperation
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
operation: "workflow.deployments.inspect" as const,
|
||||
label: "Inspect deployment",
|
||||
interpreted: {
|
||||
id: "lda_report_case_study.default",
|
||||
artifactId: "lda_report_case_study",
|
||||
artifactVersion: 1,
|
||||
bindings: [],
|
||||
driftPolicy: "block",
|
||||
},
|
||||
exchange: { request: {}, response: {} },
|
||||
equivalentCli: "uv run wf deploy inspect lda_report_case_study.default",
|
||||
durationMs: 4,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
operation: "workflow.runs.start" as const,
|
||||
label: "Start run",
|
||||
interpreted: {
|
||||
runId: "run_demo",
|
||||
deploymentId: "lda_report_case_study.default",
|
||||
artifactId: "lda_report_case_study",
|
||||
artifactVersion: 1,
|
||||
status: "interrupted",
|
||||
resumeReadiness: "ready",
|
||||
interrupt: {
|
||||
kind: "issue_review",
|
||||
payload: {
|
||||
report_markdown: "# Report",
|
||||
proposed_issues: [
|
||||
{ id: "risk-1", title: "Defense", body: "Review paths.", severity: "medium" },
|
||||
],
|
||||
},
|
||||
outcomes: ["submitted", "cancelled"],
|
||||
typed: true,
|
||||
request_schema: { type: "object" },
|
||||
resume_schema: { type: "object" },
|
||||
},
|
||||
outcome: null,
|
||||
error: null,
|
||||
output: null,
|
||||
diagnostics: [],
|
||||
traceCount: 6,
|
||||
nextActions: {
|
||||
canContinue: true,
|
||||
canSaveNow: null,
|
||||
recommendedNextTool: "wf.workflow.resume_run",
|
||||
reason: "Run is interrupted for issue review.",
|
||||
patchExamples: [],
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
exchange: { request: {}, response: {} },
|
||||
equivalentCli: "uv run wf run start lda_report_case_study.default --input '<json>'",
|
||||
durationMs: 88,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDemoTimeline("http://127.0.0.1:8765/rpc", vi.fn()),
|
||||
);
|
||||
act(() => result.current.start());
|
||||
await act(async () => vi.advanceTimersByTimeAsync(900));
|
||||
await act(async () => vi.advanceTimersByTimeAsync(900));
|
||||
expect(result.current.state.phase).toBe("review");
|
||||
|
||||
await act(async () => result.current.cancelReview("Cancelled."));
|
||||
await act(async () => vi.advanceTimersByTimeAsync(1800));
|
||||
|
||||
expect(result.current.state.phase).toBe("cancelled");
|
||||
expect(result.current.output).toBeNull();
|
||||
expect(result.current.trace).toBeNull();
|
||||
expect(mockedCallOperation).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -292,8 +292,8 @@ export const useDemoTimeline = (
|
||||
comment,
|
||||
outcome: "cancelled",
|
||||
};
|
||||
dispatch({ type: state.mode === "live" ? "continue_review" : "cancel_review" });
|
||||
}, [state.mode]);
|
||||
dispatch({ type: "cancel_review" });
|
||||
}, []);
|
||||
|
||||
const restart = useCallback(() => {
|
||||
resetRuntime();
|
||||
|
||||
Reference in New Issue
Block a user