
























1+import { describe, expect, it } from "vitest";
2+import { isSessionRunActive } from "../session-run-state.ts";
3+import type { SessionsListResult } from "../types.ts";
4+import {
5+reconcileChatRunFromCurrentSessionRow,
6+reconcileChatRunLifecycle,
7+STALE_ACTIVE_ROW_RECONCILE_WINDOW_MS,
8+} from "./run-lifecycle.ts";
9+10+type ReconcileHost = Parameters<typeof reconcileChatRunFromCurrentSessionRow>[0];
11+type TestRow = { key: string; hasActiveRun?: boolean; status?: string; startedAt?: number };
12+13+function makeSessionsResult(rows: TestRow[]): SessionsListResult {
14+return { sessions: rows } as unknown as SessionsListResult;
15+}
16+17+function makeHost(over: Partial<ReconcileHost> = {}): ReconcileHost {
18+return {
19+sessionKey: "s1",
20+chatRunId: null,
21+chatStream: null,
22+sessionsResult: makeSessionsResult([{ key: "s1", hasActiveRun: true, status: "running" }]),
23+requestUpdate: () => {},
24+ ...over,
25+};
26+}
27+28+function rowActive(host: ReconcileHost): boolean {
29+const row = host.sessionsResult?.sessions.find((r) => r.key === host.sessionKey);
30+return Boolean(row && isSessionRunActive(row));
31+}
32+33+describe("reconcileChatRunFromCurrentSessionRow stale-active suppression (#87875)", () => {
34+it("suppresses a stale active row after a recent local completion", () => {
35+const host = makeHost({
36+lastLocalTerminalReconcile: {
37+sessionKey: "s1",
38+runId: "r1",
39+phase: "done",
40+sessionStatus: "done",
41+occurredAt: Date.now(),
42+},
43+});
44+expect(reconcileChatRunFromCurrentSessionRow(host)).toBe(true);
45+expect(rowActive(host)).toBe(false);
46+expect(host.lastLocalTerminalReconcile?.runId).toBe("r1");
47+});
48+49+it("does NOT clear a genuinely recovered active run with no recent local completion", () => {
50+const host = makeHost({ lastLocalTerminalReconcile: null });
51+expect(reconcileChatRunFromCurrentSessionRow(host)).toBe(false);
52+expect(rowActive(host)).toBe(true);
53+});
54+55+it("ignores and clears a local terminal reconcile older than the window", () => {
56+const host = makeHost({
57+lastLocalTerminalReconcile: {
58+sessionKey: "s1",
59+runId: "r1",
60+phase: "done",
61+sessionStatus: "done",
62+occurredAt: Date.now() - STALE_ACTIVE_ROW_RECONCILE_WINDOW_MS - 1_000,
63+},
64+});
65+expect(reconcileChatRunFromCurrentSessionRow(host)).toBe(false);
66+expect(rowActive(host)).toBe(true);
67+expect(host.lastLocalTerminalReconcile).toBeNull();
68+});
69+70+it("does not suppress when the recent completion was for a different session", () => {
71+const host = makeHost({
72+sessionKey: "s2",
73+sessionsResult: makeSessionsResult([{ key: "s2", hasActiveRun: true, status: "running" }]),
74+lastLocalTerminalReconcile: {
75+sessionKey: "s1",
76+runId: "r1",
77+phase: "done",
78+sessionStatus: "done",
79+occurredAt: Date.now(),
80+},
81+});
82+expect(reconcileChatRunFromCurrentSessionRow(host)).toBe(false);
83+expect(rowActive(host)).toBe(true);
84+});
85+86+it("clears the flag once the server poll reports a non-active row", () => {
87+const host = makeHost({
88+sessionsResult: makeSessionsResult([{ key: "s1", hasActiveRun: false, status: "done" }]),
89+lastLocalTerminalReconcile: {
90+sessionKey: "s1",
91+runId: "r1",
92+phase: "done",
93+sessionStatus: "done",
94+occurredAt: Date.now(),
95+},
96+});
97+expect(reconcileChatRunFromCurrentSessionRow(host)).toBe(false);
98+expect(host.lastLocalTerminalReconcile).toBeNull();
99+});
100+101+it("does not arm stale-row suppression from generic lifecycle cleanup", () => {
102+const host = makeHost({
103+chatRunId: "orphaned-run",
104+chatStream: "stale stream",
105+});
106+reconcileChatRunLifecycle(host, {
107+outcome: "interrupted",
108+sessionStatus: "killed",
109+runId: "orphaned-run",
110+sessionKey: "s1",
111+clearLocalRun: true,
112+clearChatStream: true,
113+publishRunStatus: false,
114+});
115+expect(host.lastLocalTerminalReconcile ?? null).toBeNull();
116+host.sessionsResult = makeSessionsResult([
117+{ key: "s1", hasActiveRun: true, status: "running" },
118+]);
119+expect(reconcileChatRunFromCurrentSessionRow(host)).toBe(false);
120+expect(rowActive(host)).toBe(true);
121+});
122+123+it("does not suppress a newer active row after a follow-up run starts", () => {
124+const terminalAt = Date.now();
125+const host = makeHost({
126+sessionsResult: makeSessionsResult([
127+{
128+key: "s1",
129+hasActiveRun: true,
130+status: "running",
131+startedAt: terminalAt + 1,
132+},
133+]),
134+lastLocalTerminalReconcile: {
135+sessionKey: "s1",
136+runId: "r1",
137+phase: "done",
138+sessionStatus: "done",
139+occurredAt: terminalAt,
140+},
141+});
142+expect(reconcileChatRunFromCurrentSessionRow(host)).toBe(false);
143+expect(rowActive(host)).toBe(true);
144+expect(host.lastLocalTerminalReconcile).toBeNull();
145+});
146+147+it("arms suppression on a completed turn, then suppresses the racing refresh", () => {
148+const host = makeHost({
149+chatRunId: "r1",
150+chatStream: "partial...",
151+sessionsResult: makeSessionsResult([{ key: "s1", hasActiveRun: true, status: "running" }]),
152+});
153+reconcileChatRunLifecycle(host, {
154+outcome: "done",
155+sessionStatus: "done",
156+runId: "r1",
157+sessionKey: "s1",
158+clearLocalRun: true,
159+clearChatStream: true,
160+publishRunStatus: false,
161+armLocalTerminalReconcile: true,
162+});
163+expect(host.lastLocalTerminalReconcile?.sessionKey).toBe("s1");
164+expect(host.chatRunId ?? null).toBeNull();
165+// A racing sessions.list refresh re-introduces a stale active row.
166+host.sessionsResult = makeSessionsResult([
167+{ key: "s1", hasActiveRun: true, status: "running" },
168+]);
169+expect(reconcileChatRunFromCurrentSessionRow(host)).toBe(true);
170+expect(rowActive(host)).toBe(false);
171+expect(host.lastLocalTerminalReconcile?.runId).toBe("r1");
172+});
173+174+it("keeps suppressing multiple stale active refreshes within the window", () => {
175+const terminalAt = Date.now();
176+const host = makeHost({
177+lastLocalTerminalReconcile: {
178+sessionKey: "s1",
179+runId: "r1",
180+phase: "done",
181+sessionStatus: "done",
182+occurredAt: terminalAt,
183+},
184+});
185+186+expect(reconcileChatRunFromCurrentSessionRow(host)).toBe(true);
187+host.sessionsResult = makeSessionsResult([
188+{ key: "s1", hasActiveRun: true, status: "running", startedAt: terminalAt - 1 },
189+]);
190+expect(reconcileChatRunFromCurrentSessionRow(host)).toBe(true);
191+expect(rowActive(host)).toBe(false);
192+});
193+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。