fix: keep live cancellation terminal

This commit is contained in:
lda
2026-07-09 18:33:29 +07:00 Verified
parent 2606d38f70
commit aecbf21735
4 changed files with 98 additions and 7 deletions
@@ -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);
});
});
+2 -2
View File
@@ -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();