#if os(macOS) import Foundation import SQLite3 import Testing @testable import CodexBarCore struct OpenCodeGoLocalUsageReaderTests { @Test func `reads local OpenCode Go history into usage windows`() throws { let env = try Self.makeEnvironment() defer { try? FileManager.default.removeItem(at: env.root) } try Self.writeAuth(to: env.authURL) try Self.createDatabase(at: env.databaseURL) try Self.insertMessage( databaseURL: env.databaseURL, createdMs: Self.ms("2026-03-06T11:00:00.000Z"), cost: 3.0) try Self.insertMessage( databaseURL: env.databaseURL, createdMs: Self.ms("2026-03-05T12:00:00.000Z"), cost: 6.0) try Self.insertMessage( databaseURL: env.databaseURL, createdMs: Self.ms("2026-02-25T07:53:16.000Z"), cost: 2.0) let reader = OpenCodeGoLocalUsageReader(authURL: env.authURL, databaseURL: env.databaseURL) let snapshot = try reader.fetch(now: Date(timeIntervalSince1970: 1_772_798_400)) #expect(snapshot.rollingUsagePercent == 25) #expect(snapshot.weeklyUsagePercent == 30) #expect(snapshot.monthlyUsagePercent == 18.3) #expect(snapshot.rollingResetInSec == 14400) #expect(snapshot.weeklyResetInSec == 216_000) #expect(snapshot.monthlyResetInSec == 1_626_796) } @Test func `auth without history falls through to web strategy`() throws { let env = try Self.makeEnvironment() defer { try? FileManager.default.removeItem(at: env.root) } try Self.writeAuth(to: env.authURL) let reader = OpenCodeGoLocalUsageReader(authURL: env.authURL, databaseURL: env.databaseURL) #expect(throws: OpenCodeGoLocalUsageError.historyUnavailable("database not found")) { _ = try reader.fetch(now: Date(timeIntervalSince1970: 1_772_798_400)) } } @Test func `auth with unreadable history falls through to web strategy`() throws { let env = try Self.makeEnvironment() defer { try? FileManager.default.removeItem(at: env.root) } try Self.writeAuth(to: env.authURL) var db: OpaquePointer? guard sqlite3_open(env.databaseURL.path, &db) == SQLITE_OK else { throw SQLiteTestError.open } sqlite3_close(db) let reader = OpenCodeGoLocalUsageReader(authURL: env.authURL, databaseURL: env.databaseURL) #expect(throws: OpenCodeGoLocalUsageError.self) { _ = try reader.fetch(now: Date(timeIntervalSince1970: 1_772_798_400)) } } @Test func `monthly window keeps original anchor after shorter month clamp`() throws { let env = try Self.makeEnvironment() defer { try? FileManager.default.removeItem(at: env.root) } try Self.writeAuth(to: env.authURL) try Self.createDatabase(at: env.databaseURL) try Self.insertMessage( databaseURL: env.databaseURL, createdMs: Self.ms("2026-01-31T00:00:00.000Z"), cost: 1.0) try Self.insertMessage( databaseURL: env.databaseURL, createdMs: Self.ms("2026-03-29T10:00:00.000Z"), cost: 6.0) let reader = OpenCodeGoLocalUsageReader(authURL: env.authURL, databaseURL: env.databaseURL) let now = Date(timeIntervalSince1970: TimeInterval(Self.ms("2026-03-29T12:00:00.000Z")) / 1000) let snapshot = try reader.fetch(now: now) #expect(snapshot.monthlyUsagePercent == 10) #expect(snapshot.monthlyResetInSec == 129_600) } @Test func `reads step finish parts when message only stores metadata`() throws { let env = try Self.makeEnvironment() defer { try? FileManager.default.removeItem(at: env.root) } try Self.writeAuth(to: env.authURL) try Self.createDatabase(at: env.databaseURL) let messageID = try Self.insertMessage( databaseURL: env.databaseURL, createdMs: Self.ms("2026-03-06T11:00:00.000Z"), cost: nil) try Self.insertStepFinishPart( databaseURL: env.databaseURL, messageID: messageID, createdMs: Self.ms("2026-03-06T11:00:00.000Z"), cost: 3.0) let reader = OpenCodeGoLocalUsageReader(authURL: env.authURL, databaseURL: env.databaseURL) let snapshot = try reader.fetch(now: Date(timeIntervalSince1970: 1_772_798_400)) #expect(snapshot.rollingUsagePercent == 25) #expect(snapshot.weeklyUsagePercent == 10) #expect(snapshot.monthlyUsagePercent == 5) } @Test func `does not double count step finish parts when message has cost`() throws { let env = try Self.makeEnvironment() defer { try? FileManager.default.removeItem(at: env.root) } try Self.writeAuth(to: env.authURL) try Self.createDatabase(at: env.databaseURL) let messageID = try Self.insertMessage( databaseURL: env.databaseURL, createdMs: Self.ms("2026-03-06T11:00:00.000Z"), cost: 3.0) try Self.insertStepFinishPart( databaseURL: env.databaseURL, messageID: messageID, createdMs: Self.ms("2026-03-06T11:00:00.000Z"), cost: 3.0) let reader = OpenCodeGoLocalUsageReader(authURL: env.authURL, databaseURL: env.databaseURL) let snapshot = try reader.fetch(now: Date(timeIntervalSince1970: 1_772_798_400)) #expect(snapshot.rollingUsagePercent == 25) #expect(snapshot.weeklyUsagePercent == 10) #expect(snapshot.monthlyUsagePercent == 5) } @Test func `missing auth and history is not detected`() throws { let env = try Self.makeEnvironment() defer { try? FileManager.default.removeItem(at: env.root) } let reader = OpenCodeGoLocalUsageReader(authURL: env.authURL, databaseURL: env.databaseURL) #expect(throws: OpenCodeGoLocalUsageError.notDetected) { _ = try reader.fetch(now: Date(timeIntervalSince1970: 1_772_798_400)) } } private static func makeEnvironment() throws -> (root: URL, authURL: URL, databaseURL: URL) { let root = FileManager.default.temporaryDirectory .appendingPathComponent("OpenCodeGoLocalUsageReaderTests-\(UUID().uuidString)", isDirectory: true) let directory = root .appendingPathComponent(".local", isDirectory: true) .appendingPathComponent("share", isDirectory: true) .appendingPathComponent("opencode", isDirectory: true) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) return ( root, directory.appendingPathComponent("auth.json", isDirectory: false), directory.appendingPathComponent("opencode.db", isDirectory: false)) } private static func writeAuth(to url: URL) throws { let data = Data(#"{"opencode-go":{"type":"api-key","key":"go-key"}}"#.utf8) try data.write(to: url) } private static func createDatabase(at url: URL) throws { var db: OpaquePointer? guard sqlite3_open(url.path, &db) == SQLITE_OK else { throw SQLiteTestError.open } defer { sqlite3_close(db) } try Self.exec( db: db, sql: """ CREATE TABLE message ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, data TEXT NOT NULL, time_created INTEGER, time_updated INTEGER ); CREATE TABLE part ( id TEXT PRIMARY KEY, message_id TEXT NOT NULL, session_id TEXT NOT NULL, data TEXT NOT NULL, time_created INTEGER, time_updated INTEGER ); """) } @discardableResult private static func insertMessage(databaseURL: URL, createdMs: Int64, cost: Double?) throws -> String { var db: OpaquePointer? guard sqlite3_open(databaseURL.path, &db) == SQLITE_OK else { throw SQLiteTestError.open } defer { sqlite3_close(db) } let messageID = UUID().uuidString var payload: [String: Any] = [ "providerID": "opencode-go", "role": "assistant", "time": ["created": createdMs], ] if let cost { payload["cost"] = cost } let data = try JSONSerialization.data(withJSONObject: payload) let json = String(data: data, encoding: .utf8) ?? "{}" var stmt: OpaquePointer? guard sqlite3_prepare_v2( db, "INSERT INTO message (id, session_id, data, time_created, time_updated) VALUES (?, ?, ?, ?, ?)", -1, &stmt, nil) == SQLITE_OK else { throw SQLiteTestError.prepare } defer { sqlite3_finalize(stmt) } let transient = unsafeBitCast(-1, to: sqlite3_destructor_type.self) sqlite3_bind_text(stmt, 1, messageID, -1, transient) sqlite3_bind_text(stmt, 2, "session-1", -1, transient) sqlite3_bind_text(stmt, 3, json, -1, transient) sqlite3_bind_int64(stmt, 4, createdMs) sqlite3_bind_int64(stmt, 5, createdMs) guard sqlite3_step(stmt) == SQLITE_DONE else { throw SQLiteTestError.step } return messageID } private static func insertStepFinishPart( databaseURL: URL, messageID: String, createdMs: Int64, cost: Double) throws { var db: OpaquePointer? guard sqlite3_open(databaseURL.path, &db) == SQLITE_OK else { throw SQLiteTestError.open } defer { sqlite3_close(db) } let payload: [String: Any] = [ "type": "step-finish", "cost": cost, "tokens": ["input": 1, "output": 1, "total": 2], ] let data = try JSONSerialization.data(withJSONObject: payload) let json = String(data: data, encoding: .utf8) ?? "{}" var stmt: OpaquePointer? guard sqlite3_prepare_v2( db, "INSERT INTO part (id, message_id, session_id, data, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)", -1, &stmt, nil) == SQLITE_OK else { throw SQLiteTestError.prepare } defer { sqlite3_finalize(stmt) } let transient = unsafeBitCast(-1, to: sqlite3_destructor_type.self) sqlite3_bind_text(stmt, 1, UUID().uuidString, -1, transient) sqlite3_bind_text(stmt, 2, messageID, -1, transient) sqlite3_bind_text(stmt, 3, "session-1", -1, transient) sqlite3_bind_text(stmt, 4, json, -1, transient) sqlite3_bind_int64(stmt, 5, createdMs) sqlite3_bind_int64(stmt, 6, createdMs) guard sqlite3_step(stmt) == SQLITE_DONE else { throw SQLiteTestError.step } } private static func exec(db: OpaquePointer?, sql: String) throws { var message: UnsafeMutablePointer? guard sqlite3_exec(db, sql, nil, nil, &message) == SQLITE_OK else { sqlite3_free(message) throw SQLiteTestError.exec } } private static func ms(_ iso: String) -> Int64 { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return Int64((formatter.date(from: iso)?.timeIntervalSince1970 ?? 0) * 1000) } private enum SQLiteTestError: Error { case open case prepare case step case exec } } #endif