193 lines · 8.4 KB · Swift
RawDownload
import Foundationimport Testing@testable import CodexBarCore struct HooksTests {    private func event(        _ type: HookEventType = .quotaReached,        provider: String = "codex",        usagePercent: Double? = 0.95,        account: String? = nil,        window: String? = "session") -> HookEvent    {        HookEvent(            event: type,            provider: provider,            account: account,            window: window,            usagePercent: usagePercent,            resetAt: Date(timeIntervalSince1970: 1_700_000_000),            timestamp: Date(timeIntervalSince1970: 1_700_000_100))    }     // MARK: - Matching     @Test    func `rule matches on event and provider`() {        let rule = HookRule(event: .quotaReached, provider: "codex", executable: "/bin/echo")        #expect(rule.matches(self.event(.quotaReached, provider: "codex")))        #expect(!rule.matches(self.event(.quotaReached, provider: "claude")))        #expect(!rule.matches(self.event(.quotaLow, provider: "codex")))    }     @Test    func `nil provider matches any provider`() {        let rule = HookRule(event: .quotaReached, provider: nil, executable: "/bin/echo")        #expect(rule.matches(self.event(.quotaReached, provider: "codex")))        #expect(rule.matches(self.event(.quotaReached, provider: "claude")))    }     @Test    func `quotaLow threshold gates on usage percent`() {        let rule = HookRule(event: .quotaLow, threshold: 0.90, executable: "/bin/echo")        #expect(rule.matches(self.event(.quotaLow, usagePercent: 0.92)))        #expect(rule.matches(self.event(.quotaLow, usagePercent: 0.90)))        #expect(!rule.matches(self.event(.quotaLow, usagePercent: 0.80)))        #expect(!rule.matches(self.event(.quotaLow, usagePercent: nil)))    }     @Test    func `disabled rule and relative path never match`() {        let disabled = HookRule(enabled: false, event: .quotaReached, executable: "/bin/echo")        #expect(!disabled.matches(self.event()))         let relative = HookRule(event: .quotaReached, executable: "my-command")        #expect(!relative.matches(self.event()))    }     @Test    func `disabled config yields no matching rules`() {        let rule = HookRule(event: .quotaReached, executable: "/bin/echo")        let enabled = HooksConfig(enabled: true, events: [rule])        let disabled = HooksConfig(enabled: false, events: [rule])        #expect(enabled.matchingRules(for: self.event()).count == 1)        #expect(disabled.matchingRules(for: self.event()).isEmpty)    }     // MARK: - quota_low threshold crossing     @Test    func `quotaLow rule fires only when its own threshold is crossed upward`() {        let rule = HookRule(event: .quotaLow, threshold: 0.90, executable: "/bin/echo")        // Notification thresholds (50/20 remaining => 0.50/0.80 usage) would not fire        // a 0.90 rule; the rule's own threshold must drive it.        let fallback = [0.50, 0.80]         // Crossing 0.90 upward fires it.        #expect(!QuotaLowHookThreshold.crossedRules(            [rule], previousUsage: 0.85, currentUsage: 0.95, fallbackThresholds: fallback).isEmpty)        // Already above, no new crossing.        #expect(QuotaLowHookThreshold.crossedRules(            [rule], previousUsage: 0.92, currentUsage: 0.97, fallbackThresholds: fallback).isEmpty)        // Below threshold, no fire even though notification thresholds were crossed.        #expect(QuotaLowHookThreshold.crossedRules(            [rule], previousUsage: 0.40, currentUsage: 0.85, fallbackThresholds: fallback).isEmpty)    }     @Test    func `thresholdless quotaLow rule falls back to notification thresholds`() {        let rule = HookRule(event: .quotaLow, threshold: nil, executable: "/bin/echo")        let fallback = [0.50, 0.80]        #expect(!QuotaLowHookThreshold.crossedRules(            [rule], previousUsage: 0.40, currentUsage: 0.55, fallbackThresholds: fallback).isEmpty)        #expect(QuotaLowHookThreshold.crossedRules(            [rule], previousUsage: 0.55, currentUsage: 0.60, fallbackThresholds: fallback).isEmpty)    }     @Test    func `only the crossed rule is selected among several`() {        let low = HookRule(id: "low", event: .quotaLow, threshold: 0.50, executable: "/bin/echo")        let high = HookRule(id: "high", event: .quotaLow, threshold: 0.90, executable: "/bin/echo")        // Usage rises past 0.90; 0.50 already fired earlier so must not re-fire.        let crossed = QuotaLowHookThreshold.crossedRules(            [low, high], previousUsage: 0.85, currentUsage: 0.95, fallbackThresholds: [])        #expect(crossed.map(\.id) == ["high"])    }     // MARK: - Payload     @Test    func `environment variables include set fields and omit nil`() {        let env = self.event(.quotaLow, usagePercent: 0.5, account: nil, window: "weekly")            .environmentVariables()        #expect(env["CODEXBAR_EVENT"] == "quota_low")        #expect(env["CODEXBAR_PROVIDER"] == "codex")        #expect(env["CODEXBAR_WINDOW"] == "weekly")        #expect(env["CODEXBAR_USAGE_PERCENT"] == "0.5")        #expect(env["CODEXBAR_RESET_AT"] == "2023-11-14T22:13:20Z")        #expect(env["CODEXBAR_TIMESTAMP"] != nil)        #expect(env["CODEXBAR_ACCOUNT"] == nil)        #expect(env["CODEXBAR_STATUS"] == nil)    }     @Test    func `json payload round-trips`() throws {        let original = self.event(.quotaReached, provider: "claude", usagePercent: 0.42, window: "session")        let data = try original.jsonPayload()        let decoder = JSONDecoder()        decoder.dateDecodingStrategy = .iso8601        let decoded = try decoder.decode(HookEvent.self, from: data)        #expect(decoded.event == .quotaReached)        #expect(decoded.provider == "claude")        #expect(decoded.usagePercent == 0.42)        #expect(decoded.window == "session")    }     // MARK: - Rate limiter     @Test    func `rate limiter suppresses same key within window`() async {        let limiter = HookRateLimiter(window: 600)        let base = Date(timeIntervalSince1970: 1_000_000)        #expect(await limiter.allow(self.event(), now: base))        #expect(await !limiter.allow(self.event(), now: base.addingTimeInterval(300)))        #expect(await limiter.allow(self.event(), now: base.addingTimeInterval(601)))    }     @Test    func `rate limiter treats distinct keys independently`() async {        let limiter = HookRateLimiter(window: 600)        let now = Date(timeIntervalSince1970: 1_000_000)        #expect(await limiter.allow(self.event(provider: "codex"), now: now))        #expect(await limiter.allow(self.event(provider: "claude"), now: now))    }     // MARK: - Runner     @Test    func `only storm-prone events are rate limited`() {        #expect(HookEventType.refreshFailed.isRateLimited)        #expect(HookEventType.providerUnavailable.isRateLimited)        #expect(!HookEventType.quotaLow.isRateLimited)        #expect(!HookEventType.quotaReached.isRateLimited)        #expect(!HookEventType.quotaReset.isRateLimited)        #expect(!HookEventType.providerRecovered.isRateLimited)    }     @Test    func `runner executes command and passes event environment`() async throws {        // /usr/bin/env prints the environment; assert our injected vars reach the child.        let rule = HookRule(event: .quotaReached, executable: "/usr/bin/env")        let result = try await HookRunner.run(rule: rule, event: self.event())        #expect(result.stdout.contains("CODEXBAR_EVENT=quota_reached"))        #expect(result.stdout.contains("CODEXBAR_PROVIDER=codex"))    }     @Test    func `runner does not forward secrets from the base environment`() async throws {        let rule = HookRule(event: .quotaReached, executable: "/usr/bin/env")        let base = ["PATH": "/usr/bin:/bin", "UNRELATED_VARIABLE": "sensitive-fixture"]        let result = try await HookRunner.run(rule: rule, event: self.event(), baseEnvironment: base)        #expect(result.stdout.contains("PATH=/usr/bin:/bin")) // allowlisted variable forwarded        #expect(!result.stdout.contains("sensitive-fixture")) // non-allowlisted value dropped        #expect(!result.stdout.contains("UNRELATED_VARIABLE"))    }     @Test    func `runner throws on missing executable`() async {        let rule = HookRule(event: .quotaReached, executable: "/nonexistent/codexbar-hook")        await #expect(throws: SubprocessRunnerError.self) {            try await HookRunner.run(rule: rule, event: self.event())        }    }}