130 lines
4.4 KiB
JavaScript
130 lines
4.4 KiB
JavaScript
|
|
#!/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);
|