@@ -17,6 +17,9 @@ extension TokenMeterSelfTest {
1717 try syncFolderDeduplicatesAppendedDeviceLedgerRecords ( )
1818 try syncFolderCancellationSkipsPartialLocalLedgerWrite ( )
1919 try scannerMergesFreshWindowedSyncEventsWithCachedHistory ( )
20+ try scannerKeepsSyncedCodexUsageAfterLocalSessionFilesAreRemoved ( )
21+ try scannerKeepsMergedDeviceUsageAfterOneMacRemovesLocalSessionFiles ( )
22+ try scannerRebuildsMissingLocalSyncLedgerAfterCodexSessionsAreRemoved ( )
2023 try scannerRestoresMissingLocalSyncLedgerFromCachedLocalHistory ( )
2124 try scannerExcludesCachedSyncEventsWhenSyncFolderIsDisabled ( )
2225 try scannerPrunesCachedSyncEventsWhenDeviceLedgerDisappears ( )
@@ -219,6 +222,110 @@ extension TokenMeterSelfTest {
219222 try expect ( windowed. status. parseErrorCount == 0 , " sync import accepts plain ISO timestamps " )
220223 }
221224
225+ static func scannerKeepsSyncedCodexUsageAfterLocalSessionFilesAreRemoved( ) throws {
226+ let directory = FileManager . default. temporaryDirectory
227+ . appendingPathComponent ( UUID ( ) . uuidString, isDirectory: true )
228+ let home = directory. appendingPathComponent ( " home " , isDirectory: true )
229+ let syncFolder = directory. appendingPathComponent ( " sync " , isDirectory: true )
230+ try FileManager . default. createDirectory ( at: syncFolder, withIntermediateDirectories: true )
231+
232+ let device = TokenDeviceMetadata ( id: " mac-a " , name: " Mac A " )
233+ let scanner = TokenLogScanner ( homeDirectory: home, localDevice: device)
234+ let codexLog = try writeCodexLog (
235+ homeDirectory: home,
236+ fileName: " session-a.jsonl " ,
237+ timestamp: " 2026-01-01T00:00:00.000Z " ,
238+ cwd: " /tmp/codex-project-a " ,
239+ input: 10 ,
240+ output: 5
241+ )
242+
243+ let initial = scanner. scan ( syncFolder: syncFolder, replaceSyncLedger: true )
244+ try expect ( Aggregation . totalUsage ( events: initial. events) . total == 15 , " initial Codex sync total " )
245+ try expect ( initial. syncStatus. exportedEventCount == 1 , " initial Codex sync exports one event " )
246+
247+ try FileManager . default. removeItem ( at: codexLog)
248+ let removed = scanner. scan ( syncFolder: syncFolder)
249+ try expect ( Aggregation . totalUsage ( events: removed. events) . total == 15 , " synced Codex total survives removed local session file " )
250+ try expect ( removed. events. first? . rawFilePath. hasPrefix ( " sync:// " ) == true , " removed local session is restored from sync ledger " )
251+ }
252+
253+ static func scannerKeepsMergedDeviceUsageAfterOneMacRemovesLocalSessionFiles( ) throws {
254+ let directory = FileManager . default. temporaryDirectory
255+ . appendingPathComponent ( UUID ( ) . uuidString, isDirectory: true )
256+ let homeA = directory. appendingPathComponent ( " home-a " , isDirectory: true )
257+ let homeB = directory. appendingPathComponent ( " home-b " , isDirectory: true )
258+ let syncFolder = directory. appendingPathComponent ( " sync " , isDirectory: true )
259+ try FileManager . default. createDirectory ( at: syncFolder, withIntermediateDirectories: true )
260+
261+ let logA = try writeCodexLog (
262+ homeDirectory: homeA,
263+ fileName: " session-a.jsonl " ,
264+ timestamp: " 2026-01-01T00:00:00.000Z " ,
265+ cwd: " /tmp/codex-project-a " ,
266+ input: 10
267+ )
268+ try writeCodexLog (
269+ homeDirectory: homeB,
270+ fileName: " session-b.jsonl " ,
271+ timestamp: " 2026-01-01T00:01:00.000Z " ,
272+ cwd: " /tmp/codex-project-b " ,
273+ input: 20
274+ )
275+
276+ let deviceA = TokenDeviceMetadata ( id: " mac-a " , name: " Mac A " )
277+ let deviceB = TokenDeviceMetadata ( id: " mac-b " , name: " Mac B " )
278+ let scannerA = TokenLogScanner ( homeDirectory: homeA, localDevice: deviceA)
279+ let scannerB = TokenLogScanner ( homeDirectory: homeB, localDevice: deviceB)
280+
281+ let firstA = scannerA. scan ( syncFolder: syncFolder, replaceSyncLedger: true )
282+ try expect ( Aggregation . totalUsage ( events: firstA. events) . total == 10 , " first Mac exports local Codex total " )
283+ let firstB = scannerB. scan ( syncFolder: syncFolder, replaceSyncLedger: true )
284+ try expect ( Aggregation . totalUsage ( events: firstB. events) . total == 30 , " second Mac sees merged Codex total " )
285+
286+ try FileManager . default. removeItem ( at: logA)
287+ let mergedA = scannerA. scan ( syncFolder: syncFolder)
288+ try expect ( Aggregation . totalUsage ( events: mergedA. events) . total == 30 , " merged Codex total survives one Mac pruning local sessions " )
289+ try expect ( Set ( mergedA. events. map ( \. deviceId) ) == [ " mac-a " , " mac-b " ] , " merged Codex events keep both device ids " )
290+ }
291+
292+ static func scannerRebuildsMissingLocalSyncLedgerAfterCodexSessionsAreRemoved( ) throws {
293+ let directory = FileManager . default. temporaryDirectory
294+ . appendingPathComponent ( UUID ( ) . uuidString, isDirectory: true )
295+ let home = directory. appendingPathComponent ( " home " , isDirectory: true )
296+ let syncFolder = directory. appendingPathComponent ( " sync " , isDirectory: true )
297+ try FileManager . default. createDirectory ( at: syncFolder, withIntermediateDirectories: true )
298+
299+ let cache = try temporaryCache ( in: directory)
300+ let device = TokenDeviceMetadata ( id: " mac-a " , name: " Mac A " )
301+ let scanner = TokenLogScanner ( homeDirectory: home, localDevice: device, cacheStore: cache)
302+ let codexLog = try writeCodexLog (
303+ homeDirectory: home,
304+ fileName: " session-a.jsonl " ,
305+ timestamp: " 2026-01-01T00:00:00.000Z " ,
306+ cwd: " /tmp/codex-project-a " ,
307+ input: 10 ,
308+ output: 5
309+ )
310+
311+ let initial = scanner. scan ( syncFolder: syncFolder, replaceSyncLedger: true )
312+ try expect ( Aggregation . totalUsage ( events: initial. events) . total == 15 , " initial Codex cache warms local history " )
313+ try expect ( try syncLedgerLineCount ( syncFolder: syncFolder, deviceId: device. id) == 1 , " initial Codex local ledger exists " )
314+
315+ let ledgerURL = syncFolder
316+ . appendingPathComponent ( " devices " , isDirectory: true )
317+ . appendingPathComponent ( " \( device. id) .jsonl " )
318+ try FileManager . default. removeItem ( at: codexLog)
319+ try FileManager . default. removeItem ( at: ledgerURL)
320+
321+ let restored = scanner. scan ( syncFolder: syncFolder)
322+ try expect ( Aggregation . totalUsage ( events: restored. events) . total == 15 , " Codex cache survives local session removal " )
323+ try expect (
324+ try syncLedgerLineCount ( syncFolder: syncFolder, deviceId: device. id) == 1 ,
325+ " missing Codex local ledger is rebuilt from cached history "
326+ )
327+ }
328+
222329 static func syncFolderCountsMalformedCanonicalTimestampAsParseError( ) throws {
223330 let directory = FileManager . default. temporaryDirectory
224331 . appendingPathComponent ( UUID ( ) . uuidString, isDirectory: true )
0 commit comments