| ... | ... | @@ -7,6 +7,7 @@ import * as FileSystem from "effect/FileSystem"; |
| 7 | 7 | import * as Layer from "effect/Layer"; |
| 8 | 8 | import * as Path from "effect/Path"; |
| 9 | 9 | import * as SqlClient from "effect/sql/SqlClient"; |
| 10 | import { vi } from "vite-plus/test"; |
| 10 | 11 | |
| 11 | 12 | import { readOpenCodeUsage } from "./usage.ts"; |
| 12 | 13 | |
| ... | ... | @@ -17,6 +18,75 @@ const openDatabase = (filename: string) => |
| 17 | 18 | ); |
| 18 | 19 | |
| 19 | 20 | describe("readOpenCodeUsage", () => { |
| 21 | it.effect("bounds raw history allocations while counting every eligible message once", () => |
| 22 | Effect.gen(function* () { |
| 23 | const fileSystem = yield* FileSystem.FileSystem; |
| 24 | const path = yield* Path.Path; |
| 25 | const dir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "usage-reader-large-test-" }); |
| 26 | const db = yield* openDatabase(path.join(dir, "opencode.db")); |
| 27 | yield* db.unsafe( |
| 28 | "CREATE TABLE message (id TEXT, session_id TEXT, data TEXT, time_created INTEGER)", |
| 29 | ); |
| 30 | const sinceMs = 1780000000000; |
| 31 | const message = { |
| 32 | role: "assistant", |
| 33 | modelID: "claude-sonnet-4-5", |
| 34 | time: { created: sinceMs }, |
| 35 | tokens: { input: 100, output: 20 }, |
| 36 | // Transcript bodies are irrelevant to usage, but can be very large. |
| 37 | body: "x".repeat(64 * 1024), |
| 38 | }; |
| 39 | const data = JSON.stringify(message); |
| 40 | const insert = (rowId: number, id: string, payload: string, created = sinceMs) => |
| 41 | db.unsafe( |
| 42 | "INSERT INTO message (rowid, id, session_id, data, time_created) VALUES (?, ?, ?, ?, ?)", |
| 43 | [rowId, id, "session-1", payload, created], |
| 44 | ); |
| 45 | // Sparse IDs, including negative rowids, must not skip records at page boundaries. |
| 46 | for (let index = 0; index < 130; index++) { |
| 47 | yield* insert(index * 2 - 10, `msg-${index}`, data); |
| 48 | } |
| 49 | yield* insert(300, "old", data, sinceMs - 1); |
| 50 | yield* insert(301, "user", JSON.stringify({ ...message, role: "user" })); |
| 51 | yield* insert(302, "invalid", "{invalid"); |
| 52 | yield* insert(303, "msg-0", data); |
| 53 | |
| 54 | let largestRawResult = 0; |
| 55 | const originalAll = NodeSqlite.StatementSync.prototype.all; |
| 56 | yield* Effect.acquireRelease( |
| 57 | Effect.sync(() => |
| 58 | vi.spyOn(NodeSqlite.StatementSync.prototype, "all").mockImplementation(function ( |
| 59 | this: NodeSqlite.StatementSync, |
| 60 | ...params |
| 61 | ) { |
| 62 | const rows = originalAll.apply(this, params); |
| 63 | const rawSize = rows.reduce( |
| 64 | (size, row) => size + (typeof row.data === "string" ? row.data.length : 0), |
| 65 | 0, |
| 66 | ); |
| 67 | largestRawResult = Math.max(largestRawResult, rawSize); |
| 68 | return rows; |
| 69 | }), |
| 70 | ), |
| 71 | (spy) => Effect.sync(() => spy.mockRestore()), |
| 72 | ); |
| 73 | const result = yield* readOpenCodeUsage(dir, sinceMs); |
| 74 | assert.isFalse(result.error); |
| 75 | const records = result.files.flatMap((file) => file.records); |
| 76 | assert.deepStrictEqual( |
| 77 | records.map((record) => record.dedupeKey), |
| 78 | Array.from({ length: 130 }, (_, index) => `opencode:msg-${index}`), |
| 79 | ); |
| 80 | assert.strictEqual( |
| 81 | records.reduce((total, record) => total + record.totals.uncachedInputTokens, 0), |
| 82 | 13000, |
| 83 | ); |
| 84 | // The fixture exceeds 8 MiB. No query may retain its complete bodies. |
| 85 | assert.isAbove(largestRawResult, 0); |
| 86 | assert.isBelow(largestRawResult, 6 * 1024 * 1024); |
| 87 | }).pipe(Effect.provide(NodeServices.layer)), |
| 88 | ); |
| 89 | |
| 20 | 90 | it.effect("counts migrated OpenCode messages once and sees subsequent WAL writes", () => |
| 21 | 91 | Effect.gen(function* () { |
| 22 | 92 | const fileSystem = yield* FileSystem.FileSystem; |
| ... | ... | @@ -68,3 +138,4 @@ describe("readOpenCodeUsage", () => { |
| 68 | 138 | }).pipe(Effect.provide(NodeServices.layer)), |
| 69 | 139 | ); |
| 70 | 140 | }); |
| 141 | import * as NodeSqlite from "node:sqlite"; |