File size: 4,193 Bytes
391c43e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { OswsToolExecutor } from '../tool-executor';
import type { ToolCall } from '../core/types';

const h = vi.hoisted(() => ({
  toolOutput: '',
}));

vi.mock('../tool-registry', () => ({
  toolRegistry: {
    execute: vi.fn(async () => h.toolOutput),
    getDefinitions: () => [],
  },
}));

function makeExecutor(): OswsToolExecutor {
  return new OswsToolExecutor({
    projectId: 'p1',
    progress: { onEvent: vi.fn() },
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    getAgent: () => ({ type: 'orchestrator', hasTool: () => true, tools: ['bash'] } as any),
    chatMode: false,
    abortSignal: new AbortController().signal,
  });
}

function statusCall(cmd: string): ToolCall {
  return {
    id: 'tc1',
    type: 'function',
    function: { name: 'bash', arguments: JSON.stringify({ command: cmd }) },
  };
}

describe('OswsToolExecutor status signal extraction', () => {
  beforeEach(() => {
    h.toolOutput = '';
  });

  it('extracts a complete status from the terse output format', async () => {
    h.toolOutput = 'Status recorded.\nRemaining: none\nComplete: yes';
    const result = await makeExecutor().execute(
      statusCall('status --task "build site" --done "all done" --remaining "none" --complete'),
      { agentType: 'orchestrator', isReadOnly: false },
    );

    expect(result.signals?.statusComplete).toBe(true);
    const statusResult = result.signals?.statusResult as { complete: boolean; remaining: string; hasExplicitFlag: boolean };
    expect(statusResult.complete).toBe(true);
    expect(statusResult.remaining).toBe('none');
    expect(statusResult.hasExplicitFlag).toBe(true);
  });

  it('extracts an incomplete status with remaining work from terse output', async () => {
    h.toolOutput = 'Status recorded.\nRemaining: fix the nav\nComplete: no';
    const result = await makeExecutor().execute(
      statusCall('status --task "t" --done "d" --remaining "fix the nav" --incomplete'),
      { agentType: 'orchestrator', isReadOnly: false },
    );

    expect(result.signals?.statusComplete).toBeUndefined();
    const statusResult = result.signals?.statusResult as { complete: boolean; remaining: string };
    expect(statusResult.complete).toBe(false);
    expect(statusResult.remaining).toBe('fix the nav');
  });

  it('prefers output lines over command parsing when they disagree (escaped quotes)', async () => {
    // The shell parses escaped quotes correctly; the command-regex fallback cannot.
    h.toolOutput = 'Status recorded.\nRemaining: add "About" page\nComplete: no';
    const result = await makeExecutor().execute(
      statusCall('status --task "t" --done "d" --remaining "add \\"About\\" page"'),
      { agentType: 'orchestrator', isReadOnly: false },
    );

    const statusResult = result.signals?.statusResult as { remaining: string };
    expect(statusResult.remaining).toBe('add "About" page');
  });

  it('extracts a complete status when status is chained after another command (build && status)', async () => {
    // The shell ran both; the combined output carries the status lines. The
    // completion signal must still be detected even though the command does
    // not start with "status".
    h.toolOutput = 'Build successful — 0 errors\nStatus recorded.\nRemaining: none\nComplete: yes';
    const result = await makeExecutor().execute(
      statusCall('build && status --task "impl" --done "all" --remaining "none" --complete'),
      { agentType: 'orchestrator', isReadOnly: false },
    );

    expect(result.signals?.statusComplete).toBe(true);
    const statusResult = result.signals?.statusResult as { complete: boolean };
    expect(statusResult.complete).toBe(true);
  });

  it('still extracts from the legacy full output format', async () => {
    h.toolOutput = 'Task: build site\nDone: all done\nRemaining: none\nComplete: yes';
    const result = await makeExecutor().execute(
      statusCall('status --task "build site" --done "all done" --remaining "none" --complete'),
      { agentType: 'orchestrator', isReadOnly: false },
    );

    expect(result.signals?.statusComplete).toBe(true);
  });
});