pi-l1-cache 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -104
- package/fix-l1-cache.cjs +405 -0
- package/package.json +18 -7
- package/src/index.test.ts +462 -84
- package/src/index.ts +269 -214
package/src/index.test.ts
CHANGED
|
@@ -1,136 +1,514 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// L1 Cache Extension — Test Suite
|
|
2
|
+
//
|
|
3
|
+
// Tests for the working implementation with replay, disk persistence,
|
|
4
|
+
// and proper key semantics.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
import { describe, it, beforeEach, afterEach } from "node:test"
|
|
7
|
+
import * as assert from "node:assert"
|
|
8
|
+
import fs from "node:fs"
|
|
9
|
+
import path from "node:path"
|
|
10
|
+
import os from "node:os"
|
|
11
|
+
import { fileURLToPath } from "node:url"
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
|
|
15
|
+
import l1CacheExtension from "./index.js"
|
|
16
|
+
import { cache, stats, settings, fastHash, estimateSize, coalesceChunks, evictIfNeeded, cleanupExpired, keyForPayload, persistEntry, loadPersisted, resetForTests } from "./index.js"
|
|
17
|
+
|
|
18
|
+
// Test cache directory (isolated from real cache)
|
|
19
|
+
const TEST_CACHE_DIR = path.join(os.tmpdir(), "l1-cache-test-" + Date.now())
|
|
20
|
+
|
|
21
|
+
// Mock ExtensionAPI
|
|
22
|
+
function createMockExtensionAPI(): any {
|
|
23
|
+
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any>>>()
|
|
24
|
+
const commands = new Map<string, any>()
|
|
25
|
+
let sessionShutdownHandler: ((event: any, ctx?: any) => Promise<any>) | null = null
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
on: (event: string, handler: (event: any, ctx?: any) => Promise<any>) => {
|
|
29
|
+
if (event === "session_shutdown") {
|
|
30
|
+
sessionShutdownHandler = handler
|
|
31
|
+
} else {
|
|
32
|
+
if (!handlers.has(event)) handlers.set(event, [])
|
|
33
|
+
handlers.get(event)!.push(handler)
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
registerCommand: (name: string, config: any) => {
|
|
37
|
+
commands.set(name, config)
|
|
38
|
+
},
|
|
39
|
+
emit: async (event: any) => {
|
|
40
|
+
const eventHandlers = handlers.get(event.type) || []
|
|
41
|
+
for (const handler of eventHandlers) {
|
|
42
|
+
await handler(event, {})
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
_getHandlers: (event: string) => handlers.get(event) || [],
|
|
46
|
+
_getSessionShutdownHandler: () => sessionShutdownHandler,
|
|
47
|
+
_getCommands: () => commands,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("L1 Cache", () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
// Clean test cache directory
|
|
54
|
+
if (fs.existsSync(TEST_CACHE_DIR)) {
|
|
55
|
+
fs.rmSync(TEST_CACHE_DIR, { recursive: true, force: true })
|
|
56
|
+
}
|
|
57
|
+
fs.mkdirSync(TEST_CACHE_DIR, { recursive: true })
|
|
58
|
+
// Isolate: redirect persistence + reset all module state
|
|
59
|
+
process.env.L1_CACHE_DIR = TEST_CACHE_DIR
|
|
60
|
+
resetForTests()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
// Cleanup test cache directory
|
|
65
|
+
if (fs.existsSync(TEST_CACHE_DIR)) {
|
|
66
|
+
fs.rmSync(TEST_CACHE_DIR, { recursive: true, force: true })
|
|
67
|
+
}
|
|
68
|
+
})
|
|
6
69
|
|
|
7
|
-
describe("pi-l1-cache", () => {
|
|
8
70
|
describe("fastHash", () => {
|
|
9
|
-
it("
|
|
10
|
-
const
|
|
11
|
-
|
|
71
|
+
it("produces consistent hashes for identical strings", () => {
|
|
72
|
+
const str = "test string"
|
|
73
|
+
const h1 = fastHash(str)
|
|
74
|
+
const h2 = fastHash(str)
|
|
75
|
+
assert.strictEqual(h1, h2)
|
|
12
76
|
})
|
|
13
77
|
|
|
14
|
-
it("
|
|
15
|
-
|
|
78
|
+
it("produces different hashes for different strings", () => {
|
|
79
|
+
const h1 = fastHash("string1")
|
|
80
|
+
const h2 = fastHash("string2")
|
|
81
|
+
assert.notStrictEqual(h1, h2)
|
|
16
82
|
})
|
|
17
83
|
|
|
18
84
|
it("handles empty string", () => {
|
|
19
|
-
const
|
|
20
|
-
assert.ok(
|
|
85
|
+
const hash = fastHash("")
|
|
86
|
+
assert.ok(hash.length > 0)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("handles unicode characters", () => {
|
|
90
|
+
const hash = fastHash("🚀 test")
|
|
91
|
+
assert.ok(hash.length > 0)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe("keyForPayload", () => {
|
|
96
|
+
it("creates key from model and messages", () => {
|
|
97
|
+
const payload = {
|
|
98
|
+
model: "local/deepseek-v4",
|
|
99
|
+
messages: [{ role: "user", content: "test" }],
|
|
100
|
+
}
|
|
101
|
+
const key = keyForPayload(payload)
|
|
102
|
+
assert.ok(key)
|
|
103
|
+
assert.ok(key.length > 0)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("excludes volatile fields from key", () => {
|
|
107
|
+
const payload1 = {
|
|
108
|
+
model: "local/deepseek-v4",
|
|
109
|
+
messages: [{ role: "user", content: "test" }],
|
|
110
|
+
prompt_cache_key: "session-123",
|
|
111
|
+
stream: true,
|
|
112
|
+
}
|
|
113
|
+
const payload2 = {
|
|
114
|
+
model: "local/deepseek-v4",
|
|
115
|
+
messages: [{ role: "user", content: "test" }],
|
|
116
|
+
prompt_cache_key: "session-456", // different
|
|
117
|
+
stream: false, // different
|
|
118
|
+
}
|
|
119
|
+
const key1 = keyForPayload(payload1)
|
|
120
|
+
const key2 = keyForPayload(payload2)
|
|
121
|
+
assert.strictEqual(key1, key2) // keys should match despite volatile differences
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("includes tools in key", () => {
|
|
125
|
+
const payload1 = {
|
|
126
|
+
model: "local/deepseek-v4",
|
|
127
|
+
messages: [{ role: "user", content: "test" }],
|
|
128
|
+
tools: [{ name: "tool1" }],
|
|
129
|
+
}
|
|
130
|
+
const payload2 = {
|
|
131
|
+
model: "local/deepseek-v4",
|
|
132
|
+
messages: [{ role: "user", content: "test" }],
|
|
133
|
+
tools: [{ name: "tool2" }], // different
|
|
134
|
+
}
|
|
135
|
+
const key1 = keyForPayload(payload1)
|
|
136
|
+
const key2 = keyForPayload(payload2)
|
|
137
|
+
assert.notStrictEqual(key1, key2)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it("returns null for invalid payloads", () => {
|
|
141
|
+
assert.strictEqual(keyForPayload(null), null)
|
|
142
|
+
assert.strictEqual(keyForPayload(undefined), null)
|
|
143
|
+
assert.strictEqual(keyForPayload({}), null)
|
|
144
|
+
assert.strictEqual(keyForPayload({ messages: "not-array" }), null)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe("coalesceChunks", () => {
|
|
149
|
+
it("merges adjacent content deltas", () => {
|
|
150
|
+
const chunks = [
|
|
151
|
+
{ choices: [{ delta: { content: "hello" } }] },
|
|
152
|
+
{ choices: [{ delta: { content: " " } }] },
|
|
153
|
+
{ choices: [{ delta: { content: "world" } }] },
|
|
154
|
+
]
|
|
155
|
+
const coalesced = coalesceChunks(chunks)
|
|
156
|
+
assert.strictEqual(coalesced.length, 1)
|
|
157
|
+
assert.strictEqual(coalesced[0].choices[0].delta.content, "hello world")
|
|
21
158
|
})
|
|
22
159
|
|
|
23
|
-
it("
|
|
24
|
-
const
|
|
25
|
-
|
|
160
|
+
it("merges adjacent reasoning_content deltas", () => {
|
|
161
|
+
const chunks = [
|
|
162
|
+
{ choices: [{ delta: { reasoning_content: "thinking" } }] },
|
|
163
|
+
{ choices: [{ delta: { reasoning_content: " more" } }] },
|
|
164
|
+
]
|
|
165
|
+
const coalesced = coalesceChunks(chunks)
|
|
166
|
+
assert.strictEqual(coalesced.length, 1)
|
|
167
|
+
assert.strictEqual(coalesced[0].choices[0].delta.reasoning_content, "thinking more")
|
|
26
168
|
})
|
|
27
169
|
|
|
28
|
-
it("
|
|
29
|
-
const
|
|
30
|
-
|
|
170
|
+
it("does not merge chunks with finish_reason", () => {
|
|
171
|
+
const chunks = [
|
|
172
|
+
{ choices: [{ delta: { content: "hello" } }] },
|
|
173
|
+
{ choices: [{ delta: { content: "world" }, finish_reason: "stop" }] },
|
|
174
|
+
]
|
|
175
|
+
const coalesced = coalesceChunks(chunks)
|
|
176
|
+
assert.strictEqual(coalesced.length, 2)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it("does not merge chunks with tool_calls", () => {
|
|
180
|
+
const chunks = [
|
|
181
|
+
{ choices: [{ delta: { content: "hello" } }] },
|
|
182
|
+
{ choices: [{ delta: { tool_calls: [{ id: "call1" }] } }] },
|
|
183
|
+
]
|
|
184
|
+
const coalesced = coalesceChunks(chunks)
|
|
185
|
+
assert.strictEqual(coalesced.length, 2)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it("handles empty chunks array", () => {
|
|
189
|
+
const coalesced = coalesceChunks([])
|
|
190
|
+
assert.strictEqual(coalesced.length, 0)
|
|
31
191
|
})
|
|
32
192
|
})
|
|
33
193
|
|
|
34
194
|
describe("estimateSize", () => {
|
|
35
|
-
it("estimates size
|
|
36
|
-
const
|
|
195
|
+
it("estimates size for simple objects", () => {
|
|
196
|
+
const obj = { text: "hello" }
|
|
197
|
+
const size = estimateSize(obj)
|
|
37
198
|
assert.ok(size > 0)
|
|
38
|
-
assert.ok(size < 100)
|
|
39
199
|
})
|
|
40
200
|
|
|
41
|
-
it("
|
|
42
|
-
const obj = {
|
|
43
|
-
const size =
|
|
44
|
-
assert.ok(size >
|
|
201
|
+
it("handles large objects", () => {
|
|
202
|
+
const obj = { text: "x".repeat(10000) }
|
|
203
|
+
const size = estimateSize(obj)
|
|
204
|
+
assert.ok(size > 10000)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it("handles undefined", () => {
|
|
208
|
+
const size = estimateSize(undefined)
|
|
209
|
+
assert.strictEqual(size, 4096) // fallback for non-serializable
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe("cache operations", () => {
|
|
214
|
+
it("stores and retrieves entries", async () => {
|
|
215
|
+
const payload = {
|
|
216
|
+
model: "local/deepseek-v4",
|
|
217
|
+
messages: [{ role: "user", content: "test" }],
|
|
218
|
+
}
|
|
219
|
+
const key = keyForPayload(payload)!
|
|
220
|
+
const chunks = [{ choices: [{ delta: { content: "answer" } }] }]
|
|
221
|
+
const size = estimateSize(chunks)
|
|
222
|
+
|
|
223
|
+
cache.set(key, { chunks, timestamp: Date.now(), sizeBytes: size })
|
|
224
|
+
evictIfNeeded()
|
|
225
|
+
|
|
226
|
+
const entry = cache.get(key)
|
|
227
|
+
assert.ok(entry)
|
|
228
|
+
assert.strictEqual(entry!.chunks.length, 1)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it("evicts oldest entries when over memory cap", async () => {
|
|
232
|
+
settings.maxEntries = 2
|
|
233
|
+
settings.maxMemoryBytes = 100
|
|
234
|
+
const chunks = [{ choices: [{ delta: { content: "x".repeat(50) } }] }]
|
|
235
|
+
const size = estimateSize(chunks)
|
|
236
|
+
|
|
237
|
+
// Store 3 entries (should evict to stay within cap)
|
|
238
|
+
for (let i = 0; i < 3; i++) {
|
|
239
|
+
const payload = { model: "local/deepseek-v4", messages: [{ role: "user", content: `test${i}` }] }
|
|
240
|
+
const key = keyForPayload(payload)!
|
|
241
|
+
cache.set(key, { chunks, timestamp: Date.now() + i, sizeBytes: size })
|
|
242
|
+
evictIfNeeded()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
assert.ok(cache.size <= 2)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it("respects TTL expiry", async () => {
|
|
249
|
+
settings.ttlSeconds = 1
|
|
250
|
+
const payload = { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] }
|
|
251
|
+
const key = keyForPayload(payload)!
|
|
252
|
+
const chunks = [{ choices: [{ delta: { content: "answer" } }] }]
|
|
253
|
+
|
|
254
|
+
// Set timestamp in past (expired)
|
|
255
|
+
cache.set(key, { chunks, timestamp: Date.now() - 2000, sizeBytes: estimateSize(chunks) })
|
|
256
|
+
|
|
257
|
+
// Simulate cleanup
|
|
258
|
+
cleanupExpired()
|
|
259
|
+
|
|
260
|
+
assert.strictEqual(cache.has(key), false)
|
|
45
261
|
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
describe("persistence", () => {
|
|
265
|
+
it("persists entries to disk", async () => {
|
|
266
|
+
const payload = { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] }
|
|
267
|
+
const key = keyForPayload(payload)!
|
|
268
|
+
const chunks = [{ choices: [{ delta: { content: "answer" } }] }]
|
|
269
|
+
|
|
270
|
+
persistEntry(key, { chunks, timestamp: Date.now(), sizeBytes: estimateSize(chunks) })
|
|
46
271
|
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
272
|
+
// Verify file exists
|
|
273
|
+
const filePath = path.join(TEST_CACHE_DIR, key + ".json")
|
|
274
|
+
assert.ok(fs.existsSync(filePath))
|
|
275
|
+
|
|
276
|
+
// Load and verify
|
|
277
|
+
cache.clear()
|
|
278
|
+
loadPersisted()
|
|
279
|
+
assert.strictEqual(cache.size, 1)
|
|
51
280
|
})
|
|
52
281
|
|
|
53
|
-
it("
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
282
|
+
it("ignores expired entries on load", async () => {
|
|
283
|
+
settings.ttlSeconds = 1
|
|
284
|
+
const payload = { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] }
|
|
285
|
+
const key = keyForPayload(payload)!
|
|
286
|
+
|
|
287
|
+
// Persist with old timestamp
|
|
288
|
+
persistEntry(key, {
|
|
289
|
+
chunks: [{ choices: [{ delta: { content: "answer" } }] }],
|
|
290
|
+
timestamp: Date.now() - 2000,
|
|
291
|
+
sizeBytes: estimateSize([]),
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
cache.clear()
|
|
295
|
+
loadPersisted()
|
|
296
|
+
assert.strictEqual(cache.size, 0)
|
|
58
297
|
})
|
|
59
298
|
|
|
60
|
-
it("handles
|
|
61
|
-
|
|
62
|
-
|
|
299
|
+
it("handles corrupted files gracefully", async () => {
|
|
300
|
+
// Create corrupted file
|
|
301
|
+
fs.writeFileSync(path.join(TEST_CACHE_DIR, "corrupt.json"), "not valid json")
|
|
302
|
+
|
|
303
|
+
cache.clear()
|
|
304
|
+
loadPersisted()
|
|
305
|
+
// Should not crash, corrupted file should be deleted
|
|
306
|
+
assert.strictEqual(fs.existsSync(path.join(TEST_CACHE_DIR, "corrupt.json")), false)
|
|
63
307
|
})
|
|
64
308
|
})
|
|
65
309
|
|
|
66
|
-
describe("
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
310
|
+
describe("extension integration", () => {
|
|
311
|
+
it("initializes and registers commands", async () => {
|
|
312
|
+
const mockApi = createMockExtensionAPI()
|
|
313
|
+
await l1CacheExtension(mockApi)
|
|
314
|
+
|
|
315
|
+
const commands = mockApi._getCommands()
|
|
316
|
+
assert.ok(commands.has("l1-cache"))
|
|
70
317
|
})
|
|
71
318
|
|
|
72
|
-
it("
|
|
73
|
-
|
|
74
|
-
|
|
319
|
+
it("handles before_provider_request (miss)", async () => {
|
|
320
|
+
const mockApi = createMockExtensionAPI()
|
|
321
|
+
await l1CacheExtension(mockApi)
|
|
322
|
+
|
|
323
|
+
const handler = mockApi._getHandlers("before_provider_request")[0]
|
|
324
|
+
const event = {
|
|
325
|
+
payload: { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] },
|
|
75
326
|
}
|
|
76
|
-
|
|
77
|
-
|
|
327
|
+
|
|
328
|
+
const result = await handler(event, { model: { id: "local/deepseek-v4" } })
|
|
329
|
+
assert.strictEqual(result, undefined) // miss, no replay
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it("handles before_provider_request (hit)", async () => {
|
|
333
|
+
const mockApi = createMockExtensionAPI()
|
|
334
|
+
await l1CacheExtension(mockApi)
|
|
335
|
+
|
|
336
|
+
const handler = mockApi._getHandlers("before_provider_request")[0]
|
|
337
|
+
const payload = { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] }
|
|
338
|
+
const event = { payload }
|
|
339
|
+
|
|
340
|
+
// First call is miss
|
|
341
|
+
await handler(event, { model: { id: "local/deepseek-v4" } })
|
|
342
|
+
|
|
343
|
+
// Simulate a cache entry
|
|
344
|
+
const key = keyForPayload(payload)!
|
|
345
|
+
cache.set(key, {
|
|
346
|
+
chunks: [{ choices: [{ delta: { content: "cached" } }] }],
|
|
347
|
+
timestamp: Date.now(),
|
|
348
|
+
sizeBytes: 100,
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
// Second call should hit
|
|
352
|
+
const result = await handler(event, { model: { id: "local/deepseek-v4" } })
|
|
353
|
+
assert.ok(result)
|
|
354
|
+
assert.ok((result as any).__piL1Replay)
|
|
78
355
|
})
|
|
79
356
|
|
|
80
|
-
it("
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
357
|
+
it("handles provider_stream_complete", async () => {
|
|
358
|
+
const mockApi = createMockExtensionAPI()
|
|
359
|
+
await l1CacheExtension(mockApi)
|
|
360
|
+
|
|
361
|
+
const handler = mockApi._getHandlers("provider_stream_complete")[0]
|
|
362
|
+
const event = {
|
|
363
|
+
payload: { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] },
|
|
364
|
+
chunks: [{ choices: [{ delta: { content: "answer" } }] }],
|
|
85
365
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
366
|
+
|
|
367
|
+
await handler(event)
|
|
368
|
+
|
|
369
|
+
// Verify entry was stored
|
|
370
|
+
const key = keyForPayload(event.payload)!
|
|
371
|
+
const entry = cache.get(key)
|
|
372
|
+
assert.ok(entry)
|
|
373
|
+
assert.strictEqual(entry!.chunks.length, 1)
|
|
89
374
|
})
|
|
90
375
|
|
|
91
|
-
it("
|
|
92
|
-
|
|
93
|
-
|
|
376
|
+
it("handles /l1-cache clear command", async () => {
|
|
377
|
+
const mockApi = createMockExtensionAPI()
|
|
378
|
+
await l1CacheExtension(mockApi)
|
|
379
|
+
|
|
380
|
+
const command = mockApi._getCommands().get("l1-cache")
|
|
381
|
+
const mockCtx = {
|
|
382
|
+
ui: {
|
|
383
|
+
notify: () => {},
|
|
384
|
+
},
|
|
94
385
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
386
|
+
|
|
387
|
+
// Populate first so the assertion is meaningful
|
|
388
|
+
cache.set("k", { chunks: [{ choices: [{ delta: { content: "x" } }] }], timestamp: Date.now(), sizeBytes: 64 })
|
|
389
|
+
|
|
390
|
+
await command!.handler("clear", mockCtx)
|
|
391
|
+
|
|
392
|
+
assert.strictEqual(cache.size, 0)
|
|
98
393
|
})
|
|
99
394
|
|
|
100
|
-
it("
|
|
101
|
-
|
|
102
|
-
|
|
395
|
+
it("handles /l1-cache stats command", async () => {
|
|
396
|
+
const mockApi = createMockExtensionAPI()
|
|
397
|
+
await l1CacheExtension(mockApi)
|
|
398
|
+
|
|
399
|
+
const command = mockApi._getCommands().get("l1-cache")
|
|
400
|
+
const mockCtx = {
|
|
401
|
+
ui: {
|
|
402
|
+
notify: (msg: string) => {
|
|
403
|
+
assert.ok(msg.includes("L1 cache"))
|
|
404
|
+
},
|
|
405
|
+
},
|
|
103
406
|
}
|
|
104
|
-
|
|
105
|
-
|
|
407
|
+
|
|
408
|
+
await command!.handler("stats", mockCtx)
|
|
106
409
|
})
|
|
107
410
|
})
|
|
108
411
|
|
|
109
|
-
describe("
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
412
|
+
describe("edge cases", () => {
|
|
413
|
+
it("handles disabled cache", async () => {
|
|
414
|
+
settings.enabled = false
|
|
415
|
+
|
|
416
|
+
const mockApi = createMockExtensionAPI()
|
|
417
|
+
await l1CacheExtension(mockApi)
|
|
418
|
+
|
|
419
|
+
const handler = mockApi._getHandlers("before_provider_request")[0]
|
|
420
|
+
const event = {
|
|
421
|
+
payload: { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] },
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const result = await handler(event, { model: { id: "local/deepseek-v4" } })
|
|
425
|
+
assert.strictEqual(result, undefined)
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it("handles empty chunks (no storage)", async () => {
|
|
429
|
+
const mockApi = createMockExtensionAPI()
|
|
430
|
+
await l1CacheExtension(mockApi)
|
|
431
|
+
|
|
432
|
+
const handler = mockApi._getHandlers("provider_stream_complete")[0]
|
|
433
|
+
const event = {
|
|
434
|
+
payload: { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] },
|
|
435
|
+
chunks: [], // empty
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
await handler(event)
|
|
439
|
+
const key = keyForPayload(event.payload)!
|
|
440
|
+
assert.strictEqual(cache.has(key), false)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it("handles stale ctx gracefully", async () => {
|
|
444
|
+
const mockApi = createMockExtensionAPI()
|
|
445
|
+
await l1CacheExtension(mockApi)
|
|
446
|
+
|
|
447
|
+
const handler = mockApi._getHandlers("before_provider_request")[0]
|
|
448
|
+
const event = {
|
|
449
|
+
payload: { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] },
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Stale ctx (no model.id)
|
|
453
|
+
const result = await handler(event, { model: { id: undefined } })
|
|
454
|
+
assert.strictEqual(result, undefined)
|
|
113
455
|
})
|
|
114
456
|
|
|
115
|
-
it("
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
457
|
+
it("prevents re-storing replays", async () => {
|
|
458
|
+
const mockApi = createMockExtensionAPI()
|
|
459
|
+
await l1CacheExtension(mockApi)
|
|
460
|
+
|
|
461
|
+
const beforeHandler = mockApi._getHandlers("before_provider_request")[0]
|
|
462
|
+
const completeHandler = mockApi._getHandlers("provider_stream_complete")[0]
|
|
463
|
+
|
|
464
|
+
const payload = { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] }
|
|
465
|
+
const event = { payload }
|
|
466
|
+
|
|
467
|
+
// First call is miss
|
|
468
|
+
await beforeHandler(event, { model: { id: "local/deepseek-v4" } })
|
|
469
|
+
|
|
470
|
+
// Simulate cache hit
|
|
471
|
+
const key = keyForPayload(payload)!
|
|
472
|
+
cache.set(key, {
|
|
473
|
+
chunks: [{ choices: [{ delta: { content: "cached" } }] }],
|
|
474
|
+
timestamp: Date.now(),
|
|
475
|
+
sizeBytes: 100,
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
// Second call is hit
|
|
479
|
+
const result = await beforeHandler(event, { model: { id: "local/deepseek-v4" } })
|
|
480
|
+
assert.ok(result)
|
|
481
|
+
|
|
482
|
+
// Try to store (should be prevented by lastServed guard)
|
|
483
|
+
const chunksBefore = cache.get(key)!.chunks.length
|
|
484
|
+
await completeHandler({ payload, chunks: [{ choices: [{ delta: { content: "new" } }] }] })
|
|
485
|
+
const chunksAfter = cache.get(key)!.chunks.length
|
|
486
|
+
|
|
487
|
+
assert.strictEqual(chunksBefore, chunksAfter) // should not re-store
|
|
120
488
|
})
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
describe("performance", () => {
|
|
492
|
+
it("hash is fast (< 10µs per call)", () => {
|
|
493
|
+
const iterations = 1000
|
|
494
|
+
const start = Date.now()
|
|
121
495
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
496
|
+
for (let i = 0; i < iterations; i++) {
|
|
497
|
+
fastHash("test string " + i)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const elapsed = Date.now() - start
|
|
501
|
+
const avgPerCall = (elapsed * 1000) / iterations // in µs
|
|
502
|
+
assert.ok(avgPerCall < 10, `Hash average ${avgPerCall}µs, expected < 10µs`)
|
|
127
503
|
})
|
|
128
504
|
|
|
129
|
-
it("
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
505
|
+
it("coalescing reduces chunk count", () => {
|
|
506
|
+
const chunks = Array.from({ length: 100 }, (_, i) => ({
|
|
507
|
+
choices: [{ delta: { content: "x" } }],
|
|
508
|
+
}))
|
|
509
|
+
|
|
510
|
+
const coalesced = coalesceChunks(chunks)
|
|
511
|
+
assert.strictEqual(coalesced.length, 1) // all merged into one
|
|
134
512
|
})
|
|
135
513
|
})
|
|
136
514
|
})
|