test/conversation-pool-meta-tags.test.js
Rama master · réplica de lectura del repositorio original.
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { __stripMetaTags, fingerprintAfter } from '../src/conversation-pool.js';
// stripMetaTags keeps the reuse fingerprint stable by deleting the
// client-injected meta tags whose bodies change every turn (issue #24).
// These tests pin the exact semantics of the old
// /<names>[^>]*>[\s\S]*?</\1>/g replace — verified byte-identical to the
// regex on a 40-case matrix + 20k-string fuzz — while asserting the new
// linear scanner kills the quadratic blowup (CWE-1333): the regex took
// ~7s on the 256KB unclosed-opens bomb measured on this machine.
describe('stripMetaTags semantics (regex-compatible)', () => {
it('removes a known tag span including attrs and body', () => {
assert.equal(__stripMetaTags('hello <system-reminder>state</system-reminder> world'), 'hello world');
assert.equal(__stripMetaTags('<system-reminder attr="v" n=2>body</system-reminder>'), '');
});
it('strips every tag name in META_TAG_NAMES', () => {
for (const t of ['system-reminder', 'command-message', 'command-name', 'command-args',
'local-command-stdout', 'local-command-stderr', 'user-prompt-submit-hook',
'analysis', 'summary', 'example']) {
assert.equal(__stripMetaTags(`x<${t}>body</${t}>y`), 'xy', `<${t}> should strip`);
}
});
it('first closer closes the first open; trailing closer stays literal', () => {
// <a><a></a></a> — regex removes 0..first </a>, leftover is a stray closer.
assert.equal(__stripMetaTags('<analysis><analysis></analysis></analysis>'), '</analysis>');
// One closer removes from the outermost open.
assert.equal(__stripMetaTags('<analysis><analysis></analysis>'), '');
});
it('inner different-named tag is removed inside the outer span', () => {
assert.equal(__stripMetaTags('<analysis><summary></summary></analysis>'), '');
});
it('mismatched open/closer pair stays literal', () => {
assert.equal(__stripMetaTags('<analysis></summary>'), '<analysis></summary>');
});
it('unclosed opens and stray closers stay literal', () => {
assert.equal(__stripMetaTags('<analysis/>x'), '<analysis/>x');
assert.equal(__stripMetaTags('keep <analysis>unclosed'), 'keep <analysis>unclosed');
assert.equal(__stripMetaTags('stray </analysis> closer'), 'stray </analysis> closer');
});
it('mixed closed and unclosed opens', () => {
assert.equal(
__stripMetaTags('a<summary>1</summary>b<analysis>2</analysis>c<example>gone</example>d'),
'abcd'
);
// closed summary removed, unclosed analysis kept
assert.equal(
__stripMetaTags('<summary>x</summary> tail <analysis>open'),
'tail <analysis>open'
);
});
it('attrs are [^>]* — embedded quotes, =, and even < do not end the open', () => {
assert.equal(__stripMetaTags('<command-args a="1" b="2">body</command-args>'), '');
// A '<' inside attrs is legal ([^>]* matches it); the open ends at the
// first '>' — which here belongs to <evil>, and the name is analysis.
assert.equal(__stripMetaTags('<analysis =<evil>"x</analysis>'), '');
// '>' inside an attr VALUE ends the open early: <a x="1> is the open,
// 2"> is body — the whole span still strips (regex has no quote rules).
assert.equal(__stripMetaTags('<analysis x="1>2">body</analysis>'), '');
});
it('tag name is a prefix match — <examplefoo> is an open of `example`', () => {
// [^>]* absorbs 'foo', so the required closer is </example>, not </examplefoo>.
assert.equal(__stripMetaTags('<examplefoo>x</example>'), '');
assert.equal(__stripMetaTags('<examplefoo>x</examplefoo>'), '<examplefoo>x</examplefoo>');
});
it('is case-sensitive and requires the name right after <', () => {
assert.equal(__stripMetaTags('<ANALYSIS>x</ANALYSIS>'), '<ANALYSIS>x</ANALYSIS>');
assert.equal(__stripMetaTags('<analysis>x</Analysis>'), '<analysis>x</Analysis>');
assert.equal(__stripMetaTags('< analysis>x</analysis>'), '< analysis>x</analysis>');
assert.equal(__stripMetaTags('<-analysis>x</analysis>'), '<-analysis>x</analysis>');
});
it('requires the exact </name> closer — attrs on the closer do not match', () => {
assert.equal(__stripMetaTags('<analysis >x</analysis >'), '<analysis >x</analysis >');
assert.equal(__stripMetaTags('<analysis>x</analysis >y'), '<analysis>x</analysis >y');
});
it('does not match unknown tag names and strips known tags inside them', () => {
assert.equal(__stripMetaTags('<evil>x</evil>'), '<evil>x</evil>');
assert.equal(__stripMetaTags('<a><analysis>x</analysis></a>'), '<a></a>');
});
it('crossed spans: first </a> closes the a-open, orphan </b> stays', () => {
assert.equal(__stripMetaTags('<analysis>a <summary>b</analysis>c</summary>'), 'c</summary>');
});
it('applies whitespace cleanup after stripping', () => {
assert.equal(__stripMetaTags('line1 \nline2\n\n\n\nline3'), 'line1\nline2\n\nline3');
assert.equal(__stripMetaTags(' padded '), 'padded');
assert.equal(__stripMetaTags('<analysis>x</analysis>\n\n\n\ntail'), 'tail');
});
it('returns non-string and empty input as-is', () => {
assert.equal(__stripMetaTags(''), '');
assert.equal(__stripMetaTags(null), null);
assert.equal(__stripMetaTags(undefined), undefined);
assert.equal(__stripMetaTags(42), 42);
});
it('caps scanned input at 262144 chars', () => {
assert.equal(__stripMetaTags('x'.repeat(300000)).length, 262144);
});
});
describe('stripMetaTags unknown-tag audit', () => {
function auditLog(input) {
const orig = console.error;
const lines = [];
console.error = (...a) => lines.push(a.join(' '));
try { __stripMetaTags(input); } finally { console.error = orig; }
return lines.join('\n');
}
it('logs unknown open tag names once, deduped in first-seen order', () => {
const lines = [];
const orig = console.error;
console.error = (...a) => lines.push(a.join(' '));
try { __stripMetaTags('<evil>a</evil> <badtag>x</badtag> <evil>again</evil>'); }
finally { console.error = orig; }
assert.deepEqual(lines, ['[META_TAG_AUDIT] Unknown XML tags in user message: evil, badtag']);
});
it('logs unclosed unknown opens (semantic change: more coverage, not less)', () => {
// The old paired <name>…</name> audit needed a closer; open-only scan
// still surfaces the name when the closer never arrives. (An open
// still needs its '>' — `<evil attr` alone is not a tag.)
assert.match(auditLog('<evil>never closed'), /evil/);
});
it('does not log known names or stray closers', () => {
assert.equal(auditLog('<analysis>unclosed'), '');
assert.equal(auditLog('stray </evil> only'), '');
});
});
describe('stripMetaTags ReDoS regression (CWE-1333)', () => {
// Old impl measured ~7000ms on this input; new impl ~4ms. 500ms is ~30x
// headroom for CI noise and still 14x under the old impl's time.
const BUDGET_MS = 500;
it('256KB of unclosed <system-reminder> opens strips linearly', () => {
const bomb = '<system-reminder>'.repeat(Math.ceil(262144 / 17));
const t0 = performance.now();
const out = __stripMetaTags(bomb);
const elapsed = performance.now() - t0;
// No closers exist → nothing strips; input is unchanged (modulo cap).
assert.equal(out, bomb.slice(0, 262144));
assert.ok(elapsed < BUDGET_MS, `unclosed-opens bomb took ${elapsed.toFixed(1)}ms (old impl ~7000ms)`);
});
it('256KB of <system-reminder with no > at all strips linearly', () => {
const bomb = '<system-reminder'.repeat(Math.ceil(262144 / 16));
const t0 = performance.now();
const out = __stripMetaTags(bomb);
const elapsed = performance.now() - t0;
assert.equal(out, bomb.slice(0, 262144));
assert.ok(elapsed < BUDGET_MS, `no-> bomb took ${elapsed.toFixed(1)}ms`);
});
it('256KB residual <x opens do not quadratic the unknown-tag audit', () => {
// After the strip leaves this untouched, the audit must scan it — the
// old paired regex spent 42s here; the suggested /<name[^>]*>/ regex
// would still be O(n²) with no '>' present. The manual scan is linear.
const bomb = '<x'.repeat(131072);
const orig = console.error;
console.error = () => {};
const t0 = performance.now();
try { __stripMetaTags(bomb); } finally { console.error = orig; }
const elapsed = performance.now() - t0;
assert.ok(elapsed < BUDGET_MS, `audit bomb took ${elapsed.toFixed(1)}ms (old audit ~42000ms)`);
});
it('fingerprinting stays correct on a bomb-shaped message', () => {
const bomb = '<system-reminder>'.repeat(2000);
const msgs = [
{ role: 'user', content: `u ${bomb}` },
{ role: 'assistant', content: 'ok' },
{ role: 'user', content: 'next' },
];
const t0 = performance.now();
const fp = fingerprintAfter(msgs, 'm', 'c');
const elapsed = performance.now() - t0;
assert.ok(typeof fp === 'string' && fp.length === 64);
assert.ok(elapsed < BUDGET_MS, `fingerprintAfter took ${elapsed.toFixed(1)}ms`);
});
});
// L1 meta-review F1: after a removed span the scanner must resume at the
// NEXT literal '<' — `i = last` treated whatever char followed the closer as
// a potential open char, so 'Xname' at that position was parsed as '<name'.
// These shapes are the adjacency class the fuzz corpus missed.
describe('post-span resume must re-anchor on a literal < (L1-F1)', () => {
it('text immediately after a removed span is literal, not an open', () => {
// 'Xsummary' is NOT '<summary' — the old regex kept the tail verbatim;
// the buggy `i = last` resume deleted the whole tail (fingerprint loss).
assert.equal(
__stripMetaTags('<summary>a</summary>Xsummary b>c</summary>'),
'Xsummary b>c</summary>',
);
assert.equal(
__stripMetaTags('<analysis>t</analysis>Xcommand-args b>c</command-args>'),
'Xcommand-args b>c</command-args>',
);
});
it('stray closers with no removed span still pass through', () => {
assert.equal(__stripMetaTags('keep</summary>vscommand-args x>y</command-args>tail'),
'keep</summary>vscommand-args x>y</command-args>tail');
});
it('consecutive real spans still strip correctly after the fix', () => {
assert.equal(__stripMetaTags('a<summary>x</summary>b<summary>y</summary>c'), 'abc');
});
});