// Detach P1 — registry contract (R1–R9). // // The registry is the only shared state a detached turn adds. These tests pin // the four properties the design calls P0/P1: // - the key includes the trusted scope and every output-affecting decision // (a cross-tenant hit is a data leak); // - claim is a CAS (one resumer; a second identical request stays foreground); // - TTL/caps evict only non-claimed entries (a claimed entry belongs to a // live request); // - counters are one funnel (value/waste metrics cannot diverge). import './setup-env.mjs'; import { afterEach, describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { DetachedRegistry, detachedKey, cascadeDetachEnabled, detachedRegistry, } from '../src/detached-registry.js'; import * as auth from '../src/auth.js'; import { getConnectMetrics, resetConnectMetrics } from '../src/devin-connect-metrics.js'; const BODY = { model: 'gemini-2.5-flash', messages: [{ role: 'user', content: 'resume me' }], stream: true, }; function keyOpts(overrides = {}) { return { scopeTrusted: true, route: 'chat', modelKey: 'gemini-2.5-flash', toolPreambleTier: null, emulateTools: false, native: null, stop: [], displayModel: 'gemini-2.5-flash', ...overrides, }; } function snapshot(chunkText = 'hello') { return { version: 2, cascadeId: 'cascade-1', stepOffset: 1, pollFloor: 0, windowStart: 0, lastWindowStart: 0, overlap: 8, yieldedByStep: [[0, chunkText.length]], thinkingByStep: [], totalYielded: chunkText.length, totalThinking: 0, sawText: true, generatorOffset: 0, sawActive: true, usageByStep: [], consumedThrough: 1, chunks: [{ text: chunkText, thinking: '', isError: false }], }; } function pin(now, overrides = {}) { return { cascadeId: 'cascade-1', sessionId: 'session-1', lsPort: 42100, lsGeneration: 1, accountId: 'acct-1', modelKey: 'gemini-2.5-flash', generatorOffset: 0, deadline: now + 60_000, startedAt: now, ...overrides, }; } function clock(start = 1_000_000) { let now = start; return { now: () => now, advance(ms) { now += ms; }, }; } function makeRegistry(opts = {}) { const c = clock(); const registry = new DetachedRegistry({ timers: false, now: c.now, ...opts }); return { registry, clock: c }; } function admit(registry, clock, { key = 'k1', callerKey = 'session:a', snap = snapshot() } = {}) { return registry.admit({ key, callerKey, snapshot: snap, pin: pin(clock.now()) }); } describe('detachedKey', () => { it('R1 same body + same trusted scope → same key; a different scope → different key', () => { const a = detachedKey(BODY, 'session:a', keyOpts()); const a2 = detachedKey(BODY, 'session:a', keyOpts()); const b = detachedKey(BODY, 'session:b', keyOpts()); assert.equal(typeof a, 'string'); assert.equal(a, a2); assert.notEqual(a, b, 'a cross-tenant hit would serve one caller another caller\'s answer'); }); it('R2 different route / resolved model / native options / stop → different key', () => { const base = detachedKey(BODY, 'session:a', keyOpts()); const variants = [ keyOpts({ route: 'messages' }), keyOpts({ modelKey: 'claude-sonnet-4-6' }), keyOpts({ displayModel: 'claude-sonnet-4-6' }), keyOpts({ emulateTools: true }), keyOpts({ toolPreambleTier: 'schema' }), keyOpts({ native: { enabled: true, allowlist: ['view_file'] } }), keyOpts({ stop: ['STOP'] }), ]; for (const opts of variants) { assert.notEqual(detachedKey(BODY, 'session:a', opts), base, JSON.stringify(opts)); } }); it('R3 untrusted scope → null key and admit refuses no_scope', () => { assert.equal(detachedKey(BODY, 'api:deadbeef', keyOpts({ scopeTrusted: false })), null); const { registry, clock } = makeRegistry(); const refused = registry.admit({ key: detachedKey(BODY, 'api:deadbeef', keyOpts({ scopeTrusted: false })), callerKey: 'api:deadbeef', snapshot: snapshot(), pin: pin(clock.now()), }); assert.equal(refused.refused, 'no_scope'); assert.equal(registry.stats().admit, 0); // Positive control: the same body under a trusted scope is admitted. assert.ok(admit(registry, clock).token); }); it('is gated by WINDSURFAPI_CASCADE_DETACH and requires the producer flag', () => { assert.equal(cascadeDetachEnabled({}), false); assert.equal(cascadeDetachEnabled({ WINDSURFAPI_CASCADE_DETACH: '1' }), false); assert.equal(cascadeDetachEnabled({ WINDSURFAPI_CASCADE_TURN: '1' }), false); assert.equal(cascadeDetachEnabled({ WINDSURFAPI_CASCADE_TURN: '1', WINDSURFAPI_CASCADE_DETACH: '1', }), true); }); }); describe('DetachedRegistry ownership', () => { it('R4 claim is a CAS: first ok, second busy, third ok after release(running)', () => { const { registry, clock } = makeRegistry(); assert.ok(admit(registry, clock).token); const first = registry.claim('k1', 'session:a'); assert.ok(first.entry, 'first claim must win'); const second = registry.claim('k1', 'session:a'); assert.equal(second.refused, 'busy', 'a second resumer must not share cursors'); assert.equal(registry.stats().claim.busy, 1); assert.equal(registry.release('k1', first.token, { state: 'running', snapshot: snapshot('grown') }), true); const third = registry.claim('k1', 'session:a'); assert.ok(third.entry, 'a re-detached entry is claimable again'); assert.equal(registry.stats().re_detach, 1); }); it('R4b release requires the claimant token; a wrong token cannot steal an entry', () => { const { registry, clock } = makeRegistry(); admit(registry, clock); const claim = registry.claim('k1', 'session:a'); assert.equal(registry.release('k1', 'not-the-token', { state: 'served' }), false); assert.ok(registry.lookup('k1', 'session:a'), 'the entry is still claimed by the real token'); assert.equal(registry.release('k1', claim.token, { state: 'served' }), true); assert.equal(registry.lookup('k1', 'session:a'), null); assert.equal(registry.stats().serve, 1); }); it('R5 TTL expiry refuses the claim and counts evict{ttl}', () => { const { registry, clock } = makeRegistry({ ttlMs: 120_000 }); admit(registry, clock); clock.advance(120_001); assert.equal(registry.sweepExpired(), 1); assert.equal(registry.claim('k1', 'session:a').refused, 'gone'); assert.equal(registry.stats().evict.ttl, 1); assert.equal(registry.stats().entries, 0); // A real (unref'd) timer takes the same path. const timed = makeRegistry({ ttlMs: 5 }); timed.registry.timers = true; timed.registry.admit({ key: 'k2', callerKey: 'session:a', snapshot: snapshot(), pin: pin(timed.clock.now()) }); // The injected clock does not move for setTimeout, so drive sweep instead. timed.clock.advance(6); assert.equal(timed.registry.sweepExpired(), 1); assert.equal(timed.registry.stats().evict.ttl, 1); }); it('R5b TTL is an admission bound: a claimed entry survives sweep and lookup, and still releases served', () => { // Review D1: claim at t=119999, sweep at t=120001 used to delete the // claimed entry; the original token's release then returned false // (serve=0 while the turn was still billed). const { registry, clock } = makeRegistry({ ttlMs: 120_000 }); registry.admit({ key: 'k1', callerKey: 'session:a', snapshot: snapshot(), pin: pin(clock.now(), { deadline: clock.now() + 600_000 }), }); const claim = registry.claim('k1', 'session:a'); assert.ok(claim.entry, 'precondition: the claim wins'); clock.advance(120_001); assert.equal(registry.sweepExpired(), 0, 'TTL must not evict a claim'); assert.equal(registry.stats().evict.ttl, 0); assert.equal(registry.lookup('k1', 'session:a')?.state, 'claimed', 'lookup must not evict a claim past its admission window'); assert.equal(registry.release('k1', claim.token, { state: 'served' }), true, 'the original token still owns the entry past the TTL'); assert.equal(registry.stats().serve, 1); assert.equal(registry.stats().entries, 0); }); it('R5c a claim is bounded by the original request deadline, not by TTL', () => { const { registry, clock } = makeRegistry({ ttlMs: 120_000 }); registry.admit({ key: 'k1', callerKey: 'session:a', snapshot: snapshot(), pin: pin(clock.now(), { deadline: clock.now() + 1_000 }), }); const claim = registry.claim('k1', 'session:a'); assert.ok(claim.entry); clock.advance(1_001); assert.equal(registry.sweepExpired(), 1, 'the deadline still cleans up a claim whose request never returned'); assert.equal(registry.stats().evict.deadline, 1); assert.equal(registry.stats().evict.ttl, 0); assert.equal(registry.release('k1', claim.token, { state: 'served' }), false); }); it('claim_release_after_deadline_is_independent_of_cleanup_order', () => { // The deadline can be observed by the timer/sweep first or by release // first. Both interleavings must produce the same facts: false, serve=0, // exactly one deadline eviction, no re-detach, no entry left. Before the // deadline check lived in release(), the release-first interleaving // counted a serve and deleted the entry without naming the deadline. for (const state of ['served', 'running']) { for (const cleanupFirst of [true, false]) { const { registry, clock } = makeRegistry({ ttlMs: 120_000 }); registry.admit({ key: 'k1', callerKey: 'session:a', snapshot: snapshot(), pin: pin(clock.now(), { deadline: clock.now() + 1_000 }), }); const claim = registry.claim('k1', 'session:a'); assert.ok(claim.entry, 'precondition: the claim wins'); clock.advance(1_001); if (cleanupFirst) { assert.equal(registry.sweepExpired(), 1, 'the deadline sweep drops the claim'); assert.equal(registry.stats().evict.deadline, 1); } assert.equal(registry.release('k1', claim.token, { state, ...(state === 'running' ? { snapshot: snapshot('grown') } : {}), }), false, `${state}/${cleanupFirst ? 'sweep-first' : 'release-first'}: release must refuse`); const stats = registry.stats(); assert.equal(stats.serve, 0, `${state}/${cleanupFirst ? 'sweep-first' : 'release-first'}: no serve`); assert.equal(stats.re_detach, 0, `${state}/${cleanupFirst ? 'sweep-first' : 'release-first'}: no re-detach`); assert.equal(stats.evict.deadline, 1, `${state}/${cleanupFirst ? 'sweep-first' : 'release-first'}: exactly one deadline eviction`); assert.equal(stats.evict.resume_miss, 0); assert.equal(stats.evict.ttl, 0); assert.equal(stats.entries, 0); assert.equal(registry.lookup('k1', 'session:a'), null); } } }); it('R7c a claimed entry is only evictable with its own token', () => { const { registry, clock } = makeRegistry(); assert.ok(admit(registry, clock).token); const claim = registry.claim('k1', 'session:a'); assert.ok(claim.entry); assert.equal(registry.evict('k1', 'resume_miss'), false, 'a key-only evict must not drop a live claim'); assert.equal(registry.evict('k1', 'resume_miss', 'not-the-token'), false); assert.equal(registry.lookup('k1', 'session:a')?.state, 'claimed'); assert.equal(registry.evict('k1', 'resume_miss', claim.token), true); assert.equal(registry.lookup('k1', 'session:a'), null); }); it('R7d evict is identity-checked: a stale key or token cannot delete a same-key successor (ABA)', () => { const { registry, clock } = makeRegistry(); assert.ok(admit(registry, clock).token); const first = registry.claim('k1', 'session:a'); assert.ok(first.entry); // The owner re-detaches (token cleared), then a new admission supersedes // the entry under the same key — the old handler still holds `first.token`. assert.equal(registry.release('k1', first.token, { state: 'running', snapshot: snapshot('grown') }), true); assert.ok(admit(registry, clock, { key: 'k1', snap: snapshot('next') }).token); assert.equal(registry.evict('k1', 'resume_miss'), false, 'a bare key is not an identity'); assert.equal(registry.evict('k1', 'resume_miss', first.token), false, 'a stale claim token is not the successor'); const current = registry.lookup('k1', 'session:a'); assert.ok(current, 'the successor survives'); assert.equal(registry.evict('k1', 'resume_miss', current), true, 'the instance the caller holds is evictable'); assert.equal(registry.lookup('k1', 'session:a'), null); }); it('R6 byte cap refuses admission (no truncation state)', () => { const { registry, clock } = makeRegistry({ maxEntryBytes: 2048 }); const big = snapshot('x'.repeat(4096)); const refused = registry.admit({ key: 'big', callerKey: 'session:a', snapshot: big, pin: pin(clock.now()) }); assert.equal(refused.refused, 'over_bytes'); assert.equal(registry.stats().refuse.over_bytes, 1); assert.equal(registry.stats().entries, 0); // Positive control: a small snapshot is admitted. assert.ok(admit(registry, clock).token); }); it('R6b a refused over-bytes admission never destroys valid entries', () => { // Review360 P2-1: the byte checks used to run AFTER the same-key supersede // and the capacity evictions, so a 2-entry registry lost one live entry to // an admission that was then refused. Reproduce the 2→1 loss. const { registry, clock } = makeRegistry({ maxEntries: 2, maxPerCaller: 10, maxEntryBytes: 4096, maxTotalBytes: 1 << 20, }); admit(registry, clock, { key: 'a', callerKey: 'session:a', snap: snapshot('aaaa') }); admit(registry, clock, { key: 'c', callerKey: 'session:c', snap: snapshot('cccc') }); assert.equal(registry.stats().entries, 2); const refused = registry.admit({ key: 'huge', callerKey: 'session:b', snapshot: snapshot('x'.repeat(5000)), pin: pin(clock.now()), }); assert.equal(refused.refused, 'over_bytes'); assert.equal(registry.stats().entries, 2, 'a refused admission must not evict capacity entries'); assert.equal(registry.stats().evict.capacity, 0); assert.ok(registry.lookup('a', 'session:a'), 'the first valid entry survives'); assert.ok(registry.lookup('c', 'session:c'), 'the second valid entry survives'); // The same-key supersede is behind the same check: a refused replacement // leaves the original entry (and its bytes) intact. const replaced = registry.admit({ key: 'a', callerKey: 'session:a', snapshot: snapshot('y'.repeat(5000)), pin: pin(clock.now()), }); assert.equal(replaced.refused, 'over_bytes'); assert.equal(registry.stats().evict.superseded, 0); assert.equal(registry.lookup('a', 'session:a').snapshot.chunks[0].text, 'aaaa'); // The total cap is checked before capacity eviction too. registry.maxTotalBytes = registry.totalBytes + 1; const overTotal = registry.admit({ key: 'd', callerKey: 'session:d', snapshot: snapshot('dddd'), pin: pin(clock.now()), }); assert.equal(overTotal.refused, 'over_bytes'); assert.equal(registry.stats().entries, 2); assert.equal(registry.stats().evict.capacity, 0); // A same-key replacement credits the bytes it frees, so it still fits. const okReplace = registry.admit({ key: 'a', callerKey: 'session:a', snapshot: snapshot('a'), pin: pin(clock.now()), }); assert.ok(okReplace.token); assert.equal(registry.lookup('a', 'session:a').snapshot.chunks[0].text, 'a'); assert.equal(registry.stats().entries, 2); assert.ok(registry.totalBytes <= registry.maxTotalBytes); }); it('R7 entry cap evicts only non-claimed entries', () => { const { registry, clock } = makeRegistry({ maxEntries: 2, maxPerCaller: 10 }); admit(registry, clock, { key: 'a', callerKey: 'session:a' }); admit(registry, clock, { key: 'b', callerKey: 'session:b' }); const claimed = registry.claim('a', 'session:a'); assert.ok(claimed.entry); // The cap is reached; the oldest RUNNING entry (b) is evicted, never the // claimed one (a). assert.ok(admit(registry, clock, { key: 'c', callerKey: 'session:c' }).token); assert.equal(registry.stats().entries, 2); assert.equal(registry.lookup('b', 'session:b'), null); assert.ok(registry.lookup('a', 'session:a'), 'a claimed entry must not be evicted for capacity'); // All remaining entries claimed → the next admission is refused. registry.claim('c', 'session:c'); assert.equal(admit(registry, clock, { key: 'd', callerKey: 'session:d' }).refused, 'over_entries'); }); it('R7b per-caller cap evicts only that caller\'s oldest running entry', () => { const { registry, clock } = makeRegistry({ maxEntries: 8, maxPerCaller: 2 }); admit(registry, clock, { key: 'a1', callerKey: 'session:a' }); admit(registry, clock, { key: 'a2', callerKey: 'session:a' }); assert.ok(admit(registry, clock, { key: 'a3', callerKey: 'session:a' }).token); assert.equal(registry.lookup('a1', 'session:a'), null, 'oldest of that caller is evicted'); assert.ok(registry.lookup('a2', 'session:a')); assert.ok(registry.lookup('a3', 'session:a')); }); it('R8 beginDrain refuses admissions; close evicts every entry', () => { const { registry, clock } = makeRegistry(); admit(registry, clock, { key: 'a', callerKey: 'session:a' }); registry.claim('a', 'session:a'); admit(registry, clock, { key: 'b', callerKey: 'session:b' }); registry.beginDrain(); assert.equal(registry.stats().entries, 0, 'drain drops claimed and running entries alike'); assert.equal(registry.stats().evict.drain, 2); assert.equal(admit(registry, clock, { key: 'c', callerKey: 'session:c' }).refused, 'draining'); assert.equal(registry.stats().refuse.draining, 1); registry.close(); assert.equal(registry.stats().entries, 0); }); it('R9 counters are one funnel and stay internally consistent', () => { const { registry, clock } = makeRegistry({ maxEntries: 2, maxPerCaller: 10, maxEntryBytes: 4096 }); admit(registry, clock, { key: 'a', callerKey: 'session:a' }); // admit admit(registry, clock, { key: 'b', callerKey: 'session:b' }); // admit registry.lookup('missing', 'session:a'); // miss const claim = registry.claim('a', 'session:a'); // ok registry.claim('a', 'session:a'); // busy registry.release('a', claim.token, { state: 'served' }); // serve registry.admit({ key: 'big', callerKey: 'session:a', snapshot: snapshot('x'.repeat(2048)), pin: pin(clock.now()) }); // over_bytes const stats = registry.stats(); assert.equal(stats.admit, 2); assert.equal(stats.refuse.over_bytes, 1); assert.equal(stats.claim.ok, 1); assert.equal(stats.claim.busy, 1); assert.equal(stats.serve, 1); assert.equal(stats.lookup.miss, 1); assert.equal(stats.entries, 1); assert.equal(stats.bytes, registry.totalBytes); assert.equal( stats.entries, stats.admit - stats.serve - stats.re_detach - Object.values(stats.evict).reduce((a, b) => a + b, 0), 'entries must equal admits minus every deletion', ); }); it('re-detach keeps the original TTL (a re-detach buys no extra time)', () => { const { registry, clock } = makeRegistry({ ttlMs: 120_000 }); registry.admit({ key: 'k1', callerKey: 'session:a', snapshot: snapshot(), pin: pin(clock.now(), { deadline: clock.now() + 300_000 }), }); clock.advance(60_000); const claim = registry.claim('k1', 'session:a'); assert.equal(registry.release('k1', claim.token, { state: 'running', snapshot: snapshot('grown') }), true); clock.advance(60_001); assert.equal(registry.sweepExpired(), 1); assert.equal(registry.claim('k1', 'session:a').refused, 'gone'); }); it('evictForAccount drops only entries pinned to that account', () => { const { registry, clock } = makeRegistry(); registry.admit({ key: 'a', callerKey: 'session:a', snapshot: snapshot(), pin: pin(clock.now(), { accountId: 'acct-1' }) }); registry.admit({ key: 'b', callerKey: 'session:b', snapshot: snapshot(), pin: pin(clock.now(), { accountId: 'acct-2' }) }); assert.equal(registry.evictForAccount('acct-1'), 1); assert.equal(registry.lookup('a', 'session:a'), null); assert.ok(registry.lookup('b', 'session:b')); assert.equal(registry.stats().evict.account_removed, 1); }); it('refuses a snapshot without the v2 consumed pointer', () => { const { registry, clock } = makeRegistry(); const bad = snapshot(); delete bad.consumedThrough; assert.equal( registry.admit({ key: 'bad', callerKey: 'session:a', snapshot: bad, pin: pin(clock.now()) }).refused, 'bad_snapshot', ); assert.equal(registry.stats().refuse.bad_snapshot, 1); }); it('refuses a non-client-gone cause and a missing deadline', () => { const { registry, clock } = makeRegistry(); assert.equal( registry.admit({ key: 'a', callerKey: 'session:a', snapshot: snapshot(), pin: pin(clock.now()), cause: 'shutdown' }).refused, 'not_client_gone', ); assert.equal( registry.admit({ key: 'a', callerKey: 'session:a', snapshot: snapshot(), pin: { ...pin(clock.now()), deadline: null } }).refused, 'no_deadline', ); }); }); // G14 — the account-gone cleanups auth.js fires (conversation-pool // invalidateFor, detached-registry evictForAccount, user-JWT epoch bump) are // fire-and-forget promises. A rejection used to die in `.catch(()=>{})`: // a failed invalidateFor left stale pool entries resuming onto a dead // account and a failed evictForAccount left a resumable detached pin, with // zero signal anywhere. A failed cleanup must now be observable — counted // (account_cleanup_failed) and logged — while the caller still never waits. describe('G14 — auth cleanup rejections are observable (metric/log)', () => { const createdIds = []; function mkAccount() { const a = auth.addAccountByKey(`sk-g14-${Date.now()}-${Math.random().toString(36).slice(2)}`, 'g14'); createdIds.push(a.id); return a; } function captureWarns() { const warnings = []; const orig = console.warn; console.warn = (...args) => { warnings.push(args.map(String).join(' ')); }; return { warnings, restore: () => { console.warn = orig; } }; } const settle = () => new Promise((r) => setImmediate(r)); afterEach(() => { auth.__setAccountCleanupDeps?.(null); while (createdIds.length) auth.removeAccount(createdIds.pop()); resetConnectMetrics(); }); it('a rejected evictForAccount inside removeAccount is logged and counted', async () => { // Inject through the real path: the module-level evictForAccount // delegates to this instance method, so the dynamic import, the .then // call and the rejection all run for real. const original = detachedRegistry.evictForAccount; detachedRegistry.evictForAccount = () => { throw new Error('detached evict broke'); }; const cap = captureWarns(); try { const a = mkAccount(); resetConnectMetrics(); assert.equal(auth.removeAccount(a.id), true, 'removal still succeeds — the caller never waits'); createdIds.pop(); await settle(); assert.match(cap.warnings.join('\n'), /evictForAccount.*detached evict broke/, 'the rejected cleanup must be logged, not swallowed'); assert.equal(getConnectMetrics().account_cleanup_failed, 1, 'the rejected cleanup must be counted, not silent'); } finally { detachedRegistry.evictForAccount = original; cap.restore(); } }); it('rejected invalidateFor / user-JWT epoch inside removeAccount are logged and counted', async () => { const cap = captureWarns(); try { auth.__setAccountCleanupDeps?.({ invalidateFor: () => Promise.reject(new Error('pool invalidate broke')), bumpUserJwtCacheEpoch: () => Promise.reject(new Error('jwt epoch broke')), }); const a = mkAccount(); resetConnectMetrics(); auth.removeAccount(a.id); createdIds.pop(); await settle(); const logged = cap.warnings.join('\n'); assert.match(logged, /invalidateFor.*pool invalidate broke/); assert.match(logged, /bumpUserJwtCacheEpoch.*jwt epoch broke/); assert.equal(getConnectMetrics().account_cleanup_failed, 2); } finally { cap.restore(); } }); it('a rejected invalidateFor inside reportBanSignal is logged and counted; the ban still lands', async () => { const cap = captureWarns(); try { const a = mkAccount(); auth.__setAccountCleanupDeps?.({ invalidateFor: () => Promise.reject(new Error('pool invalidate broke')), }); resetConnectMetrics(); auth.reportBanSignal(a.apiKey, 'Account suspended'); auth.reportBanSignal(a.apiKey, 'Invalid API key'); await settle(); assert.equal(auth.getAccountInternal(a.id).status, 'banned', 'a cleanup failure must not roll back the ban decision'); assert.match(cap.warnings.join('\n'), /invalidateFor.*pool invalidate broke/); assert.equal(getConnectMetrics().account_cleanup_failed, 1); } finally { cap.restore(); } }); it('a successful cleanup emits no failure signal', async () => { const cap = captureWarns(); try { const a = mkAccount(); resetConnectMetrics(); auth.removeAccount(a.id); createdIds.pop(); await settle(); assert.doesNotMatch(cap.warnings.join('\n'), /account cleanup .* failed/); assert.equal(getConnectMetrics().account_cleanup_failed, 0); } finally { cap.restore(); } }); });