Files
deepseek-harness/packages/support/llm-mock-server/tests/cli.spec.ts
T
Tianyi Cui c4647a8609 test: adopt execa for hand-rolled subprocess plumbing, parseArgs for llm-mock-server CLI, vi.waitFor for poll loops
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}.
2026-07-26 22:29:41 +08:00

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