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);
});
});
|