File size: 3,387 Bytes
76289e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import { afterEach, expect, it, vi } from "vitest";
import { recordCommandPoll } from "../agents/command-poll-backoff.js";
import { detectToolCallLoop, recordToolCall } from "../agents/tool-loop-detection.js";
import {
  onDiagnosticEvent,
  setDiagnosticsEnabledForProcess,
  waitForDiagnosticEventsDrained,
  type DiagnosticEventPayload,
} from "../infra/diagnostic-events.js";
import {
  getDiagnosticSessionState,
  isDiagnosticSessionStateCurrent,
  peekDiagnosticSessionState,
} from "./diagnostic-session-state.js";
import {
  logMessageQueued,
  logSessionStateChange,
  logWebhookReceived,
  startDiagnosticHeartbeat,
  stopDiagnosticHeartbeat,
} from "./diagnostic.js";
import { resetDiagnosticStateForTest } from "./diagnostic.test-support.js";

afterEach(() => {
  resetDiagnosticStateForTest();
  setDiagnosticsEnabledForProcess(true);
  vi.useRealTimers();
});

it("preserves independent tool-loop and poll-backoff policy when diagnostic observation stops", () => {
  const session = { sessionKey: "diagnostic-tool-history" };
  const state = getDiagnosticSessionState(session);
  const args = { path: "fixture.txt" };
  for (let index = 0; index < 10; index += 1) {
    recordToolCall(state, "read", args);
  }
  const before = detectToolCallLoop(state, "read", args, { enabled: true });
  expect(before).toMatchObject({ stuck: true, detector: "generic_repeat", count: 10 });
  expect(recordCommandPoll(state, "fixture-command", false)).toBe(5_000);
  expect(recordCommandPoll(state, "fixture-command", false)).toBe(10_000);
  setDiagnosticsEnabledForProcess(false);
  stopDiagnosticHeartbeat();
  const current = getDiagnosticSessionState(session);
  expect(detectToolCallLoop(current, "read", args, { enabled: true })).toEqual(before);
  expect(recordCommandPoll(current, "fixture-command", false)).toBe(30_000);
});

it("retires interrupted diagnostic observations before re-enable without reviving their authority", async () => {
  vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] });
  setDiagnosticsEnabledForProcess(true);
  const events: DiagnosticEventPayload[] = [];
  const unsubscribe = onDiagnosticEvent((event) => events.push(event));
  const session = { sessionKey: "diagnostic-lifecycle", sessionId: "diagnostic-lifecycle" };
  try {
    startDiagnosticHeartbeat({}, { sampleLiveness: () => null });
    logMessageQueued({ ...session, source: "test" });
    logSessionStateChange({ ...session, state: "processing" });
    const generation = peekDiagnosticSessionState(session)?.generation;
    expect(generation).toBeTypeOf("number");
    setDiagnosticsEnabledForProcess(false);
    stopDiagnosticHeartbeat();
    logSessionStateChange({ ...session, state: "idle" });
    setDiagnosticsEnabledForProcess(true);
    startDiagnosticHeartbeat({}, { sampleLiveness: () => null });
    logWebhookReceived({ channel: "test" });
    await vi.advanceTimersByTimeAsync(30_000);
    await waitForDiagnosticEventsDrained();
    expect(events.findLast((event) => event.type === "diagnostic.heartbeat")).toMatchObject({
      active: 0,
      queued: 0,
      waiting: 0,
    });
    logMessageQueued({ ...session, source: "test" });
    logSessionStateChange({ ...session, state: "processing" });
    expect(isDiagnosticSessionStateCurrent({ ...session, generation, state: "processing" })).toBe(
      false,
    );
  } finally {
    unsubscribe();
  }
});