@@ -7,6 +7,7 @@ import * as FileSystem from "effect/FileSystem";
77import * as Layer from "effect/Layer" ;
88import * as Path from "effect/Path" ;
99import * as SqlClient from "effect/sql/SqlClient" ;
10+ import { vi } from "vite-plus/test" ;
1011
1112import { readOpenCodeUsage } from "./usage.ts" ;
1213
@@ -17,6 +18,75 @@ const openDatabase = (filename: string) =>
1718 ) ;
1819
1920describe ( "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+
2090 it . effect ( "counts migrated OpenCode messages once and sees subsequent WAL writes" , ( ) =>
2191 Effect . gen ( function * ( ) {
2292 const fileSystem = yield * FileSystem . FileSystem ;
@@ -68,3 +138,4 @@ describe("readOpenCodeUsage", () => {
68138 } ) . pipe ( Effect . provide ( NodeServices . layer ) ) ,
69139 ) ;
70140} ) ;
141+ import * as NodeSqlite from "node:sqlite" ;
0 commit comments