-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathtiering.test.ts
More file actions
210 lines (173 loc) · 8.16 KB
/
Copy pathtiering.test.ts
File metadata and controls
210 lines (173 loc) · 8.16 KB
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/**
* Tests for L0/L1/L2 content tiering (v0.8.0)
* tiering.test.ts
*
* Tests:
* - indexContent stores l0_summary and l1_summary in source_meta
* - L0 is <= TIER_L0_CHARS chars
* - L1 is <= TIER_L1_CHARS chars
* - getContentAtDepth returns l0/l1/full correctly
* - explainRetrieval returns scoring breakdown
*/
import { describe, it, expect } from "vitest";
import { mkdtempSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { createHash } from "node:crypto";
const TEST_DB_DIR = mkdtempSync(join(tmpdir(), "zc-tiering-test-"));
process.env["ZC_TEST_DB_DIR"] = TEST_DB_DIR;
import { indexContent, searchKnowledge, getContentAtDepth, explainRetrieval } from "./knowledge.js";
import { Config } from "./config.js";
import { runMigrations } from "./migrations.js";
function makeProjectPath(suffix: string): string {
return join(TEST_DB_DIR, `tier-${suffix}`);
}
function openProjectDb(projectPath: string): DatabaseSync {
mkdirSync(Config.DB_DIR, { recursive: true });
const hash = createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
const dbFile = join(Config.DB_DIR, `${hash}.db`);
const db = new DatabaseSync(dbFile);
db.exec("PRAGMA journal_mode = WAL");
db.exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS knowledge USING fts5(source, content, created_at UNINDEXED, tokenize='porter unicode61');
CREATE TABLE IF NOT EXISTS project_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS working_memory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL, value TEXT NOT NULL,
importance INTEGER NOT NULL DEFAULT 3,
agent_id TEXT NOT NULL DEFAULT 'default',
created_at TEXT NOT NULL,
UNIQUE(key, agent_id)
);
`);
runMigrations(db);
return db;
}
// ── L0/L1 summaries stored in source_meta ────────────────────────────────────
describe("indexContent — L0/L1 tier storage", () => {
it("stores l0_summary and l1_summary in source_meta", () => {
const projectPath = makeProjectPath(`store-l0l1-${Date.now()}`);
const content = "A".repeat(2000);
indexContent(projectPath, content, "test-source-tiers");
const db = openProjectDb(projectPath);
type Row = { l0_summary: string; l1_summary: string };
const row = db.prepare(
"SELECT l0_summary, l1_summary FROM source_meta WHERE source = 'test-source-tiers'"
).get() as Row | undefined;
db.close();
expect(row).toBeDefined();
expect(row!.l0_summary.length).toBeGreaterThan(0);
expect(row!.l1_summary.length).toBeGreaterThan(0);
});
it("L0 is <= TIER_L0_CHARS characters", () => {
const projectPath = makeProjectPath(`l0-len-${Date.now()}`);
const content = "B".repeat(2000);
indexContent(projectPath, content, "test-l0-length");
const db = openProjectDb(projectPath);
type Row = { l0_summary: string };
const row = db.prepare("SELECT l0_summary FROM source_meta WHERE source = 'test-l0-length'").get() as Row | undefined;
db.close();
expect(row).toBeDefined();
expect(row!.l0_summary.length).toBeLessThanOrEqual(Config.TIER_L0_CHARS);
});
it("L1 is <= TIER_L1_CHARS characters", () => {
const projectPath = makeProjectPath(`l1-len-${Date.now()}`);
const content = "C".repeat(5000);
indexContent(projectPath, content, "test-l1-length");
const db = openProjectDb(projectPath);
type Row = { l1_summary: string };
const row = db.prepare("SELECT l1_summary FROM source_meta WHERE source = 'test-l1-length'").get() as Row | undefined;
db.close();
expect(row).toBeDefined();
expect(row!.l1_summary.length).toBeLessThanOrEqual(Config.TIER_L1_CHARS);
});
it("content shorter than L0_CHARS stores full content as l0_summary", () => {
const projectPath = makeProjectPath(`l0-short-${Date.now()}`);
const content = "Short content here.";
indexContent(projectPath, content, "short-content-source");
const db = openProjectDb(projectPath);
type Row = { l0_summary: string };
const row = db.prepare("SELECT l0_summary FROM source_meta WHERE source = 'short-content-source'").get() as Row | undefined;
db.close();
expect(row).toBeDefined();
expect(row!.l0_summary).toBe(content.trim());
});
});
// ── getContentAtDepth ─────────────────────────────────────────────────────────
describe("getContentAtDepth", () => {
const fullContent = "D".repeat(5000);
const l0 = "L0 summary here.";
const l1 = "L1 longer summary with more planning detail.";
it("L0 returns l0 string", () => {
const result = getContentAtDepth(fullContent, l0, l1, "L0");
expect(result).toBe(l0);
});
it("L1 returns l1 string", () => {
const result = getContentAtDepth(fullContent, l0, l1, "L1");
expect(result).toBe(l1);
});
it("L2 returns full content", () => {
const result = getContentAtDepth(fullContent, l0, l1, "L2");
expect(result).toBe(fullContent);
});
it("L0 with empty l0 falls back to first TIER_L0_CHARS of content", () => {
const result = getContentAtDepth(fullContent, "", l1, "L0");
expect(result).toBe(fullContent.slice(0, Config.TIER_L0_CHARS));
});
it("L1 with empty l1 falls back to first TIER_L1_CHARS of content", () => {
const result = getContentAtDepth(fullContent, l0, "", "L1");
expect(result).toBe(fullContent.slice(0, Config.TIER_L1_CHARS));
});
});
// ── explainRetrieval ──────────────────────────────────────────────────────────
describe("explainRetrieval", () => {
it("returns empty results array when nothing is indexed", async () => {
const projectPath = makeProjectPath(`explain-empty-${Date.now()}`);
const result = await explainRetrieval(projectPath, "machine learning", "L2");
expect(result.query).toBe("machine learning");
expect(result.depth).toBe("L2");
expect(result.results).toHaveLength(0);
});
it("returns bm25Score, hybridScore, contentLength for indexed content", async () => {
const projectPath = makeProjectPath(`explain-scored-${Date.now()}`);
const content = "The quick brown fox jumps over the lazy dog. Relevant content about foxes.";
indexContent(projectPath, content, "fox-source");
// Wait a moment for sync write to land
await new Promise((r) => setTimeout(r, 50));
const result = await explainRetrieval(projectPath, "fox", "L2");
if (result.results.length > 0) {
const r = result.results[0]!;
expect(typeof r.bm25Score).toBe("number");
expect(typeof r.bm25Normalized).toBe("number");
expect(typeof r.hybridScore).toBe("number");
expect(r.contentLength).toBeGreaterThan(0);
expect(r.source).toBe("fox-source");
expect(typeof r.sourceType).toBe("string");
}
// Even if BM25-only (no Ollama), bm25Only field should be boolean
expect(typeof result.bm25Only).toBe("boolean");
});
it("vectorScore is null when Ollama not available (BM25 only)", async () => {
const projectPath = makeProjectPath(`explain-bm25only-${Date.now()}`);
indexContent(projectPath, "testing bm25 only retrieval mode", "bm25-source");
await new Promise((r) => setTimeout(r, 50));
const result = await explainRetrieval(projectPath, "bm25", "L0");
// In test environment, Ollama is likely not running
if (result.bm25Only) {
for (const r of result.results) {
expect(r.vectorScore).toBeNull();
}
}
});
it("tieredContent respects requested depth", async () => {
const projectPath = makeProjectPath(`explain-depth-${Date.now()}`);
const longContent = "Tiered content test. " + "X".repeat(2000);
indexContent(projectPath, longContent, "tiered-source");
await new Promise((r) => setTimeout(r, 50));
const l0Result = await explainRetrieval(projectPath, "tiered", "L0");
if (l0Result.results.length > 0) {
expect(l0Result.results[0]!.tieredContent.length).toBeLessThanOrEqual(Config.TIER_L0_CHARS + 1);
}
});
});