bookworm-smart-assistant/scripts/patches/_observer-tests.js

130 lines
4.4 KiB
JavaScript
Raw Normal View History

#!/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 含 <trace>', () => {
const input = { tool_input: { prompt: 'do <trace>bwr-20260422-abc123</trace> x' } };
assert.strictEqual(extractTraceId(input, ''), 'bwr-20260422-abc123');
});
test('extractTraceId: prompt 无, text 含 echo', () => {
const input = { tool_input: { prompt: 'nothing' } };
assert.strictEqual(
extractTraceId(input, 'done <trace>bwr-20260422xyz456-2aff6c</trace>'),
'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: '<trace>bwr-a</trace>' } }, ''), '');
});
test('extractTraceId: 长度 > 64 拒绝', () => {
const long = 'bwr-' + 'a'.repeat(80);
assert.strictEqual(extractTraceId({ tool_input: { prompt: '<trace>' + long + '</trace>' } }, ''), '');
});
test('extractTraceId: prompt 优先于 text', () => {
const input = { tool_input: { prompt: '<trace>bwr-from-prompt-1</trace>' } };
assert.strictEqual(
extractTraceId(input, '<trace>bwr-from-text-99</trace>'),
'bwr-from-prompt-1'
);
});
// === ReDoS 压测 ===
test('extractTraceId: 50KB 恶意输入 < 100ms', () => {
const evil = '<'.repeat(50 * 1024) + 'trace>bwr-xyz12345-abc</trace>';
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);