File size: 4,618 Bytes
f0634fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
123
124
125
126
127
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { IAgentTaskService } from '#/agent/task/task';
import { SubagentTask } from '#/agent/tools/agent/subagent-task';
import { createTestAgent, type TestAgentContext } from '../../harness';

function agentTask(
  completion: Promise<{ result: string }>,
  description: string,
): SubagentTask {
  return new SubagentTask(
    { agentId: 'agent-child', profileName: 'coder', completion },
    description,
    new AbortController(),
  );
}

describe('SubagentTask — timeoutMs', () => {
  let ctx: TestAgentContext;
  let background: IAgentTaskService;

  beforeEach(() => {
    ctx = createTestAgent();
    background = ctx.get(IAgentTaskService);
  });

  afterEach(async () => {
    vi.useRealTimers();
    try {
      await ctx.expectResumeMatches();
    } finally {
      await ctx.dispose();
    }
  });

  it('external deadline marks task timed_out', async () => {
    vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
    const hangForever = new Promise<{ result: string }>(() => {});
    const taskId = background.registerTask(agentTask(hangForever, 'hang'), {
      timeoutMs: 2_000,
    });

    const terminalPromise = background.wait(taskId);
    await vi.advanceTimersByTimeAsync(7_100);
    const info = await terminalPromise;

    expect(info?.status).toBe('timed_out');
    expect(info?.stopReason).toBeUndefined();
  });

  it('omitting timeoutMs lets the task run to completion without a manager deadline', async () => {
    let resolveFn!: (r: { result: string }) => void;
    const completion = new Promise<{ result: string }>((res) => {
      resolveFn = res;
    });
    const taskId = background.registerTask(agentTask(completion, 'no deadline'));

    resolveFn({ result: 'finished' });
    const info = await background.wait(taskId);
    expect(info?.status).toBe('completed');
    expect(info?.stopReason).toBeUndefined();
  });

  it('internal TimeoutError rejection = generic failure with error reason', async () => {
    const internalErr = new Error('aiohttp sock_read timeout');
    internalErr.name = 'TimeoutError';
    const rejecting = Promise.reject(internalErr);
    const taskId = background.registerTask(agentTask(rejecting, 'internal timeout'), {
      timeoutMs: 900_000,
    });

    const info = await background.wait(taskId);
    expect(info?.status).toBe('failed');
    expect(info?.stopReason).toBe('aiohttp sock_read timeout');
  });

  it('explicit timeoutMs is persisted on the task info', async () => {
    vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
    let resolveFn!: (r: { result: string }) => void;
    const completion = new Promise<{ result: string }>((res) => {
      resolveFn = res;
    });
    const taskId = background.registerTask(
      agentTask(completion, 'persist timeout'),
      { timeoutMs: 1_800_000 },
    );
    const info = background.getTask(taskId);
    expect((info as unknown as { timeoutMs?: number }).timeoutMs).toBe(1_800_000);
    resolveFn({ result: 'finished' });
    await expect(background.wait(taskId)).resolves.toMatchObject({ status: 'completed' });
  });

  it('omitted timeoutMs leaves the task info field undefined', async () => {
    let resolveFn!: (r: { result: string }) => void;
    const completion = new Promise<{ result: string }>((res) => {
      resolveFn = res;
    });
    const taskId = background.registerTask(agentTask(completion, 'default timeout'));
    const info = background.getTask(taskId);
    expect((info as unknown as { timeoutMs?: number }).timeoutMs).toBeUndefined();
    resolveFn({ result: 'finished' });
    await expect(background.wait(taskId)).resolves.toMatchObject({ status: 'completed' });
  });

  it('timeoutMs=0 is preserved on the task info and does not arm a deadline', async () => {
    let resolveFn!: (r: { result: string }) => void;
    const completion = new Promise<{ result: string }>((res) => {
      resolveFn = res;
    });
    const taskId = background.registerTask(agentTask(completion, 'zero timeout'), {
      timeoutMs: 0,
    });
    const initial = background.getTask(taskId);
    expect((initial as unknown as { timeoutMs?: number }).timeoutMs).toBe(0);

    const info = await background.wait(taskId, 5);
    const raced = info === undefined ? undefined : {
      status: info.status,
      stopReason: info.stopReason,
    };
    expect(raced?.status).toBe('running');
    expect(raced?.stopReason).toBeUndefined();
    resolveFn({ result: 'finished' });
    await expect(background.wait(taskId)).resolves.toMatchObject({ status: 'completed' });
  });
});