File size: 3,782 Bytes
e249c6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import { afterEach, describe, expect, it, vi } from "vitest";
import { createDeferred } from "../../../test/helpers/promise.js";
import {
  closePluginTestAdmissions,
  createExecution,
  runPlugin,
  SUCCESS_RESULT,
} from "./execute-plugin.test-support.js";

async function startBlockedRun(options: {
  timeoutMs: number;
  getDeadline: () => number | undefined;
  onChange?: (listener: () => void) => () => void;
}) {
  const { context } = await createExecution({ timeoutMs: options.timeoutMs });
  const started = createDeferred();
  const run = runPlugin(
    context,
    async function* ({ abortSignal }) {
      if (!abortSignal) {
        throw new Error("Host execution did not expose its abort signal.");
      }
      started.resolve();
      await new Promise<never>((_, reject) => {
        abortSignal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
      });
      yield SUCCESS_RESULT;
    },
    {
      noOutputTimeoutMs: 100,
      activeToolCount: () => 1,
      getActiveLoopbackAskUserDeadline: options.getDeadline,
      onActiveLoopbackAskUserDeadlineChange: options.onChange ?? (() => () => {}),
    },
  );
  await started.promise;
  return { run };
}

afterEach(() => {
  closePluginTestAdmissions();
  vi.useRealTimers();
});

describe("plugin-owned CLI ask_user timeout", () => {
  it.each([
    ["uses the exact question deadline", 4_000_000, 3_610_000, "no-output-timeout"],
    ["keeps the earlier overall deadline authoritative", 150, 150, "overall-timeout"],
  ] as const)("%s", async (_name, timeoutMs, advanceMs, reason) => {
    vi.useFakeTimers();
    const deadline = Date.now() + 3_610_000;
    const { run } = await startBlockedRun({ timeoutMs, getDeadline: () => deadline });
    let completed = false;
    void run.then(() => {
      completed = true;
    });
    if (reason === "no-output-timeout") {
      await vi.advanceTimersByTimeAsync(900_000);
      expect(completed).toBe(false);
      await vi.advanceTimersByTimeAsync(advanceMs - 900_000);
    } else {
      await vi.advanceTimersByTimeAsync(advanceMs);
    }
    await expect(run).resolves.toMatchObject({
      reason,
      timedOut: true,
      noOutputTimedOut: reason === "no-output-timeout",
    });
  });

  it("does not let a short question shrink the blocked-tool watchdog floor", async () => {
    vi.useFakeTimers();
    const deadline = Date.now() + 40_000;
    const { run } = await startBlockedRun({
      timeoutMs: 2_000_000,
      getDeadline: () => deadline,
    });
    let completed = false;
    void run.then(() => {
      completed = true;
    });

    await vi.advanceTimersByTimeAsync(899_999);
    expect(completed).toBe(false);
    await vi.advanceTimersByTimeAsync(1);
    await expect(run).resolves.toMatchObject({
      reason: "no-output-timeout",
      noOutputTimedOut: true,
    });
  });

  it("restores the remaining ordinary watchdog when ask_user clears early", async () => {
    vi.useFakeTimers();
    let deadline: number | undefined = Date.now() + 3_610_000;
    const listeners = new Set<() => void>();
    const { run } = await startBlockedRun({
      timeoutMs: 2_000_000,
      getDeadline: () => deadline,
      onChange: (listener) => {
        listeners.add(listener);
        return () => listeners.delete(listener);
      },
    });

    await vi.advanceTimersByTimeAsync(200);
    deadline = undefined;
    listeners.forEach((listener) => listener());
    await vi.advanceTimersByTimeAsync(899_799);
    let completed = false;
    void run.then(() => {
      completed = true;
    });
    expect(completed).toBe(false);

    await vi.advanceTimersByTimeAsync(1);
    await expect(run).resolves.toMatchObject({
      reason: "no-output-timeout",
      noOutputTimedOut: true,
    });
  });
});