/** * v1.8.3 — CLI bin/resolve.js --batch device-friendly output * * 设备端 follow-up request (device-team-followup-request-cli-tool-v1.8.3.md): * 设备端可弃用 docs/scripts/resolve-base-meta.js 临时 mirror script, * 切换到 npx @thejrsoft/subway-protocol resolve --batch < docs/wire-emit-codes-extended.jsonl * * 测试范围: * 1. 基本批量 (无 expected) — 输出 {code, ctx, derivedMeta, matchedBy} * 2. FALLBACK 路径 (未知 code) * 3. EXPLICIT 路径 (EXPLICIT_META 命中) * 4. SUFFIX 路径 (_FAILED / _SUCCESS 后缀) * 5. R1-R7 ctx 派生 (BATCH 子项 PROGRESS_UPDATE) */ import { spawnSync } from 'child_process'; import * as path from 'path'; const RESOLVE_BIN = path.resolve(__dirname, '../../bin/resolve.js'); function runBatch(input: string): { code: number; stdout: string; stderr: string } { const result = spawnSync('node', [RESOLVE_BIN, '--batch'], { input, encoding: 'utf8', timeout: 5000, }); return { code: result.status ?? -1, stdout: result.stdout || '', stderr: result.stderr || '', }; } function parseJsonl(s: string): any[] { return s.split('\n').filter(l => l.trim()).map(l => JSON.parse(l)); } describe('v1.8.3 — CLI bin/resolve.js --batch device-friendly output', () => { test('1. 基本批量 — 输出 {code, ctx, derivedMeta, matchedBy}', () => { const input = JSON.stringify({ code: 'SYNC_FUNCTIONS_SWITCH_READ_FAILED' }) + '\n'; const { code, stdout } = runBatch(input); expect(code).toBe(0); const out = parseJsonl(stdout); expect(out).toHaveLength(1); expect(out[0]).toMatchObject({ code: 'SYNC_FUNCTIONS_SWITCH_READ_FAILED', derivedMeta: expect.objectContaining({ isTerminal: true, status: expect.any(String), }), matchedBy: expect.stringMatching(/^(SUFFIX|EXPLICIT|FALLBACK)/), }); }); test('2. FALLBACK 路径 — 未知 code → matchedBy=FALLBACK', () => { const input = JSON.stringify({ code: 'TOTALLY_UNKNOWN_CODE_XYZ' }) + '\n'; const { code, stdout } = runBatch(input); expect(code).toBe(0); const out = parseJsonl(stdout); expect(out[0].matchedBy).toMatch(/^FALLBACK/); }); test('3. EXPLICIT 路径 — EXPLICIT_META 命中 → matchedBy=EXPLICIT', () => { const input = JSON.stringify({ code: 'PROGRAM_COMPLETED' }) + '\n'; const { code, stdout } = runBatch(input); expect(code).toBe(0); const out = parseJsonl(stdout); expect(out[0].matchedBy).toMatch(/^EXPLICIT/); }); test('4. SUFFIX 路径 — _FAILED 后缀 → matchedBy=SUFFIX:_FAILED', () => { const input = JSON.stringify({ code: 'BARGRAPH_LED_SWITCH_WRITE_FAILED' }) + '\n'; const { code, stdout } = runBatch(input); expect(code).toBe(0); const out = parseJsonl(stdout); expect(out[0].matchedBy).toMatch(/^SUFFIX:_/); }); test('5. R1-R7 ctx 派生 — BATCH 子项 PROGRESS_UPDATE → derivedMeta.appliedRules 含 R1 或 R2', () => { // BATCH 子项 PROGRESS_UPDATE 应触发 R1 (PROGRESS_UPDATE + base.isTerminal) 降级 const input = JSON.stringify({ code: 'BARGRAPH_LED_SWITCH_WRITE_FAILED', ctx: { messageType: 'PROGRESS_UPDATE', parentCommandType: 'BATCH', subCommandType: 'SIMPLE', }, }) + '\n'; const { code, stdout } = runBatch(input); expect(code).toBe(0); const out = parseJsonl(stdout); expect(out[0].derivedMeta.appliedRules).toEqual( expect.arrayContaining([expect.stringMatching(/^R[12]$/)]) ); expect(out[0].matchedBy).toMatch(/\+R[12]/); // status 应降级为 IN_PROGRESS expect(out[0].derivedMeta.status).toBe('IN_PROGRESS'); }); test('6. 多行批量 — 每行独立输出, JSONL 格式', () => { const lines = [ { code: 'SYNC_FUNCTIONS_SWITCH_READ_FAILED' }, { code: 'BARGRAPH_LED_SWITCH_WRITE_FAILED' }, { code: 'PROGRAM_COMPLETED' }, ].map(l => JSON.stringify(l)).join('\n') + '\n'; const { code, stdout } = runBatch(lines); expect(code).toBe(0); const out = parseJsonl(stdout); expect(out).toHaveLength(3); expect(out[0].code).toBe('SYNC_FUNCTIONS_SWITCH_READ_FAILED'); expect(out[1].code).toBe('BARGRAPH_LED_SWITCH_WRITE_FAILED'); expect(out[2].code).toBe('PROGRAM_COMPLETED'); }); test('7. 向后兼容 — 含 expected 字段时仍输出 match/diff', () => { const input = JSON.stringify({ code: 'SYNC_FUNCTIONS_SWITCH_READ_FAILED', expected: { isTerminal: false, status: 'FAILED', level: 'ERROR', semantic: 'failure', requiresErrorBlock: true }, }) + '\n'; const { code, stdout } = runBatch(input); // expected 不全匹配 (isTerminal 应为 true) → 退出码 1 expect([0, 1]).toContain(code); const out = parseJsonl(stdout); expect(out[0]).toHaveProperty('expected'); expect(out[0]).toHaveProperty('match'); }); test('8. subcommand 风格 — `resolve --batch` 第一个 positional "resolve" 被 shift 掉', () => { // npx 调用风格: `subway-protocol resolve --batch` → argv = ["resolve", "--batch"] const result = spawnSync('node', [RESOLVE_BIN, 'resolve', '--batch'], { input: JSON.stringify({ code: 'SYNC_FUNCTIONS_SWITCH_READ_FAILED' }) + '\n', encoding: 'utf8', timeout: 5000, }); expect(result.status).toBe(0); const out = parseJsonl(result.stdout || ''); expect(out[0].code).toBe('SYNC_FUNCTIONS_SWITCH_READ_FAILED'); }); });