| import { describe, it } from 'node:test'; |
| import assert from 'node:assert/strict'; |
| import { fingerprintBefore, fingerprintAfter, checkout, checkin, poolStats, poolClear, invalidateFor } from '../src/conversation-pool.js'; |
|
|
| describe('fingerprintBefore', () => { |
| it('returns null for single-message conversations', () => { |
| assert.equal(fingerprintBefore([{ role: 'user', content: 'hi' }]), null); |
| }); |
|
|
| it('produces stable hash for same messages', () => { |
| const msgs = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: 'hi there' }, |
| { role: 'user', content: 'how are you' }, |
| ]; |
| assert.equal(fingerprintBefore(msgs), fingerprintBefore(msgs)); |
| }); |
|
|
| it('changes when prior user messages change', () => { |
| const msgs1 = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: 'hi' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| const msgs2 = [ |
| { role: 'user', content: 'different' }, |
| { role: 'assistant', content: 'hi' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| assert.notEqual(fingerprintBefore(msgs1), fingerprintBefore(msgs2)); |
| }); |
|
|
| it('changes when prior assistant text changes (v2.0.25 semantic key)', () => { |
| const msgs1 = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: 'response A' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| const msgs2 = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: 'completely different response' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| |
| |
| |
| |
| assert.notEqual(fingerprintBefore(msgs1), fingerprintBefore(msgs2)); |
| }); |
|
|
| it('changes when prior assistant tool_calls change (v2.0.25 semantic key)', () => { |
| const msgs1 = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: '', tool_calls: [{ function: { name: 'get_weather', arguments: '{"city":"SF"}' } }] }, |
| { role: 'tool', tool_call_id: 't1', content: '60F' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| const msgs2 = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: '', tool_calls: [{ function: { name: 'get_weather', arguments: '{"city":"NYC"}' } }] }, |
| { role: 'tool', tool_call_id: 't1', content: '60F' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| assert.notEqual(fingerprintBefore(msgs1), fingerprintBefore(msgs2)); |
| }); |
|
|
| it('canonicalizes assistant whitespace so reformatting still hits', () => { |
| const msgs1 = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: 'response\n\n A' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| const msgs2 = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: 'response A' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| assert.equal(fingerprintBefore(msgs1), fingerprintBefore(msgs2)); |
| }); |
|
|
| it('strips system-reminder meta tags before hashing', () => { |
| const msgs1 = [ |
| { role: 'user', content: 'hello <system-reminder>some state</system-reminder>' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| const msgs2 = [ |
| { role: 'user', content: 'hello <system-reminder>different state</system-reminder>' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| assert.equal(fingerprintBefore(msgs1), fingerprintBefore(msgs2)); |
| }); |
|
|
| it('includes model key in fingerprint', () => { |
| const msgs = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| assert.notEqual( |
| fingerprintBefore(msgs, 'gpt-4o'), |
| fingerprintBefore(msgs, 'claude-4.5-sonnet') |
| ); |
| }); |
|
|
| it('does not learn user XML tags into the global fingerprint stripper', () => { |
| poolClear(); |
| const caller = 'api:shared'; |
| const model = 'claude-opus-4.7'; |
| |
| const stored = fingerprintAfter( |
| [{ role: 'user', content: '<evil>attacker</evil>' }], |
| model, |
| caller |
| ); |
| |
| const victim = fingerprintBefore([ |
| { role: 'user', content: '<evil>victim</evil>' }, |
| { role: 'assistant', content: 'ok' }, |
| { role: 'user', content: 'continue' }, |
| ], model, caller); |
| assert.notEqual(stored, victim); |
| }); |
|
|
| it('changes when system prompt changes (v2.0.25 default-on)', () => { |
| const base = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: 'hi' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| const fpA = fingerprintBefore([{ role: 'system', content: 'be helpful' }, ...base]); |
| const fpB = fingerprintBefore([{ role: 'system', content: 'be terse' }, ...base]); |
| assert.notEqual(fpA, fpB); |
| }); |
|
|
| it('changes when image_url changes between turns (v2.0.25 stable media digest)', () => { |
| const buildMsgs = (url) => [ |
| { role: 'user', content: [{ type: 'text', text: 'describe' }, { type: 'image_url', image_url: { url } }] }, |
| { role: 'assistant', content: 'ok' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| assert.notEqual( |
| fingerprintBefore(buildMsgs('https://example.com/a.png')), |
| fingerprintBefore(buildMsgs('https://example.com/b.png')) |
| ); |
| }); |
|
|
| it('disables reuse (returns null) for image content with no stable id', () => { |
| const msgs = [ |
| { role: 'user', content: [{ type: 'image_url', image_url: { } }] }, |
| { role: 'assistant', content: 'ok' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| assert.equal(fingerprintBefore(msgs), null); |
| }); |
|
|
| it('changes when tool schema changes for emulated requests (v2.0.25 MED-1)', () => { |
| const msgs = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: 'ok' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| const tools1 = [{ function: { name: 'get_weather', description: 'get weather', parameters: { type: 'object', properties: { city: { type: 'string' } } } } }]; |
| const tools2 = [{ function: { name: 'get_weather', description: 'get weather', parameters: { type: 'object', properties: { country: { type: 'string' } } } } }]; |
| assert.notEqual( |
| fingerprintBefore(msgs, 'm', 'c', { emulateTools: true, tools: tools1 }), |
| fingerprintBefore(msgs, 'm', 'c', { emulateTools: true, tools: tools2 }) |
| ); |
| }); |
|
|
| it('object key order in mixed content is stable', () => { |
| const msgs1 = [ |
| { role: 'user', content: [{ type: 'image_url', image_url: { url: 'https://x/a.png', detail: 'auto' } }] }, |
| { role: 'assistant', content: 'ok' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| const msgs2 = [ |
| { role: 'user', content: [{ type: 'image_url', image_url: { detail: 'auto', url: 'https://x/a.png' } }] }, |
| { role: 'assistant', content: 'ok' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| assert.equal(fingerprintBefore(msgs1), fingerprintBefore(msgs2)); |
| }); |
| }); |
|
|
| describe('fingerprintAfter', () => { |
| it('produces a hash for single-message conversations', () => { |
| const fp = fingerprintAfter([{ role: 'user', content: 'hi' }]); |
| assert.ok(typeof fp === 'string' && fp.length === 64); |
| }); |
|
|
| it('differs from fingerprintBefore on same messages', () => { |
| const msgs = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| assert.notEqual(fingerprintBefore(msgs), fingerprintAfter(msgs)); |
| }); |
|
|
| it('after(turn1) matches before(turn1+assistant+turn2) — round-trip across turns', () => { |
| |
| |
| |
| |
| |
| const turn1 = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: 'hi there' }, |
| ]; |
| const turn2 = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: 'hi there' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| assert.equal( |
| fingerprintAfter(turn1, 'm', 'c'), |
| fingerprintBefore(turn2, 'm', 'c') |
| ); |
| }); |
| }); |
|
|
| describe('checkout / checkin', () => { |
| it('isolates identical prompt fingerprints by caller', () => { |
| poolClear(); |
| const msgs = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: 'hi' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| const fpA = fingerprintBefore(msgs, 'claude-opus-4.6', 'caller-a'); |
| const fpB = fingerprintBefore(msgs, 'claude-opus-4.6', 'caller-b'); |
| assert.notEqual(fpA, fpB); |
| checkin(fpA, { cascadeId: 'c-a', sessionId: 's-a', lsPort: 42100, apiKey: 'key-a' }, 'caller-a'); |
| assert.equal(checkout(fpB, 'caller-b'), null); |
| assert.equal(checkout(fpA, 'caller-a')?.cascadeId, 'c-a'); |
| }); |
|
|
| it('reuses for the same caller and prompt trajectory', () => { |
| poolClear(); |
| const msgs = [ |
| { role: 'user', content: 'hello' }, |
| { role: 'assistant', content: 'hi' }, |
| { role: 'user', content: 'next' }, |
| ]; |
| const fp = fingerprintBefore(msgs, 'claude-opus-4.6', 'caller-a'); |
| checkin(fp, { cascadeId: 'c-same', sessionId: 's-same', lsPort: 42100, apiKey: 'key-a' }, 'caller-a'); |
| assert.equal(checkout(fp, 'caller-a')?.cascadeId, 'c-same'); |
| }); |
|
|
| it('returns null on miss', () => { |
| assert.equal(checkout('nonexistent-fp'), null); |
| }); |
|
|
| it('round-trips an entry', () => { |
| const entry = { cascadeId: 'c1', sessionId: 's1', lsPort: 42100, apiKey: 'key1' }; |
| checkin('fp-test-1', entry); |
| const got = checkout('fp-test-1'); |
| assert.ok(got); |
| assert.equal(got.cascadeId, 'c1'); |
| assert.equal(got.lsPort, 42100); |
| }); |
|
|
| it('removes entry on checkout (mutual exclusion)', () => { |
| const entry = { cascadeId: 'c2', sessionId: 's2', lsPort: 42100, apiKey: 'key2' }; |
| checkin('fp-test-2', entry); |
| checkout('fp-test-2'); |
| assert.equal(checkout('fp-test-2'), null); |
| }); |
|
|
| it('rejects checkout with mismatched expected owner (v2.0.25 MED-3)', () => { |
| poolClear(); |
| const fp = 'fp-owner-test'; |
| checkin(fp, { cascadeId: 'c-own', sessionId: 's', lsPort: 42100, apiKey: 'key-A', lsGeneration: 'gen1' }); |
| assert.equal(checkout(fp, '', { apiKey: 'key-B' }), null, 'apiKey mismatch should miss'); |
| |
| checkin(fp, { cascadeId: 'c-own', sessionId: 's', lsPort: 42100, apiKey: 'key-A', lsGeneration: 'gen1' }); |
| assert.equal(checkout(fp, '', { apiKey: 'key-A', lsPort: 42999 }), null, 'lsPort mismatch should miss'); |
| checkin(fp, { cascadeId: 'c-own', sessionId: 's', lsPort: 42100, apiKey: 'key-A', lsGeneration: 'gen1' }); |
| assert.equal(checkout(fp, '', { apiKey: 'key-A', lsPort: 42100, lsGeneration: 'gen2' }), null, 'lsGeneration mismatch should miss'); |
| checkin(fp, { cascadeId: 'c-own', sessionId: 's', lsPort: 42100, apiKey: 'key-A', lsGeneration: 'gen1' }); |
| const ok = checkout(fp, '', { apiKey: 'key-A', lsPort: 42100, lsGeneration: 'gen1' }); |
| assert.equal(ok?.cascadeId, 'c-own', 'matching owner should hit'); |
| }); |
| }); |
|
|
| describe('invalidateFor', () => { |
| it('drops entries by apiKey', () => { |
| poolClear(); |
| checkin('fp-A', { cascadeId: 'cA', sessionId: 's', lsPort: 1, apiKey: 'key-A' }); |
| checkin('fp-B', { cascadeId: 'cB', sessionId: 's', lsPort: 1, apiKey: 'key-B' }); |
| assert.equal(invalidateFor({ apiKey: 'key-A' }), 1); |
| assert.ok(checkout('fp-B')); |
| }); |
|
|
| it('drops entries by lsPort but spares same-port entries with newer lsGeneration', () => { |
| poolClear(); |
| checkin('fp-old', { cascadeId: 'cold', sessionId: 's', lsPort: 100, apiKey: 'k', lsGeneration: 'g1' }); |
| checkin('fp-new', { cascadeId: 'cnew', sessionId: 's', lsPort: 100, apiKey: 'k', lsGeneration: 'g2' }); |
| |
| |
| assert.equal(invalidateFor({ lsPort: 100, lsGeneration: 'g1' }), 1); |
| assert.ok(checkout('fp-new')); |
| }); |
| }); |
|
|
| describe('poolStats', () => { |
| it('returns stats object with expected keys', () => { |
| const s = poolStats(); |
| assert.ok('size' in s); |
| assert.ok('hits' in s); |
| assert.ok('misses' in s); |
| assert.ok('hitRate' in s); |
| }); |
| }); |
|
|
| describe('TTL hint inheritance (v2.0.25 MED-2)', () => { |
| it('clears inherited 1h hint when checkin explicitly passes 0', () => { |
| poolClear(); |
| const fp1 = 'fp-ttl-1'; |
| checkin(fp1, { cascadeId: 'c', sessionId: 's', lsPort: 1, apiKey: 'k', ttlHintMs: 60 * 60 * 1000 }); |
| const got = checkout(fp1); |
| assert.equal(got?.ttlHintMs, 60 * 60 * 1000); |
| |
| checkin(fp1, got, '', 0); |
| const got2 = checkout(fp1); |
| assert.equal(got2?.ttlHintMs, undefined, 'explicit 0 should drop the inherited 1h hint'); |
| }); |
| }); |
|
|