#!/usr/bin/env node /** * _observer-tests.js · agent-claim-observer 单元测试 * 零依赖, 直接跑: node scripts/patches/_observer-tests.js * * 覆盖: * - extractText: null / undefined / string / content-string / content-array / 非法对象 * - extractTraceId: 从 prompt / 从 text / 都无 / input=null / 边界 (< 8 char, > 64 char) * - ReDoS: 50KB 恶意输入 < 100ms */ 'use strict'; const assert = require('assert'); const path = require('path'); const CLAUDE_ROOT = path.resolve(__dirname, '..', '..'); const observer = require(path.join(CLAUDE_ROOT, 'hooks', 'agent-claim-observer.js')); const { extractText, extractTraceId } = observer; let pass = 0, fail = 0; const results = []; function test(name, fn) { try { fn(); pass++; results.push(['PASS', name]); } catch (e) { fail++; results.push(['FAIL', name + ' · ' + (e.message || e)]); } } // === extractText === test('extractText: null → ""', () => { assert.strictEqual(extractText(null), ''); }); test('extractText: undefined → ""', () => { assert.strictEqual(extractText(undefined), ''); }); test('extractText: 原始 string 原样返回', () => { assert.strictEqual(extractText('hello'), 'hello'); }); test('extractText: {content:"abc"} → "abc"', () => { assert.strictEqual(extractText({ content: 'abc' }), 'abc'); }); test('extractText: content 数组 block 拼接', () => { const input = { content: [ { type: 'text', text: 'a' }, { type: 'text', text: 'b' }, { type: 'image', source: 'x' }, // 非 text block 应忽略 ] }; assert.strictEqual(extractText(input), 'a\nb'); }); test('extractText: content 为非数组/非字符串对象 → ""', () => { assert.strictEqual(extractText({ content: { type: 'x' } }), ''); }); test('extractText: content=null → ""', () => { assert.strictEqual(extractText({ content: null }), ''); }); test('extractText: text 字段缺失的 block 忽略', () => { const input = { content: [{ type: 'text' }, { type: 'text', text: 'valid' }] }; assert.strictEqual(extractText(input), 'valid'); }); // === extractTraceId === test('extractTraceId: prompt 含 ', () => { const input = { tool_input: { prompt: 'do bwr-20260422-abc123 x' } }; assert.strictEqual(extractTraceId(input, ''), 'bwr-20260422-abc123'); }); test('extractTraceId: prompt 无, text 含 echo', () => { const input = { tool_input: { prompt: 'nothing' } }; assert.strictEqual( extractTraceId(input, 'done bwr-20260422xyz456-2aff6c'), 'bwr-20260422xyz456-2aff6c' ); }); test('extractTraceId: 都无 → ""', () => { assert.strictEqual(extractTraceId({ tool_input: {} }, ''), ''); }); test('extractTraceId: input=null → ""', () => { assert.strictEqual(extractTraceId(null, ''), ''); }); test('extractTraceId: 长度 < 8 拒绝 (bwr-a = 1 字符后缀)', () => { assert.strictEqual(extractTraceId({ tool_input: { prompt: 'bwr-a' } }, ''), ''); }); test('extractTraceId: 长度 > 64 拒绝', () => { const long = 'bwr-' + 'a'.repeat(80); assert.strictEqual(extractTraceId({ tool_input: { prompt: '' + long + '' } }, ''), ''); }); test('extractTraceId: prompt 优先于 text', () => { const input = { tool_input: { prompt: 'bwr-from-prompt-1' } }; assert.strictEqual( extractTraceId(input, 'bwr-from-text-99'), 'bwr-from-prompt-1' ); }); // === ReDoS 压测 === test('extractTraceId: 50KB 恶意输入 < 100ms', () => { const evil = '<'.repeat(50 * 1024) + 'trace>bwr-xyz12345-abc'; const start = Date.now(); extractTraceId({ tool_input: { prompt: evil } }, ''); const elapsed = Date.now() - start; assert.ok(elapsed < 100, 'elapsed ' + elapsed + 'ms (预期 < 100ms)'); }); test('extractText: 100KB content string 快速通过', () => { const big = { content: 'x'.repeat(100 * 1024) }; const start = Date.now(); const out = extractText(big); const elapsed = Date.now() - start; assert.strictEqual(out.length, 100 * 1024); assert.ok(elapsed < 50, 'elapsed ' + elapsed + 'ms'); }); // === 输出 === const LINE = '─'.repeat(60); console.log(LINE); console.log('agent-claim-observer · 单元测试'); console.log(LINE); for (const [status, name] of results) { const tag = status === 'PASS' ? '\x1b[32m[PASS]\x1b[0m' : '\x1b[31m[FAIL]\x1b[0m'; console.log(tag + ' ' + name); } console.log(LINE); console.log('Total: ' + (pass + fail) + ' Pass: ' + pass + ' Fail: ' + fail); process.exit(fail === 0 ? 0 : 1);