Implements the execa Agent Note's four sub-changes:
- execa (root devDep + loader-smoke dep) replaces the hand-rolled
spawn-collect-timeout choreography in loader-smoke, apps/cli and
cli-demo/acp-demo built-bin e2e, lsp-local and code-runtime-worker
built-lib e2e, the tui pty-harness outer collector, the jsonrpc
keyless smoke, and crash-recovery's child spawn. Genuinely custom
parts stay custom: cli-demo's interrupt-on-marker, jsonrpc's
line-predicate protocol driving, crash-recovery's SIGKILL-at-failpoint.
The two loader-smoke /* v8 ignore */ OS-error branches are gone.
- llm-mock-server CLI tokenizes via node:util parseArgs; numeric
coercion/bounds/cross-option constraints stay manual; pinned
error-message tests updated to the parseArgs texts.
- both loadRootEnv copies in apps/web/tests are deleted: the owning
vitest configs (web unconditionally, snapshot in record mode)
already load the repo-root .env before these files run.
- the four poll loops (acp-snapshot harness waits + crash-recovery
waitForFile) ride vi.waitFor with explicit {interval, timeout}.
129 lines
4.5 KiB
TypeScript
129 lines
4.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
MOCK_LLM_CLI_USAGE,
|
|
parseMockLlmCliArgs,
|
|
} from '../src/cli.ts'
|
|
|
|
describe('mock LLM server CLI parser', () => {
|
|
it('returns help without requiring a sequence', () => {
|
|
expect(parseMockLlmCliArgs(['--help'])).toEqual({ kind: 'help' })
|
|
expect(MOCK_LLM_CLI_USAGE).toContain('--sequence')
|
|
})
|
|
|
|
it('parses every request and listener option', () => {
|
|
expect(parseMockLlmCliArgs([
|
|
'--sequence', 'connection_refused,partial_disconnect,success',
|
|
'--host', 'localhost',
|
|
'--port', '9010',
|
|
'--api-key', 'mock-key',
|
|
'--listen-delay-ms', '100',
|
|
'--repeat-last',
|
|
'--success-text', 'done',
|
|
'--partial-text', 'half',
|
|
'--reasoning-text', 'think',
|
|
'--chunk-size', '2',
|
|
'--chunk-delay-ms', '3',
|
|
'--disconnect-delay-ms', '4',
|
|
'--retry-after-ms', '5000',
|
|
'--request-id', 'request-1',
|
|
'--tool-name', 'lookup',
|
|
'--tool-arguments', '{"id":1}',
|
|
])).toEqual({
|
|
kind: 'run',
|
|
config: {
|
|
startsUnavailable: true,
|
|
listenDelayMs: 100,
|
|
server: {
|
|
sequence: ['partial_disconnect', 'success'],
|
|
host: 'localhost',
|
|
port: 9010,
|
|
apiKey: 'mock-key',
|
|
repeatLast: true,
|
|
successText: 'done',
|
|
partialText: 'half',
|
|
reasoningText: 'think',
|
|
chunkSize: 2,
|
|
chunkDelayMs: 3,
|
|
disconnectDelayMs: 4,
|
|
retryAfterMs: 5000,
|
|
requestId: 'request-1',
|
|
toolName: 'lookup',
|
|
toolArguments: '{"id":1}',
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
it('uses standalone defaults for an ordinary sequence', () => {
|
|
expect(parseMockLlmCliArgs(['--sequence', 'success'])).toEqual({
|
|
kind: 'run',
|
|
config: {
|
|
startsUnavailable: false,
|
|
listenDelayMs: 0,
|
|
server: {
|
|
sequence: ['success'],
|
|
port: 8000,
|
|
repeatLast: false,
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
it('uses the default unavailable interval', () => {
|
|
const result = parseMockLlmCliArgs(['--sequence', 'connection_refused,success', '--port', '8001'])
|
|
expect(result).toMatchObject({
|
|
kind: 'run',
|
|
config: { startsUnavailable: true, listenDelayMs: 750 },
|
|
})
|
|
})
|
|
|
|
it('parses a reproducible weighted random profile', () => {
|
|
expect(parseMockLlmCliArgs([
|
|
'--sequence', 'random',
|
|
'--repeat-last',
|
|
'--seed', '42',
|
|
'--random-weights', 'success=8,partial_disconnect=2',
|
|
])).toEqual({
|
|
kind: 'run',
|
|
config: {
|
|
startsUnavailable: false,
|
|
listenDelayMs: 0,
|
|
server: {
|
|
sequence: ['random'],
|
|
port: 8000,
|
|
repeatLast: true,
|
|
randomSeed: 42,
|
|
randomWeights: { success: 8, partial_disconnect: 2 },
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
it.each([
|
|
[[], /--sequence is required/],
|
|
// Tokenizer-level failures carry node:util parseArgs's own messages.
|
|
[['--wat'], /Unknown option '--wat'/],
|
|
[['--wat', 'x'], /Unknown option '--wat'/],
|
|
[['--port'], /Option '--port <value>' argument missing/],
|
|
[['--sequence', 'success', 'stray'], /Unexpected argument 'stray'/],
|
|
[['--port', 'NaN', '--sequence', 'success'], /finite number/],
|
|
[['--sequence', 'success,'], /non-empty/],
|
|
[['--sequence', 'success,connection_refused'], /only as the first/],
|
|
[['--sequence', 'connection_refused'], /must be followed/],
|
|
[['--sequence', 'unknown'], /unknown behavior/],
|
|
[['--sequence', 'connection_refused,success', '--port', '0'], /nonzero/],
|
|
[['--sequence', 'success', '--listen-delay-ms', '5'], /requires connection_refused/],
|
|
// `=` syntax: a space-separated leading-dash value is a tokenizer error, not a bounds probe.
|
|
[['--sequence', 'connection_refused,success', '--listen-delay-ms=-1'], /integer between 0 and 2147483647/],
|
|
[['--sequence', 'connection_refused,success', '--listen-delay-ms', '1.5'], /integer between 0 and 2147483647/],
|
|
[['--sequence', 'connection_refused,success', '--listen-delay-ms', '2147483648'], /integer between 0 and 2147483647/],
|
|
[['--sequence', 'success', '--seed', '1'], /require random/],
|
|
[['--sequence', 'random', '--random-weights', 'success'], /expects behavior=weight/],
|
|
[['--sequence', 'random', '--random-weights', 'random=1'], /concrete behavior/],
|
|
[['--sequence', 'random', '--random-weights', 'success=1,success=2'], /duplicate/],
|
|
[['--sequence', 'random', '--random-weights', 'success=nope'], /finite number/],
|
|
])('rejects invalid argv %#', (argv, expected) => {
|
|
expect(() => parseMockLlmCliArgs(argv)).toThrow(expected)
|
|
})
|
|
})
|