|
| 1 | +import { describe, expect, it } from "bun:test"; |
| 2 | +import { Database } from "../../shared/sqlite"; |
| 3 | +import { |
| 4 | + computeProtectionWindow, |
| 5 | + getProtectionWindowForSession, |
| 6 | + type ProtectionWindowRow, |
| 7 | +} from "./protection-window"; |
| 8 | +import { TAG_SELECT_COLUMNS } from "./storage-tags"; |
| 9 | + |
| 10 | +type SeedRow = { tag: number; tokens: number | null; type?: string }; |
| 11 | +const generated = (count: number): SeedRow[] => |
| 12 | + Array.from({ length: count }, (_, i) => ({ |
| 13 | + tag: i + 1, |
| 14 | + tokens: i % 7 === 0 ? null : i % 5 === 0 ? 0 : 100, |
| 15 | + })); |
| 16 | + |
| 17 | +function compareWithReference(seeds: SeedRow[], floor: number) { |
| 18 | + const db = new Database(":memory:"); |
| 19 | + try { |
| 20 | + // Legacy chronology can contain duplicate tag numbers; the current migration |
| 21 | + // adds uniqueness, so use the reader's older supported row shape for tie coverage. |
| 22 | + db.exec(`CREATE TABLE tags ( |
| 23 | + id INTEGER PRIMARY KEY, session_id TEXT, message_id TEXT, type TEXT, |
| 24 | + status TEXT, drop_mode TEXT, tool_name TEXT, input_byte_size INTEGER DEFAULT 0, |
| 25 | + byte_size INTEGER, reasoning_byte_size INTEGER DEFAULT 0, |
| 26 | + tag_number INTEGER NOT NULL, caveman_depth INTEGER DEFAULT 0, |
| 27 | + tool_owner_message_id TEXT, token_count INTEGER |
| 28 | + ); CREATE INDEX tags_chronology ON tags(session_id, tag_number, id)`); |
| 29 | + const insert = db.prepare(`INSERT INTO tags |
| 30 | + (session_id, message_id, type, status, tag_number, token_count, byte_size, |
| 31 | + tool_name, tool_owner_message_id, drop_mode) |
| 32 | + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`); |
| 33 | + db.transaction(() => { |
| 34 | + seeds.forEach((row, i) => { |
| 35 | + insert.run( |
| 36 | + "paged", |
| 37 | + `message-${i}`, |
| 38 | + row.type ?? "tool", |
| 39 | + i % 2 === 0 ? "dropped" : "active", |
| 40 | + row.tag, |
| 41 | + row.tokens, |
| 42 | + i * 13, |
| 43 | + "read", |
| 44 | + `owner-${i}`, |
| 45 | + i % 2 === 0 ? "skeleton" : null, |
| 46 | + ); |
| 47 | + }); |
| 48 | + insert.run( |
| 49 | + "other-session", |
| 50 | + "other", |
| 51 | + "tool", |
| 52 | + "active", |
| 53 | + 999999, |
| 54 | + 99999, |
| 55 | + 1, |
| 56 | + "read", |
| 57 | + "other", |
| 58 | + null, |
| 59 | + ); |
| 60 | + })(); |
| 61 | + const allRows = db |
| 62 | + .prepare(`SELECT ${TAG_SELECT_COLUMNS} FROM tags |
| 63 | + WHERE session_id = ? AND type = 'tool' ORDER BY tag_number ASC, id ASC`) |
| 64 | + .all("paged") as ProtectionWindowRow[]; |
| 65 | + const expected = computeProtectionWindow(allRows, floor); |
| 66 | + const actual = getProtectionWindowForSession(db, "paged", floor); |
| 67 | + const { isProtected: expectedPredicate, ...expectedFields } = expected; |
| 68 | + const { isProtected: actualPredicate, ...actualFields } = actual; |
| 69 | + expect(actualFields).toEqual(expectedFields); |
| 70 | + for (const row of allRows) expect(actualPredicate(row)).toBe(expectedPredicate(row)); |
| 71 | + return actual; |
| 72 | + } finally { |
| 73 | + db.close(); |
| 74 | + } |
| 75 | +} |
| 76 | + |
| 77 | +describe("paged protection-window differential", () => { |
| 78 | + it("completes a duplicate tag group across the 256-row page boundary", () => { |
| 79 | + const seeds = generated(27); |
| 80 | + for (let i = 0; i < 300; i++) seeds.push({ tag: 28, tokens: 100 }); |
| 81 | + seeds.push({ tag: 29, tokens: 100 }, { tag: 30, tokens: 100 }); |
| 82 | + const result = compareWithReference(seeds, 100); |
| 83 | + expect(result.status.protectedCount).toBe(302); |
| 84 | + expect(result.cutoff).toBe(28); |
| 85 | + }); |
| 86 | + |
| 87 | + for (const count of [0, 1, 2, 256, 257, 20000]) { |
| 88 | + for (const floor of [0, 4000, count * 100 + 1]) { |
| 89 | + it(`matches all fields for ${count} rows at floor ${floor}`, () => { |
| 90 | + compareWithReference(generated(count), floor); |
| 91 | + }); |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + it("exhausts zero and NULL mass history with interleaved dropped rows", () => { |
| 96 | + compareWithReference( |
| 97 | + generated(600).map((row, i) => ({ ...row, tokens: i % 2 ? 0 : null })), |
| 98 | + 4000, |
| 99 | + ); |
| 100 | + }); |
| 101 | + |
| 102 | + it("retains complete ties with fewer than three distinct tool tags", () => { |
| 103 | + compareWithReference( |
| 104 | + generated(600).map((row, i) => ({ ...row, tag: i % 2 })), |
| 105 | + 0, |
| 106 | + ); |
| 107 | + }); |
| 108 | + |
| 109 | + it("ignores non-tool chronology and other sessions", () => { |
| 110 | + compareWithReference( |
| 111 | + generated(600).map((row, i) => ({ ...row, type: i % 3 ? "tool" : "message" })), |
| 112 | + 4000, |
| 113 | + ); |
| 114 | + }); |
| 115 | +}); |
0 commit comments