



























@@ -24,6 +24,7 @@ import {
2424} from "./diagnostic-stability.js";
2525import {
2626logSessionStateChange,
27+logMessageQueued,
2728resetDiagnosticStateForTest,
2829resolveStuckSessionWarnMs,
2930startDiagnosticHeartbeat,
@@ -162,6 +163,45 @@ describe("stuck session diagnostics threshold", () => {
162163});
163164});
164165166+it("keeps queued stale sessions eligible for lane recovery", () => {
167+const events: DiagnosticEventPayload[] = [];
168+const recoverStuckSession = vi.fn();
169+const unsubscribe = onDiagnosticEvent((event) => {
170+events.push(event);
171+});
172+try {
173+startDiagnosticHeartbeat(
174+{
175+diagnostics: {
176+enabled: true,
177+stuckSessionWarnMs: 30_000,
178+},
179+},
180+{ recoverStuckSession },
181+);
182+logMessageQueued({ sessionId: "s1", sessionKey: "main", source: "test" });
183+logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" });
184+vi.advanceTimersByTime(61_000);
185+} finally {
186+unsubscribe();
187+}
188+189+expect(events.filter((event) => event.type === "session.long_running")).toHaveLength(0);
190+const stuckEvents = events.filter((event) => event.type === "session.stuck");
191+expect(stuckEvents).toHaveLength(1);
192+expect(stuckEvents[0]).toMatchObject({
193+classification: "stale_session_state",
194+reason: "queued_work_without_active_run",
195+queueDepth: 1,
196+});
197+expect(recoverStuckSession).toHaveBeenCalledWith({
198+sessionId: "s1",
199+sessionKey: "main",
200+ageMs: expect.any(Number),
201+queueDepth: 1,
202+});
203+});
204+165205it("reports active sessions as stalled instead of stuck when active work stops progressing", () => {
166206const events: DiagnosticEventPayload[] = [];
167207const recoverStuckSession = vi.fn();
@@ -232,6 +272,44 @@ describe("stuck session diagnostics threshold", () => {
232272expect(recoverStuckSession).not.toHaveBeenCalled();
233273});
234274275+it("keeps queued sessions non-recoverable while active work is making progress", () => {
276+const events: DiagnosticEventPayload[] = [];
277+const recoverStuckSession = vi.fn();
278+const unsubscribe = onDiagnosticEvent((event) => {
279+events.push(event);
280+});
281+try {
282+startDiagnosticHeartbeat(
283+{
284+diagnostics: {
285+enabled: true,
286+stuckSessionWarnMs: 30_000,
287+},
288+},
289+{ recoverStuckSession },
290+);
291+logMessageQueued({ sessionId: "s1", sessionKey: "main", source: "test" });
292+logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" });
293+vi.advanceTimersByTime(45_000);
294+markDiagnosticEmbeddedRunStarted({ sessionId: "s1", sessionKey: "main" });
295+vi.advanceTimersByTime(16_000);
296+} finally {
297+unsubscribe();
298+}
299+300+expect(events.filter((event) => event.type === "session.stuck")).toHaveLength(0);
301+expect(events.filter((event) => event.type === "session.stalled")).toHaveLength(0);
302+const longRunningEvents = events.filter((event) => event.type === "session.long_running");
303+expect(longRunningEvents).toHaveLength(1);
304+expect(longRunningEvents[0]).toMatchObject({
305+classification: "long_running",
306+reason: "queued_behind_active_work",
307+activeWorkKind: "embedded_run",
308+queueDepth: 1,
309+});
310+expect(recoverStuckSession).not.toHaveBeenCalled();
311+});
312+235313it("starts and stops the stability recorder with the heartbeat lifecycle", () => {
236314startDiagnosticHeartbeat({
237315diagnostics: {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。