1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
import { beforeEach, describe, expect, it, vi } from "vitest";
interface ChunkRecord {
id: string;
tab_id: string;
seq: number;
turn_id: string;
step: number;
role: string;
type: string;
data_json: string;
created_at: number;
}
/**
* Minimal in-memory fake of bun:sqlite supporting only the queries
* `appendChunks`, `getChunksForTab`, and `rekeyChunks` issue. Mirrors the
* approach in chunks.db.test.ts (exact normalized-string branches).
*/
class FakeDatabase {
rows: ChunkRecord[] = [];
query(sql: string) {
return {
all: (params?: Record<string, unknown>) => this.execSelect(sql, params),
get: (params?: Record<string, unknown>) => this.execSelect(sql, params)[0] ?? null,
run: (params?: Record<string, unknown>) => this.execMutation(sql, params),
};
}
transaction(fn: () => void): () => void {
return () => fn();
}
private execSelect(sql: string, params?: Record<string, unknown>): unknown[] {
const norm = sql.replace(/\s+/g, " ").trim();
const tabId = params?.$tabId as string | undefined;
const forTab = this.rows.filter((r) => r.tab_id === tabId);
const visible = forTab.filter((r) => r.type !== "usage");
if (norm === "SELECT COALESCE(MAX(seq), -1) as max_seq FROM chunks WHERE tab_id = $tabId") {
const seqs = forTab.map((r) => r.seq);
return [{ max_seq: seqs.length > 0 ? Math.max(...seqs) : -1 }];
}
if (
norm === "SELECT * FROM chunks WHERE tab_id = $tabId AND type != 'usage' ORDER BY seq ASC"
) {
return [...visible].sort((a, b) => a.seq - b.seq);
}
throw new Error(`FakeDatabase: unsupported SELECT: ${norm}`);
}
private execMutation(sql: string, params?: Record<string, unknown>): { changes: number } {
const norm = sql.replace(/\s+/g, " ").trim();
if (
norm ===
"INSERT INTO chunks (id, tab_id, seq, turn_id, step, role, type, data_json, created_at) VALUES ($id, $tabId, $seq, $turnId, $step, $role, $type, $dataJson, $now)"
) {
this.rows.push({
id: params?.$id as string,
tab_id: params?.$tabId as string,
seq: params?.$seq as number,
turn_id: params?.$turnId as string,
step: (params?.$step as number) ?? 0,
role: params?.$role as string,
type: params?.$type as string,
data_json: params?.$dataJson as string,
created_at: (params?.$now as number) ?? 0,
});
return { changes: 1 };
}
if (norm === "UPDATE chunks SET tab_id = $to WHERE tab_id = $from") {
const from = params?.$from as string;
const to = params?.$to as string;
let changes = 0;
for (const r of this.rows) {
if (r.tab_id === from) {
r.tab_id = to;
changes++;
}
}
return { changes };
}
throw new Error(`FakeDatabase: unsupported mutation: ${norm}`);
}
}
let fakeDb: FakeDatabase;
vi.mock("../../src/db/index.js", () => ({ getDatabase: vi.fn(() => fakeDb) }));
const { appendChunks, getChunksForTab, rekeyChunks } = await import("../../src/db/chunks.js");
beforeEach(() => {
fakeDb = new FakeDatabase();
});
describe("rekeyChunks", () => {
it("relocates all rows from one tab to another and reports the count", () => {
appendChunks("src", [
{ turnId: "t1", step: 0, role: "user", type: "text", data: { text: "hi" } },
{ turnId: "t1", step: 0, role: "assistant", type: "text", data: { text: "yo" } },
]);
expect(getChunksForTab("src")).toHaveLength(2);
const moved = rekeyChunks("src", "backup");
expect(moved).toBe(2);
expect(getChunksForTab("src")).toHaveLength(0);
const dst = getChunksForTab("backup");
expect(dst).toHaveLength(2);
// turn id + seq preserved on the destination
expect(dst.map((r) => r.turnId)).toEqual(["t1", "t1"]);
expect(dst.map((r) => r.seq)).toEqual([0, 1]);
});
it("returns 0 when the source tab has no rows", () => {
expect(rekeyChunks("nope", "backup")).toBe(0);
});
it("does not touch unrelated tabs", () => {
appendChunks("src", [
{ turnId: "t1", step: 0, role: "user", type: "text", data: { text: "a" } },
]);
appendChunks("other", [
{ turnId: "t9", step: 0, role: "user", type: "text", data: { text: "b" } },
]);
rekeyChunks("src", "backup");
expect(getChunksForTab("other")).toHaveLength(1);
});
});
|