213 lines · 8.9 KB · Swift
RawDownload
import Foundationimport Testing@testable import CodexBarCore struct CodexCompactSubagentAccountingTests {    private typealias Fixture = CodexCompactSubagentFixture    private typealias Usage = Fixture.Usage     @Test    func `parent-confirmed first turn marker drops a compact copied prefix`() throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let day = try env.makeLocalNoon(year: 2026, month: 7, day: 17)        let parentModel = "openai/gpt-5.3"        let leafModel = "openai/gpt-5.4"        let prefix: Usage = (input: 1000, cached: 900, output: 100)        let suffix: Usage = (input: 50, cached: 10, output: 5)         _ = try env.writeCodexSessionFile(            day: day,            filename: "rollout-parent.jsonl",            contents: Fixture.parentContents(                env: env,                day: day,                sessionID: "compact-parent",                model: parentModel,                totals: prefix))        _ = try env.writeCodexSessionFile(            day: day,            filename: "rollout-child.jsonl",            contents: Fixture.childContents(                env: env,                day: day,                fixture: Fixture.Child(                    sessionID: "compact-child",                    parentID: "compact-parent",                    leafModel: leafModel,                    prefix: prefix,                    suffix: suffix,                    preBoundaryLast: (input: 7, cached: 3, output: 2))))         var options = CostUsageScanner.Options(            codexSessionsRoot: env.codexSessionsRoot,            claudeProjectsRoots: nil,            cacheRoot: env.cacheRoot)        options.refreshMinIntervalSeconds = 0         let cold = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day,            options: options)        let warm = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day.addingTimeInterval(1),            options: options)        options.forceRescan = true        let forced = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day.addingTimeInterval(2),            options: options)         for report in [cold, warm, forced] {            let daily = try #require(report.data.first)            #expect(daily.totalTokens == 1155)            let breakdowns = try #require(daily.modelBreakdowns)            #expect(!breakdowns.contains { $0.modelName == CostUsagePricing.codexUnattributedModel })            #expect(breakdowns.first {                $0.modelName == CostUsagePricing.normalizeCodexModel(parentModel)            }?.totalTokens == 1100)            #expect(breakdowns.first {                $0.modelName == CostUsagePricing.normalizeCodexModel(leafModel)            }?.totalTokens == 55)        }         let cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)        let child = try #require(cache.files.values.first { $0.sessionId == "compact-child" })        #expect(child.days[CostUsageScanner.CostUsageDayRange.dayKey(from: day)]?[            CostUsagePricing.normalizeCodexModel(leafModel),        ] == [50, 10, 5])        #expect(child.days.values.allSatisfy { $0[CostUsagePricing.codexUnattributedModel] == nil })        #expect(child.forkBaselineDependencyKey?.hasPrefix("file|") == true)    }     @Test    func `parent snapshot change invalidates a cached compact child classification`() throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let day = try env.makeLocalNoon(year: 2026, month: 7, day: 17)        let parentModel = "openai/gpt-5.3"        let leafModel = "openai/gpt-5.4"        let prefix: Usage = (input: 1000, cached: 900, output: 100)        let mismatchedParent: Usage = (input: 999, cached: 899, output: 99)        let suffix: Usage = (input: 50, cached: 10, output: 5)        let initialParentContents = try Fixture.parentContents(            env: env,            day: day,            sessionID: "cache-parent",            model: parentModel,            totals: mismatchedParent)        let parentURL = try env.writeCodexSessionFile(            day: day,            filename: "rollout-0-cache-parent.jsonl",            contents: initialParentContents)        _ = try env.writeCodexSessionFile(            day: day,            filename: "rollout-1-cache-child.jsonl",            contents: Fixture.childContents(                env: env,                day: day,                fixture: Fixture.Child(                    sessionID: "cache-child",                    parentID: "cache-parent",                    leafModel: leafModel,                    prefix: prefix,                    suffix: suffix,                    preBoundaryLast: nil)))         var options = CostUsageScanner.Options(            codexSessionsRoot: env.codexSessionsRoot,            claudeProjectsRoots: nil,            cacheRoot: env.cacheRoot)        options.refreshMinIntervalSeconds = 0        let before = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day,            options: options)        let beforeDay = try #require(before.data.first)        #expect(beforeDay.totalTokens == 2253)        #expect(beforeDay.modelBreakdowns?.first {            $0.modelName == CostUsagePricing.codexUnattributedModel        }?.totalTokens == 1100)        let beforeCache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)        let beforeChild = try #require(beforeCache.files.values.first { $0.sessionId == "cache-child" })        let beforeDependency = try #require(beforeChild.forkBaselineDependencyKey)        #expect(beforeDependency.hasPrefix("file|"))         let appendedParentSnapshot = try env.jsonl([            Fixture.tokenCount(                timestamp: env.isoString(for: day.addingTimeInterval(-0.5)),                model: parentModel,                total: prefix,                last: (input: 1, cached: 1, output: 1)),        ])        try (initialParentContents + appendedParentSnapshot)            .write(to: parentURL, atomically: true, encoding: .utf8)         let after = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day.addingTimeInterval(1),            options: options)        let afterDay = try #require(after.data.first)        #expect(afterDay.totalTokens == 1155)        #expect(!(afterDay.modelBreakdowns ?? []).contains {            $0.modelName == CostUsagePricing.codexUnattributedModel        })        let afterCache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)        let afterChild = try #require(afterCache.files.values.first { $0.sessionId == "cache-child" })        #expect(afterChild.forkBaselineDependencyKey?.hasPrefix("file|") == true)        #expect(afterChild.forkBaselineDependencyKey != beforeDependency)        #expect(afterChild.days.values.allSatisfy { $0[CostUsagePricing.codexUnattributedModel] == nil })    }     @Test    func `unconfirmed compact prefix stays independent and parent-dependent`() throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let day = try env.makeLocalNoon(year: 2026, month: 7, day: 17)        let leafModel = "openai/gpt-5.4"        let prefix: Usage = (input: 1000, cached: 900, output: 100)        let suffix: Usage = (input: 50, cached: 10, output: 5)        let fileURL = try env.writeCodexSessionFile(            day: day,            filename: "rollout-unconfirmed-child.jsonl",            contents: Fixture.childContents(                env: env,                day: day,                fixture: Fixture.Child(                    sessionID: "unconfirmed-child",                    parentID: "unconfirmed-parent",                    leafModel: leafModel,                    prefix: prefix,                    suffix: suffix,                    preBoundaryLast: nil)))        let baselines: [CostUsageScanner.CodexForkBaseline] = [            .resolved(.init(input: 999, cached: 899, output: 99)),            .unresolved,        ]         for baseline in baselines {            let parsed = CostUsageScanner.parseCodexFile(                fileURL: fileURL,                range: CostUsageScanner.CostUsageDayRange(since: day, until: day),                inheritedTotalsResolver: { _, _ in baseline })            let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day)            #expect(parsed.days[dayKey]?[CostUsagePricing.codexUnattributedModel] == [1000, 900, 100])            #expect(parsed.days[dayKey]?[CostUsagePricing.normalizeCodexModel(leafModel)] == [50, 10, 5])            #expect(parsed.dependsOnParentTotals)        }    }}