693 lines
31 KiB
Swift
693 lines
31 KiB
Swift
import Foundation
|
|
#if canImport(SQLite3)
|
|
import SQLite3
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
struct CostUsageScannerCodexPriorityTests {
|
|
@Test
|
|
func `parses priority turn metadata without exposing request body`() {
|
|
let body = "INFO thread_id=11111111-1111-1111-1111-111111111111 "
|
|
+ "turn.id=22222222-2222-2222-2222-222222222222 websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority","#
|
|
+ #""instructions":"secret prompt"}"#
|
|
|
|
let parsed = CostUsageScanner.parseCodexPriorityTraceRow(timestamp: "2026-05-10T12:00:00Z", body: body)
|
|
|
|
#expect(parsed?.threadID == "11111111-1111-1111-1111-111111111111")
|
|
#expect(parsed?.turnID == "22222222-2222-2222-2222-222222222222")
|
|
#expect(parsed?.model == "request-model")
|
|
#expect(parsed?.timestamp == "2026-05-10T12:00:00Z")
|
|
}
|
|
|
|
@Test
|
|
func `ignores non priority malformed and non response request rows`() {
|
|
let prefix = "thread_id=thread turn.id=turn websocket request: "
|
|
|
|
#expect(CostUsageScanner.parseCodexPriorityTraceRow(
|
|
timestamp: nil,
|
|
body: prefix + #"{"type":"session.update","service_tier":"priority"}"#) == nil)
|
|
#expect(CostUsageScanner.parseCodexPriorityTraceRow(
|
|
timestamp: nil,
|
|
body: prefix + #"{"type":"response.create"}"#) == nil)
|
|
#expect(CostUsageScanner.parseCodexPriorityTraceRow(
|
|
timestamp: nil,
|
|
body: prefix + #"{"type":"response.create","service_tier":"default"}"#) == nil)
|
|
#expect(CostUsageScanner.parseCodexPriorityTraceRow(
|
|
timestamp: nil,
|
|
body: prefix + #"{"#) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `parses completed response model without exposing response body`() {
|
|
let body = "INFO thread_id=thread turn.id=turn websocket event: "
|
|
+ #"{"type":"response.completed","response":{"model":"completed-model","output":[{"content":"private"}]}}"#
|
|
|
|
let parsed = CostUsageScanner.parseCodexCompletedTraceRow(body: body)
|
|
|
|
#expect(parsed?.turnID == "turn")
|
|
#expect(parsed?.model == "completed-model")
|
|
}
|
|
|
|
@Test
|
|
func `reads priority turns from sqlite logs table`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:00Z",
|
|
body: "thread_id=thread-a turn.id=turn-a websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority","input":"private"}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:01:00Z",
|
|
body: """
|
|
thread_id=thread-b turn.id=turn-b websocket request: {"type":"response.create","model":"request-model"}
|
|
""")
|
|
|
|
let turns = CostUsageScanner.codexPriorityTurns(databaseURL: dbURL)
|
|
|
|
#expect(turns.keys.sorted() == ["turn-a"])
|
|
#expect(turns["turn-a"]?.threadID == "thread-a")
|
|
#expect(turns["turn-a"]?.model == "request-model")
|
|
}
|
|
|
|
@Test
|
|
func `cold scan uses timestamp index and warm scan uses rowid cursor`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
|
|
var db: OpaquePointer?
|
|
guard sqlite3_open_v2(dbURL.path, &db, SQLITE_OPEN_READONLY, nil) == SQLITE_OK else {
|
|
throw SQLiteTestError.open
|
|
}
|
|
defer { sqlite3_close(db) }
|
|
|
|
let coldQuery = CostUsageScanner._test_codexPriorityAccumulationQuery(
|
|
db,
|
|
lastRowID: 0,
|
|
coverageSinceEpoch: 1)
|
|
let coldPlan = try Self.queryPlan(db: db, query: coldQuery, bindings: [1])
|
|
#expect(coldPlan.contains { $0.contains("USING INDEX idx_logs_ts") })
|
|
|
|
let unboundedColdQuery = CostUsageScanner._test_codexPriorityAccumulationQuery(
|
|
db,
|
|
lastRowID: 0,
|
|
coverageSinceEpoch: 0)
|
|
let unboundedColdPlan = try Self.queryPlan(
|
|
db: db,
|
|
query: unboundedColdQuery,
|
|
bindings: [0, 0])
|
|
#expect(unboundedColdPlan.contains { $0.contains("USING INTEGER PRIMARY KEY") })
|
|
#expect(!unboundedColdPlan.contains { $0.contains("USE TEMP B-TREE") })
|
|
|
|
let warmQuery = CostUsageScanner._test_codexPriorityAccumulationQuery(
|
|
db,
|
|
lastRowID: 1,
|
|
coverageSinceEpoch: 0)
|
|
let warmPlan = try Self.queryPlan(db: db, query: warmQuery, bindings: [1, 0])
|
|
#expect(warmPlan.contains { $0.contains("USING INTEGER PRIMARY KEY") })
|
|
}
|
|
|
|
@Test
|
|
func `sqlite scan upgrades priority request alias with completed response model`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:00Z",
|
|
body: "thread_id=thread turn.id=turn websocket request: "
|
|
+ #"{"type":"response.create","model":"request-alias","service_tier":"priority"}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:01Z",
|
|
body: "thread_id=thread turn.id=turn websocket event: "
|
|
+ #"{"type":"response.completed","response":{"model":"completed-model","input":"private"}}"#)
|
|
|
|
let turns = CostUsageScanner.codexPriorityTurns(databaseURL: dbURL)
|
|
|
|
#expect(turns["turn"]?.model == "completed-model")
|
|
}
|
|
|
|
@Test
|
|
func `sqlite scan matches spaced completed response json`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:00Z",
|
|
body: "thread_id=thread turn.id=turn websocket request: "
|
|
+ #"{"type":"response.create","model":"request-alias","service_tier":"priority"}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:01Z",
|
|
body: "thread_id=thread turn.id=turn websocket event: "
|
|
+ #"{"type": "response.completed", "response": {"model": "completed-model"}}"#)
|
|
|
|
let turns = CostUsageScanner.codexPriorityTurns(databaseURL: dbURL)
|
|
|
|
#expect(turns["turn"]?.model == "completed-model")
|
|
}
|
|
|
|
@Test
|
|
func `sqlite scan only returns priority turns in requested day range`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
let previousDay = try #require(Calendar.current.date(byAdding: .day, value: -1, to: day))
|
|
let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: env.isoString(for: previousDay),
|
|
body: "thread_id=thread-old turn.id=turn-old websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: env.isoString(for: day),
|
|
body: "thread_id=thread-new turn.id=turn-new websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
|
|
let turns = CostUsageScanner.codexPriorityTurns(
|
|
databaseURL: dbURL,
|
|
sinceDayKey: dayKey,
|
|
untilDayKey: dayKey)
|
|
|
|
#expect(turns.keys.sorted() == ["turn-new"])
|
|
}
|
|
|
|
@Test
|
|
func `sqlite scan uses local day boundaries for integer timestamps`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
|
|
var components = DateComponents()
|
|
components.calendar = Calendar.current
|
|
components.year = 2026
|
|
components.month = 5
|
|
components.day = 10
|
|
let dayStart = try #require(components.date)
|
|
let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: dayStart)
|
|
let previousSecond = try #require(Calendar.current.date(byAdding: .second, value: -1, to: dayStart))
|
|
let nextSecond = try #require(Calendar.current.date(byAdding: .second, value: 1, to: dayStart))
|
|
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
epochSeconds: Int64(previousSecond.timeIntervalSince1970),
|
|
body: "thread_id=thread-before turn.id=turn-before websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
epochSeconds: Int64(nextSecond.timeIntervalSince1970),
|
|
body: "thread_id=thread-after turn.id=turn-after websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
|
|
let turns = CostUsageScanner.codexPriorityTurns(
|
|
databaseURL: dbURL,
|
|
sinceDayKey: dayKey,
|
|
untilDayKey: dayKey)
|
|
|
|
#expect(turns.keys.sorted() == ["turn-after"])
|
|
}
|
|
|
|
@Test
|
|
func `incremental memo picks up rows appended after the first query`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:00Z",
|
|
body: "thread_id=thread-a turn.id=turn-a websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
|
|
#expect(CostUsageScanner.codexPriorityTurns(databaseURL: dbURL).keys.sorted() == ["turn-a"])
|
|
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:05:00Z",
|
|
body: "thread_id=thread-b turn.id=turn-b websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:05:01Z",
|
|
body: "thread_id=thread-a turn.id=turn-a websocket event: "
|
|
+ #"{"type":"response.completed","response":{"model":"resolved-model"}}"#)
|
|
|
|
let merged = CostUsageScanner.codexPriorityTurns(databaseURL: dbURL)
|
|
#expect(merged.keys.sorted() == ["turn-a", "turn-b"])
|
|
// A completed event appended later still upgrades the model of a turn accumulated earlier.
|
|
#expect(merged["turn-a"]?.model == "resolved-model")
|
|
}
|
|
|
|
@Test
|
|
func `memo drops pruned requests while ids keep increasing`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:00Z",
|
|
body: "thread_id=thread-a turn.id=turn-a websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:01:00Z",
|
|
body: "thread_id=thread-b turn.id=turn-b websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
#expect(CostUsageScanner.codexPriorityTurns(databaseURL: dbURL).keys.sorted() == ["turn-a", "turn-b"])
|
|
|
|
try Self.execDatabase(dbURL: dbURL, sql: "delete from logs where rowid = 1")
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:02:00Z",
|
|
body: "thread_id=thread-c turn.id=turn-c websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
|
|
let rebuilt = CostUsageScanner.codexPriorityTurns(databaseURL: dbURL)
|
|
#expect(rebuilt.keys.sorted() == ["turn-b", "turn-c"])
|
|
}
|
|
|
|
@Test
|
|
func `memo drops a pruned completion model without losing its request`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:00Z",
|
|
body: "thread_id=thread-a turn.id=turn-a websocket request: "
|
|
+ #"{"type":"response.create","model":"request-alias","service_tier":"priority"}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:01Z",
|
|
body: "thread_id=thread-a turn.id=turn-a websocket event: "
|
|
+ #"{"type":"response.completed","response":{"model":"resolved-model"}}"#)
|
|
#expect(CostUsageScanner.codexPriorityTurns(databaseURL: dbURL)["turn-a"]?.model == "resolved-model")
|
|
|
|
try Self.execDatabase(dbURL: dbURL, sql: "delete from logs where rowid = 2")
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:01:00Z",
|
|
body: "thread_id=thread-b turn.id=turn-b websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
|
|
let pruned = CostUsageScanner.codexPriorityTurns(databaseURL: dbURL)
|
|
#expect(pruned["turn-a"]?.model == "request-alias")
|
|
#expect(pruned["turn-b"]?.model == "request-model")
|
|
}
|
|
|
|
@Test
|
|
func `memo falls back to retained duplicate request and completion rows`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:00Z",
|
|
body: "thread_id=thread-old turn.id=turn-a websocket request: "
|
|
+ #"{"type":"response.create","model":"request-old","service_tier":"priority"}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:01Z",
|
|
body: "thread_id=thread-new turn.id=turn-a websocket request: "
|
|
+ #"{"type":"response.create","model":"request-new","service_tier":"priority"}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:02Z",
|
|
body: "thread_id=thread-old turn.id=turn-a websocket event: "
|
|
+ #"{"type":"response.completed","response":{"model":"completed-old"}}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:03Z",
|
|
body: "thread_id=thread-new turn.id=turn-a websocket event: "
|
|
+ #"{"type":"response.completed","response":{"model":"completed-new"}}"#)
|
|
|
|
let initial = CostUsageScanner.codexPriorityTurns(databaseURL: dbURL)
|
|
#expect(initial["turn-a"]?.threadID == "thread-new")
|
|
#expect(initial["turn-a"]?.model == "completed-new")
|
|
|
|
try Self.execDatabase(dbURL: dbURL, sql: "delete from logs where rowid in (2, 4)")
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:01:00Z",
|
|
body: "thread_id=thread-b turn.id=turn-b websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
|
|
let pruned = CostUsageScanner.codexPriorityTurns(databaseURL: dbURL)
|
|
#expect(pruned["turn-a"]?.threadID == "thread-old")
|
|
#expect(pruned["turn-a"]?.model == "completed-old")
|
|
#expect(pruned["turn-b"]?.model == "request-model")
|
|
}
|
|
|
|
@Test
|
|
func `failed incremental scan does not report completion`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:00Z",
|
|
body: "thread_id=thread-a turn.id=turn-a websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
|
|
var db: OpaquePointer?
|
|
guard sqlite3_open_v2(dbURL.path, &db, SQLITE_OPEN_READONLY, nil) == SQLITE_OK else {
|
|
throw SQLiteTestError.open
|
|
}
|
|
defer { sqlite3_close(db) }
|
|
sqlite3_progress_handler(db, 1, { _ in 1 }, nil)
|
|
|
|
var state = CostUsageScanner.CodexPriorityTurnsMemoState(
|
|
observationID: 1,
|
|
coverageSinceEpoch: 0,
|
|
lastRowID: 0,
|
|
fileIdentity: nil,
|
|
turns: [:],
|
|
requestSourcesByTurnID: [:],
|
|
priorityCompletedModelsByTurnID: [:],
|
|
completedModelsByTurnID: [:],
|
|
completedTurnIDInsertionOrder: [],
|
|
completedTurnIDInsertionOrderStartIndex: 0)
|
|
|
|
#expect(!CostUsageScanner._test_accumulateCodexPriorityTurns(db, into: &state))
|
|
}
|
|
|
|
@Test
|
|
func `memo rescans when requested window expands earlier than accumulated coverage`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
// Live refreshes always query through today, which is the memoized path.
|
|
let today = Date()
|
|
let yesterday = try #require(Calendar.current.date(byAdding: .day, value: -1, to: today))
|
|
let todayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: today)
|
|
let yesterdayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: yesterday)
|
|
let formatter = ISO8601DateFormatter()
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: formatter.string(from: yesterday),
|
|
body: "thread_id=thread-old turn.id=turn-old websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: formatter.string(from: today),
|
|
body: "thread_id=thread-new turn.id=turn-new websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
|
|
let narrow = CostUsageScanner.codexPriorityTurns(
|
|
databaseURL: dbURL,
|
|
sinceDayKey: todayKey,
|
|
untilDayKey: todayKey)
|
|
#expect(narrow.keys.sorted() == ["turn-new"])
|
|
|
|
let expanded = CostUsageScanner.codexPriorityTurns(
|
|
databaseURL: dbURL,
|
|
sinceDayKey: yesterdayKey,
|
|
untilDayKey: todayKey)
|
|
#expect(expanded.keys.sorted() == ["turn-new", "turn-old"])
|
|
}
|
|
|
|
@Test
|
|
func `memo rescans when the database shrinks or is replaced`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:00Z",
|
|
body: "thread_id=thread-a turn.id=turn-a websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:01:00Z",
|
|
body: "thread_id=thread-b turn.id=turn-b websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
#expect(CostUsageScanner.codexPriorityTurns(databaseURL: dbURL).count == 2)
|
|
|
|
try FileManager.default.removeItem(at: dbURL)
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-11T09:00:00Z",
|
|
body: "thread_id=thread-c turn.id=turn-c websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
|
|
let replaced = CostUsageScanner.codexPriorityTurns(databaseURL: dbURL)
|
|
#expect(replaced.keys.sorted() == ["turn-c"])
|
|
}
|
|
|
|
@Test
|
|
func `database replacement during open is rejected`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
let oldURL = env.root.appendingPathComponent("logs-old.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
var replacementError: Error?
|
|
|
|
let opened = CostUsageScanner.openCodexPriorityDatabase(at: dbURL) {
|
|
do {
|
|
try FileManager.default.moveItem(at: dbURL, to: oldURL)
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
} catch {
|
|
replacementError = error
|
|
}
|
|
}
|
|
if let opened {
|
|
sqlite3_close(opened.db)
|
|
}
|
|
|
|
#expect(replacementError == nil)
|
|
#expect(opened == nil)
|
|
}
|
|
|
|
@Test
|
|
func `overlapping refresh writeback cannot replace newer memo state`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:00:00Z",
|
|
body: "thread_id=thread-a turn.id=turn-a websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
try Self.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: "2026-05-10T12:01:00Z",
|
|
body: "thread_id=thread-b turn.id=turn-b websocket request: "
|
|
+ #"{"type":"response.create","model":"request-model","service_tier":"priority"}"#)
|
|
#expect(CostUsageScanner.codexPriorityTurns(databaseURL: dbURL).count == 2)
|
|
let stored = try #require(CostUsageScanner._test_codexPriorityTurnsMemoState(forPath: dbURL.path))
|
|
|
|
// A slower overlapping refresh writes back a snapshot read before the second row was
|
|
// appended: an older cursor that only observed the first turn. It must not win.
|
|
var stale = stored
|
|
stale.lastRowID -= 1
|
|
stale.turns = stored.turns.filter { $0.key == "turn-a" }
|
|
CostUsageScanner.storeCodexPriorityTurnsMemoIfNewer(stale, forPath: dbURL.path)
|
|
|
|
let retained = try #require(CostUsageScanner._test_codexPriorityTurnsMemoState(forPath: dbURL.path))
|
|
#expect(retained.lastRowID == stored.lastRowID)
|
|
#expect(retained.turns.keys.sorted() == ["turn-a", "turn-b"])
|
|
|
|
// A snapshot with a newer cursor still replaces the stored state.
|
|
var newer = stored
|
|
newer.lastRowID += 1
|
|
CostUsageScanner.storeCodexPriorityTurnsMemoIfNewer(newer, forPath: dbURL.path)
|
|
#expect(
|
|
CostUsageScanner._test_codexPriorityTurnsMemoState(forPath: dbURL.path)?
|
|
.lastRowID == stored.lastRowID + 1)
|
|
|
|
// A full rescan that expanded coverage earlier than the stored window also replaces,
|
|
// even when its cursor is not ahead, so broader history is never discarded.
|
|
var broader = stored
|
|
broader.coverageSinceEpoch -= 1
|
|
CostUsageScanner.storeCodexPriorityTurnsMemoIfNewer(broader, forPath: dbURL.path)
|
|
#expect(
|
|
CostUsageScanner._test_codexPriorityTurnsMemoState(forPath: dbURL.path)?
|
|
.coverageSinceEpoch == broader.coverageSinceEpoch)
|
|
}
|
|
|
|
@Test
|
|
func `memo bounds retained completion metadata for non-priority turns`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
|
|
try Self.createTestLogsDatabase(at: dbURL)
|
|
let limit = CostUsageScanner.codexPriorityCompletedModelRetentionLimit
|
|
let overflow = limit + 8
|
|
let epoch = Self.epochSeconds("2026-05-10T12:00:00Z")
|
|
|
|
// A known priority turn keeps its resolved completion outside the bounded pending
|
|
// cache while thousands of unrelated completions flow through the process.
|
|
var rows = [
|
|
(
|
|
epochSeconds: epoch,
|
|
body: "thread_id=priority turn.id=priority websocket request: "
|
|
+ #"{"type":"response.create","model":"priority-alias","service_tier":"priority"}"#),
|
|
(
|
|
epochSeconds: epoch,
|
|
body: "thread_id=priority turn.id=priority websocket event: "
|
|
+ #"{"type":"response.completed","response":{"model":"resolved-model"}}"#),
|
|
]
|
|
rows.append(contentsOf: (0..<(limit + overflow)).map { index in
|
|
(
|
|
epochSeconds: epoch,
|
|
body: "thread_id=thread-\(index) turn.id=turn-\(index) websocket event: "
|
|
+ #"{"type":"response.completed","response":{"model":"completed-model"}}"#)
|
|
})
|
|
rows.append((
|
|
epochSeconds: epoch,
|
|
body: "thread_id=thread-0 turn.id=turn-0 websocket request: "
|
|
+ #"{"type":"response.create","model":"alias-evicted","service_tier":"priority"}"#))
|
|
let newest = limit + overflow - 1
|
|
rows.append((
|
|
epochSeconds: epoch,
|
|
body: "thread_id=thread-\(newest) turn.id=turn-\(newest) "
|
|
+ "websocket request: "
|
|
+ #"{"type":"response.create","model":"alias-retained","service_tier":"priority"}"#))
|
|
try Self.insertTestLogs(dbURL: dbURL, rows: rows)
|
|
|
|
let turns = CostUsageScanner.codexPriorityTurns(databaseURL: dbURL)
|
|
|
|
let memo = try #require(CostUsageScanner._test_codexPriorityTurnsMemoState(forPath: dbURL.path))
|
|
#expect(memo.completedModelsByTurnID.count == limit - 1)
|
|
#expect(
|
|
memo.completedTurnIDInsertionOrder.count
|
|
- memo.completedTurnIDInsertionOrderStartIndex == limit - 1)
|
|
#expect(memo.completedTurnIDInsertionOrder.count < limit * 2)
|
|
#expect(memo.priorityCompletedModelsByTurnID.count == 2)
|
|
// The oldest completions were evicted, so the early request keeps its alias; the
|
|
// recent completion is still retained and upgrades its request.
|
|
#expect(turns["priority"]?.model == "resolved-model")
|
|
#expect(turns["turn-0"]?.model == "alias-evicted")
|
|
#expect(turns["turn-\(newest)"]?.model == "completed-model")
|
|
}
|
|
|
|
static func insertTestLogs(dbURL: URL, rows: [(epochSeconds: Int64, body: String)]) throws {
|
|
var db: OpaquePointer?
|
|
guard sqlite3_open(dbURL.path, &db) == SQLITE_OK else { throw SQLiteTestError.open }
|
|
defer { sqlite3_close(db) }
|
|
|
|
var stmt: OpaquePointer?
|
|
guard sqlite3_prepare_v2(db, "insert into logs (ts, feedback_log_body) values (?, ?)", -1, &stmt, nil)
|
|
== SQLITE_OK
|
|
else { throw SQLiteTestError.prepare }
|
|
defer { sqlite3_finalize(stmt) }
|
|
|
|
try self.exec(db, "begin transaction")
|
|
let transient = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
|
|
for row in rows {
|
|
sqlite3_bind_int64(stmt, 1, row.epochSeconds)
|
|
sqlite3_bind_text(stmt, 2, row.body, -1, transient)
|
|
guard sqlite3_step(stmt) == SQLITE_DONE else { throw SQLiteTestError.step }
|
|
sqlite3_reset(stmt)
|
|
}
|
|
try self.exec(db, "commit")
|
|
}
|
|
|
|
static func createTestLogsDatabase(at dbURL: URL) throws {
|
|
var db: OpaquePointer?
|
|
guard sqlite3_open(dbURL.path, &db) == SQLITE_OK else { throw SQLiteTestError.open }
|
|
defer { sqlite3_close(db) }
|
|
try self.exec(
|
|
db,
|
|
"create table logs (id integer primary key autoincrement, ts integer not null, feedback_log_body text)")
|
|
try self.exec(db, "create index idx_logs_ts on logs(ts desc, id desc)")
|
|
}
|
|
|
|
static func queryPlan(
|
|
db: OpaquePointer?,
|
|
query: String,
|
|
bindings: [Int64]) throws -> [String]
|
|
{
|
|
var stmt: OpaquePointer?
|
|
guard sqlite3_prepare_v2(db, "explain query plan \(query)", -1, &stmt, nil) == SQLITE_OK else {
|
|
throw SQLiteTestError.prepare
|
|
}
|
|
defer { sqlite3_finalize(stmt) }
|
|
for (offset, value) in bindings.enumerated() {
|
|
sqlite3_bind_int64(stmt, Int32(offset + 1), value)
|
|
}
|
|
|
|
var details: [String] = []
|
|
while sqlite3_step(stmt) == SQLITE_ROW {
|
|
if let detail = sqlite3_column_text(stmt, 3) {
|
|
details.append(String(cString: detail))
|
|
}
|
|
}
|
|
return details
|
|
}
|
|
|
|
static func insertTestLog(dbURL: URL, timestamp: String, body: String) throws {
|
|
try self.insertTestLog(dbURL: dbURL, epochSeconds: self.epochSeconds(timestamp), body: body)
|
|
}
|
|
|
|
static func insertTestLog(dbURL: URL, epochSeconds: Int64, body: String) throws {
|
|
var db: OpaquePointer?
|
|
guard sqlite3_open(dbURL.path, &db) == SQLITE_OK else { throw SQLiteTestError.open }
|
|
defer { sqlite3_close(db) }
|
|
|
|
var stmt: OpaquePointer?
|
|
guard sqlite3_prepare_v2(db, "insert into logs (ts, feedback_log_body) 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_int64(stmt, 1, epochSeconds)
|
|
sqlite3_bind_text(stmt, 2, body, -1, transient)
|
|
guard sqlite3_step(stmt) == SQLITE_DONE else { throw SQLiteTestError.step }
|
|
}
|
|
|
|
private static func execDatabase(dbURL: URL, sql: String) throws {
|
|
var db: OpaquePointer?
|
|
guard sqlite3_open(dbURL.path, &db) == SQLITE_OK else { throw SQLiteTestError.open }
|
|
defer { sqlite3_close(db) }
|
|
try self.exec(db, sql)
|
|
}
|
|
|
|
private static func epochSeconds(_ timestamp: String) -> Int64 {
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.formatOptions = [.withInternetDateTime]
|
|
guard let date = formatter.date(from: timestamp) else { return 0 }
|
|
return Int64(date.timeIntervalSince1970)
|
|
}
|
|
|
|
private static func exec(_ db: OpaquePointer?, _ sql: String) throws {
|
|
var message: UnsafeMutablePointer<CChar>?
|
|
guard sqlite3_exec(db, sql, nil, nil, &message) == SQLITE_OK else {
|
|
sqlite3_free(message)
|
|
throw SQLiteTestError.exec
|
|
}
|
|
}
|
|
|
|
private enum SQLiteTestError: Error {
|
|
case open
|
|
case prepare
|
|
case step
|
|
case exec
|
|
}
|
|
}
|
|
#endif
|