Files
2026-07-13 12:22:33 +08:00

1684 lines
60 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
#if canImport(Darwin)
import Darwin
#else
import Glibc
#endif
private func waitForFile(_ url: URL) async throws {
for _ in 0..<100 where !FileManager.default.fileExists(atPath: url.path) {
try await Task.sleep(for: .milliseconds(20))
}
#expect(FileManager.default.fileExists(atPath: url.path))
}
@Suite(.serialized)
struct KiroStatusProbeTests {
@Test
func `fetch returns usage when account probe times out`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
sleep 5
printf 'Logged in with Google\\nEmail: person@example.com\\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\\n'
printf 'Credits (12.50 of 50 covered in plan)\\n'
printf '████████████████████ 25%%\\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let probe = KiroStatusProbe(cliBinaryResolver: { cliURL.path }, accountProbeTimeout: 0.2)
let snapshot = try await probe.fetch()
#expect(snapshot.planName == "KIRO FREE")
#expect(snapshot.creditsUsed == 12.50)
#expect(snapshot.accountEmail == nil)
#expect(snapshot.authMethod == nil)
}
@Test
func `pipe and PTY share the account deadline`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
if [ ! -t 1 ]; then
sleep 5
exit 1
fi
sleep 0.45
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let snapshot = try await KiroStatusProbe(
cliBinaryResolver: { cliURL.path },
accountProbeTimeout: 0.8,
pipeTimeoutCap: 0.4).fetch()
#expect(snapshot.planName == "KIRO FREE")
#expect(snapshot.accountEmail == nil)
#expect(snapshot.authMethod == nil)
}
@Test
func `accepted pipe output cannot overrun the usage deadline`() async throws {
let pipePIDFile = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-deadline-\(UUID().uuidString).pid")
let ptyMarker = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-deadline-\(UUID().uuidString).pty")
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
if [ ! -t 1 ]; then
printf '%s\n' "$$" > '\(pipePIDFile.path)'
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
trap '' TERM
while true; do sleep 1; done
fi
: > '\(ptyMarker.path)'
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer {
if let text = try? String(contentsOf: pipePIDFile, encoding: .utf8),
let pipePID = pid_t(text.trimmingCharacters(in: .whitespacesAndNewlines))
{
_ = kill(pipePID, SIGKILL)
}
try? FileManager.default.removeItem(at: pipePIDFile)
try? FileManager.default.removeItem(at: ptyMarker)
try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent())
}
let clock = ContinuousClock()
let startedAt = clock.now
let probe = KiroStatusProbe(
cliBinaryResolver: { cliURL.path },
usageProbeTimeout: 0.8,
pipeTimeoutCap: 0.4)
await #expect {
_ = try await probe.fetch()
} throws: { error in
guard case KiroStatusProbeError.timeout = error else { return false }
return true
}
#expect(startedAt.duration(to: clock.now) < .seconds(2))
#expect(!FileManager.default.fileExists(atPath: ptyMarker.path))
let pipePIDText = try String(contentsOf: pipePIDFile, encoding: .utf8)
let pipePID = try #require(pid_t(pipePIDText.trimmingCharacters(in: .whitespacesAndNewlines)))
#expect(kill(pipePID, 0) == -1)
}
@Test
func `fetch preserves account info when account probe succeeds`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\\nEmail: person@example.com\\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\\n'
printf 'Credits (12.50 of 50 covered in plan)\\n'
printf '████████████████████ 25%%\\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let probe = KiroStatusProbe(cliBinaryResolver: { cliURL.path })
let snapshot = try await probe.fetch()
#expect(snapshot.accountEmail == "person@example.com")
#expect(snapshot.authMethod == "Google")
}
}
extension KiroStatusProbeTests {
@Test
func `fetch supports kiro cli that only completes through pipes`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ -t 1 ]; then
sleep 30
exit 1
fi
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let probe = KiroStatusProbe(cliBinaryResolver: { cliURL.path })
let snapshot = try await probe.fetch()
#expect(snapshot.planName == "KIRO FREE")
#expect(snapshot.creditsUsed == 12.50)
#expect(snapshot.accountEmail == "person@example.com")
}
@Test
func `slow pipe remains viable after PTY fallback starts`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ -t 1 ]; then
exit 97
fi
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
sleep 1
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let snapshot = try await KiroStatusProbe(
cliBinaryResolver: { cliURL.path },
usageProbeTimeout: 2,
pipeTimeoutCap: 0.2).fetch()
#expect(snapshot.planName == "KIRO FREE")
}
@Test
func `fetch falls back to PTY for older kiro cli`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ ! -t 1 ]; then
sleep 30
exit 1
fi
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let probe = KiroStatusProbe(
cliBinaryResolver: { cliURL.path },
pipeTimeoutCap: 0.2)
let snapshot = try await probe.fetch()
#expect(snapshot.planName == "KIRO FREE")
#expect(snapshot.creditsUsed == 12.50)
#expect(snapshot.accountEmail == "person@example.com")
}
@Test
func `fetch falls back to PTY after incomplete pipe output`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ ! -t 1 ]; then
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Plan: loading...\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
fi
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let snapshot = try await KiroStatusProbe(cliBinaryResolver: { cliURL.path }).fetch()
#expect(snapshot.planName == "KIRO FREE")
#expect(snapshot.creditsUsed == 12.50)
}
@Test
func `pipe cleanup finishes before PTY fallback starts`() async throws {
let childPIDFile = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-pipe-child-\(UUID().uuidString).pid")
let ptyMarker = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-pty-fallback-\(UUID().uuidString).started")
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
if [ ! -t 1 ]; then
(trap '' TERM; while true; do sleep 1; done) &
child=$!
printf '%s\n' "$child" > '\(childPIDFile.path)'
printf 'Plan: loading...\n'
exit 0
fi
if test -s '\(childPIDFile.path)' && kill -0 "$(cat '\(childPIDFile.path)')" 2>/dev/null; then
printf 'pipe child still running\n' >&2
exit 97
fi
: > '\(ptyMarker.path)'
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer {
if let text = try? String(contentsOf: childPIDFile, encoding: .utf8),
let childPID = pid_t(text.trimmingCharacters(in: .whitespacesAndNewlines))
{
_ = kill(childPID, SIGKILL)
}
try? FileManager.default.removeItem(at: childPIDFile)
try? FileManager.default.removeItem(at: ptyMarker)
try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent())
}
let snapshot = try await KiroStatusProbe(cliBinaryResolver: { cliURL.path }).fetch()
#expect(snapshot.planName == "KIRO FREE")
#expect(FileManager.default.fileExists(atPath: ptyMarker.path))
let childPIDText = try String(contentsOf: childPIDFile, encoding: .utf8)
let childPID = try #require(pid_t(childPIDText.trimmingCharacters(in: .whitespacesAndNewlines)))
#expect(kill(childPID, 0) == -1)
}
@Test
func `shutdown registry terminates an active pipe probe`() async throws {
let pipePIDFile = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-shutdown-\(UUID().uuidString).pid")
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
if [ -t 1 ]; then
exit 97
fi
printf '%s\n' "$$" > '\(pipePIDFile.path)'
printf 'Plan: loading...\n'
trap '' TERM
while true; do sleep 1; done
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
let registry = KiroTestProcessRegistry()
let task = Task {
try await KiroStatusProbe(
cliBinaryResolver: { cliURL.path },
pipeProcessRegistry: registry.dependencies).fetch()
}
defer {
task.cancel()
if let text = try? String(contentsOf: pipePIDFile, encoding: .utf8),
let pipePID = pid_t(text.trimmingCharacters(in: .whitespacesAndNewlines))
{
_ = kill(pipePID, SIGKILL)
}
try? FileManager.default.removeItem(at: pipePIDFile)
try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent())
}
try await waitForFile(pipePIDFile)
let pipePIDText = try String(contentsOf: pipePIDFile, encoding: .utf8)
let pipePID = try #require(pid_t(pipePIDText.trimmingCharacters(in: .whitespacesAndNewlines)))
#expect(registry.isRegistered(pipePID))
let clock = ContinuousClock()
let shutdownStartedAt = clock.now
registry.terminate(pipePID)
await #expect(throws: (any Error).self) {
_ = try await task.value
}
#expect(shutdownStartedAt.duration(to: clock.now) < .seconds(2))
#expect(!registry.isRegistered(pipePID))
#expect(registry.didUnregister(pipePID))
#expect(kill(pipePID, 0) == -1)
}
@Test
func `fetch combines pipe stdout with stderr warnings`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ -t 1 ]; then
exit 91
fi
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
printf 'warning: cached session\n' >&2
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
printf 'warning: telemetry unavailable\n' >&2
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let snapshot = try await KiroStatusProbe(cliBinaryResolver: { cliURL.path }).fetch()
#expect(snapshot.planName == "KIRO FREE")
#expect(snapshot.accountEmail == "person@example.com")
#expect(snapshot.authMethod == "Google")
}
@Test
func `fetch falls back to PTY after pipe requires a terminal`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ ! -t 1 ]; then
printf 'terminal required\n' >&2
exit 2
fi
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
printf 'Context window: 7.5%% used (estimated)\n'
printf '█ Context files 2.5%% (estimated)\n'
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let snapshot = try await KiroStatusProbe(cliBinaryResolver: { cliURL.path }).fetch()
#expect(snapshot.planName == "KIRO FREE")
#expect(snapshot.accountEmail == "person@example.com")
#expect(snapshot.contextUsage?.totalPercentUsed == 7.5)
#expect(snapshot.contextUsage?.contextFilesPercent == 2.5)
}
@Test
func `pipe auth failure on stderr remains authoritative`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ ! -t 1 ]; then
printf 'Opening browser...\n'
printf 'Not logged in\n' >&2
exit 1
fi
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
await #expect {
_ = try await KiroStatusProbe(cliBinaryResolver: { cliURL.path }).fetch()
} throws: { error in
guard case KiroStatusProbeError.notLoggedIn = error else { return false }
return true
}
}
@Test
func `fetch rejects account markers from failed whoami`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 23
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let snapshot = try await KiroStatusProbe(cliBinaryResolver: { cliURL.path }).fetch()
#expect(snapshot.accountEmail == nil)
#expect(snapshot.authMethod == nil)
}
@Test
func `fetch rejects valid-looking usage from failed command`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ -t 1 ]; then
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 0
fi
fi
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 23
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let probe = KiroStatusProbe(cliBinaryResolver: { cliURL.path })
await #expect {
_ = try await probe.fetch()
} throws: { error in
guard case KiroStatusProbeError.cliFailed = error else { return false }
return true
}
}
@Test
func `fetch preserves not logged in when usage fails without auth detail`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
printf 'Not logged in\\n'
exit 1
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
exit 1
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let probe = KiroStatusProbe(cliBinaryResolver: { cliURL.path })
await #expect {
_ = try await probe.fetch()
} throws: { error in
guard case KiroStatusProbeError.notLoggedIn = error else { return false }
return true
}
}
@Test
func `fetch preserves not logged in when whoami idles after login marker`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
printf 'Not logged in\n'
sleep 5
exit 1
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
exit 1
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let probe = KiroStatusProbe(cliBinaryResolver: { cliURL.path }, accountProbeTimeout: 2)
await #expect {
_ = try await probe.fetch()
} throws: { error in
guard case KiroStatusProbeError.notLoggedIn = error else { return false }
return true
}
}
@Test
func `fetch preserves not logged in when usage output cannot be parsed`() async throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
printf 'Not logged in\\n'
exit 1
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let probe = KiroStatusProbe(cliBinaryResolver: { cliURL.path })
await #expect {
_ = try await probe.fetch()
} throws: { error in
guard case KiroStatusProbeError.notLoggedIn = error else { return false }
return true
}
}
@Test
func `fetch cancellation during context probe is preserved`() async throws {
let contextStarted = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-context-\(UUID().uuidString).started")
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
: > '\(contextStarted.path)'
trap '' TERM
while true; do sleep 1; done
fi
exit 1
""")
defer {
try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent())
try? FileManager.default.removeItem(at: contextStarted)
}
let probe = KiroStatusProbe(cliBinaryResolver: { cliURL.path })
let task = Task { try await probe.fetch() }
defer { task.cancel() }
try await waitForFile(contextStarted)
let cancelledAt = Date()
task.cancel()
await #expect(throws: CancellationError.self) {
try await task.value
}
#expect(Date().timeIntervalSince(cancelledAt) < 4)
}
@Test
func `cancellation during pipe cleanup wins over an expired context deadline`() async throws {
let cleanupStarted = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-cleanup-\(UUID().uuidString).started")
let unblockCleanup = DispatchSemaphore(value: 0)
let registry = KiroTestProcessRegistry(
blockOnUnregister: 3,
blockStartedURL: cleanupStarted,
unblock: unblockCleanup)
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\nEmail: person@example.com\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\n'
printf 'Credits (12.50 of 50 covered in plan)\n'
printf '████████████████████ 25%%\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer {
unblockCleanup.signal()
try? FileManager.default.removeItem(at: cleanupStarted)
try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent())
}
let probe = KiroStatusProbe(
cliBinaryResolver: { cliURL.path },
contextProbeTimeout: 0.2,
pipeProcessRegistry: registry.dependencies)
let task = Task { try await probe.fetch() }
defer { task.cancel() }
try await waitForFile(cleanupStarted)
task.cancel()
try await Task.sleep(for: .milliseconds(250))
unblockCleanup.signal()
await #expect(throws: CancellationError.self) {
try await task.value
}
}
@Test
func `fetch cancellation while waiting for account probe is preserved`() async throws {
let accountStarted = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-account-\(UUID().uuidString).started")
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
: > '\(accountStarted.path)'
trap '' TERM
while true; do sleep 1; done
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\\n'
printf 'Credits (12.50 of 50 covered in plan)\\n'
printf '████████████████████ 25%%\\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
""")
defer {
try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent())
try? FileManager.default.removeItem(at: accountStarted)
}
let probe = KiroStatusProbe(cliBinaryResolver: { cliURL.path })
let task = Task { try await probe.fetch() }
defer { task.cancel() }
try await waitForFile(accountStarted)
let cancelledAt = Date()
task.cancel()
await #expect(throws: CancellationError.self) {
try await task.value
}
#expect(Date().timeIntervalSince(cancelledAt) < 4)
}
@Test
func `fetch returns promptly when usage helper spawns a detached child`() async throws {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-pipe-\(UUID().uuidString)", isDirectory: true)
let childPIDFile = root.appendingPathComponent("child.pid")
let cliURL = root.appendingPathComponent("kiro-cli")
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
defer {
if let text = try? String(contentsOf: childPIDFile, encoding: .utf8),
let childPID = pid_t(text.trimmingCharacters(in: .whitespacesAndNewlines))
{
_ = kill(childPID, SIGKILL)
}
}
let script = """
#!/bin/bash
set -e
if [ "$1" = "whoami" ]; then
printf 'Logged in with Google\\nEmail: person@example.com\\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
/usr/bin/python3 -c '
import os
import subprocess
import sys
ready_read, ready_write = os.pipe()
child = subprocess.Popen(
[
sys.executable,
"-c",
"import os,signal,sys,time; "
"signal.signal(signal.SIGHUP, signal.SIG_IGN); "
"signal.signal(signal.SIGTERM, signal.SIG_IGN); "
"handle=open(sys.argv[1], \\\"w\\\"); handle.write(str(os.getpid())); handle.close(); "
"os.write(int(sys.argv[2]), b\\\"1\\\"); os.close(int(sys.argv[2])); time.sleep(30)",
os.environ["CODEXBAR_TEST_CHILD_PID_FILE"],
str(ready_write),
],
start_new_session=True,
pass_fds=(ready_write,),
)
os.close(ready_write)
if os.read(ready_read, 1) != b"1":
raise RuntimeError("detached helper exited before signaling readiness")
os.close(ready_read)
'
test -s "$CODEXBAR_TEST_CHILD_PID_FILE"
printf 'Estimated Usage | resets on 2026-06-01 | KIRO FREE\\n'
printf 'Credits (12.50 of 50 covered in plan)\\n'
printf '████████████████████ 25%%\\n'
exit 0
fi
if [ "$1" = "chat" ] && [ "$3" = "/context" ]; then
exit 0
fi
exit 1
"""
try script.write(to: cliURL, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: cliURL.path)
let previousPIDFile = ProcessInfo.processInfo.environment["CODEXBAR_TEST_CHILD_PID_FILE"]
setenv("CODEXBAR_TEST_CHILD_PID_FILE", childPIDFile.path, 1)
defer {
if let previousPIDFile {
setenv("CODEXBAR_TEST_CHILD_PID_FILE", previousPIDFile, 1)
} else {
unsetenv("CODEXBAR_TEST_CHILD_PID_FILE")
}
}
let probe = KiroStatusProbe(cliBinaryResolver: { cliURL.path })
let start = Date()
let snapshot = try await probe.fetch()
let elapsed = Date().timeIntervalSince(start)
let childPIDText = try String(contentsOf: childPIDFile, encoding: .utf8)
let childPID = try #require(pid_t(childPIDText.trimmingCharacters(in: .whitespacesAndNewlines)))
for _ in 0..<50 where kill(childPID, 0) == 0 {
try await Task.sleep(for: .milliseconds(20))
}
// The PTY runner must return promptly and reap a detached helper that keeps the terminal open.
#expect(snapshot.planName == "KIRO FREE")
#expect(snapshot.creditsUsed == 12.50)
#expect(elapsed < 8, "Kiro usage capture should return promptly even with a detached child, took \(elapsed)s")
#expect(kill(childPID, 0) == -1)
}
@Test
func `tty runner hard stops a process that ignores SIGTERM`() throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
trap '' TERM
printf 'partial output\\n'
while true; do sleep 1; done
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let start = Date()
let result = try TTYCommandRunner().run(
binary: cliURL.path,
send: "",
options: .init(timeout: 2, idleTimeout: 0.1))
let elapsed = Date().timeIntervalSince(start)
#expect(result.completion == .idleTimeout)
#expect(result.text.contains("partial output"))
#expect(elapsed < 3, "Ignored SIGTERM should escalate to SIGKILL, took \(elapsed)s")
}
@Test
func `tty runner kills a pipe holder that escapes the process group`() async throws {
let childPIDFile = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-escaped-\(UUID().uuidString).pid")
let cliURL = try self.makeCLI(
"""
#!/usr/bin/python3
import subprocess
import sys
import time
child = subprocess.Popen(
[
sys.executable,
"-c",
"import signal,time; signal.signal(signal.SIGTERM, signal.SIG_IGN); time.sleep(30)",
],
start_new_session=True,
)
with open(sys.argv[1], "w") as handle:
handle.write(str(child.pid))
print("partial output", flush=True)
time.sleep(30)
""")
defer {
try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent())
try? FileManager.default.removeItem(at: childPIDFile)
}
let result = try TTYCommandRunner().run(
binary: cliURL.path,
send: "",
options: .init(
timeout: 2,
idleTimeout: 0.1,
extraArgs: [childPIDFile.path]))
#expect(result.completion == .idleTimeout)
#expect(result.text.contains("partial output"))
let childPIDText = try String(contentsOf: childPIDFile, encoding: .utf8)
let childPID = try #require(pid_t(childPIDText.trimmingCharacters(in: .whitespacesAndNewlines)))
defer { _ = kill(childPID, SIGKILL) }
let cleanupDeadline = Date().addingTimeInterval(1)
while kill(childPID, 0) == 0, Date() < cleanupDeadline {
try await Task.sleep(for: .milliseconds(20))
}
#expect(kill(childPID, 0) == -1)
}
@Test
func `tty runner cleans a same group helper after normal exit`() async throws {
let childPIDFile = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-normal-exit-\(UUID().uuidString).pid")
let cliURL = try self.makeCLI(
"""
#!/usr/bin/python3
import os
import signal
import sys
import time
child = os.fork()
if child == 0:
os.close(1)
os.close(2)
signal.signal(signal.SIGTERM, signal.SIG_IGN)
with open(sys.argv[1], "w") as handle:
handle.write(str(os.getpid()))
time.sleep(30)
os._exit(0)
while not os.path.exists(sys.argv[1]):
time.sleep(0.01)
print("parent complete", flush=True)
os._exit(0)
""")
defer {
try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent())
try? FileManager.default.removeItem(at: childPIDFile)
}
let result = try TTYCommandRunner().run(
binary: cliURL.path,
send: "",
options: .init(timeout: 2, extraArgs: [childPIDFile.path]))
#expect(result.completion == .processExited(status: 0))
#expect(result.text.contains("parent complete"))
let childPIDText = try String(contentsOf: childPIDFile, encoding: .utf8)
let childPID = try #require(pid_t(childPIDText.trimmingCharacters(in: .whitespacesAndNewlines)))
defer { _ = kill(childPID, SIGKILL) }
let cleanupDeadline = Date().addingTimeInterval(1)
while kill(childPID, 0) == 0, Date() < cleanupDeadline {
try await Task.sleep(for: .milliseconds(20))
}
#expect(kill(childPID, 0) == -1)
}
@Test
func `tty runner preserves completed no-output failure status`() throws {
let cliURL = try self.makeCLI(
"""
#!/bin/sh
exit 23
""")
defer { try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent()) }
let result = try TTYCommandRunner().run(
binary: cliURL.path,
send: "",
options: .init(timeout: 2, returnOnEmptyProcessExit: true))
#expect(result.text.isEmpty)
#expect(result.completion == .processExited(status: 23))
}
@Test
func `tty runner cancellation terminates the process`() async throws {
let pidFile = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-cancel-\(UUID().uuidString).pid")
let cliURL = try self.makeCLI(
"""
#!/bin/sh
printf '%s\\n' "$$" > "$1"
trap '' TERM
while true; do sleep 1; done
""")
defer {
try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent())
try? FileManager.default.removeItem(at: pidFile)
}
let task = Task {
try TTYCommandRunner().run(
binary: cliURL.path,
send: "",
options: .init(timeout: 20, extraArgs: [pidFile.path]))
}
defer { task.cancel() }
var capturedProcessID: pid_t?
for _ in 0..<100 {
if let text = try? String(contentsOf: pidFile, encoding: .utf8) {
capturedProcessID = pid_t(text.trimmingCharacters(in: .whitespacesAndNewlines))
break
}
try await Task.sleep(for: .milliseconds(20))
}
let processID = try #require(capturedProcessID)
defer { _ = kill(processID, SIGKILL) }
task.cancel()
await #expect(throws: CancellationError.self) {
try await task.value
}
#expect(kill(processID, 0) == -1)
}
}
extension KiroStatusProbeTests {
// MARK: - Happy Path Parsing
@Test
func `parses basic usage output`() throws {
let output = """
| KIRO FREE |
████████████████████████████████████████████████████ 25%
(12.50 of 50 covered in plan), resets on 01/15
"""
let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)
#expect(snapshot.planName == "KIRO FREE")
#expect(snapshot.displayPlanName == "Kiro Free")
#expect(snapshot.creditsPercent == 25)
#expect(snapshot.creditsUsed == 12.50)
#expect(snapshot.creditsTotal == 50)
#expect(snapshot.bonusCreditsUsed == nil)
#expect(snapshot.bonusCreditsTotal == nil)
#expect(snapshot.bonusExpiryDays == nil)
#expect(snapshot.resetsAt != nil)
}
private func makeCLI(_ script: String) throws -> URL {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-cli-\(UUID().uuidString)", isDirectory: true)
let cliURL = root.appendingPathComponent("kiro-cli")
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
try script.write(to: cliURL, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: cliURL.path)
return cliURL
}
@Test
func `parses output with bonus credits`() throws {
let output = """
| KIRO PRO |
████████████████████████████████████████████████████ 80%
(40.00 of 50 covered in plan), resets on 02/01
Bonus credits: 5.00/10 credits used, expires in 7 days
"""
let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)
#expect(snapshot.planName == "KIRO PRO")
#expect(snapshot.displayPlanName == "Kiro Pro")
#expect(snapshot.creditsPercent == 80)
#expect(snapshot.creditsUsed == 40.00)
#expect(snapshot.creditsTotal == 50)
#expect(snapshot.bonusCreditsUsed == 5.00)
#expect(snapshot.bonusCreditsTotal == 10)
#expect(snapshot.bonusExpiryDays == 7)
}
@Test
func `parses output without percent fallbacks to credits ratio`() throws {
let output = """
| KIRO FREE |
(12.50 of 50 covered in plan), resets on 01/15
"""
let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)
#expect(snapshot.creditsPercent == 25)
}
@Test
func `parses bonus credits without expiry`() throws {
let output = """
| KIRO FREE |
████████████████████████████████████████████████████ 60%
(30.00 of 50 covered in plan), resets on 04/01
Bonus credits: 2.00/5 credits used
"""
let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)
#expect(snapshot.bonusCreditsUsed == 2.0)
#expect(snapshot.bonusCreditsTotal == 5.0)
#expect(snapshot.bonusExpiryDays == nil)
}
@Test
func `parses output with ANSI codes`() throws {
let output = """
\u{001B}[32m| KIRO FREE |\u{001B}[0m
\u{001B}[38;5;11m████████████████████████████████████████████████████\u{001B}[0m 50%
(25.00 of 50 covered in plan), resets on 03/15
"""
let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)
#expect(snapshot.planName == "KIRO FREE")
#expect(snapshot.creditsPercent == 50)
#expect(snapshot.creditsUsed == 25.00)
#expect(snapshot.creditsTotal == 50)
}
@Test
func `parses output with single day`() throws {
let output = """
| KIRO FREE |
████████████████████████████████████████████████████ 10%
(5.00 of 50 covered in plan)
Bonus credits: 2.00/5 credits used, expires in 1 day
"""
let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)
#expect(snapshot.bonusExpiryDays == 1)
}
@Test
func `rejects output missing usage markers`() throws {
let output = """
| KIRO FREE |
"""
let probe = KiroStatusProbe()
#expect(throws: KiroStatusProbeError.self) {
try probe.parse(output: output)
}
}
// MARK: - New Format (kiro-cli 1.24+, Q Developer)
@Test
func `parses Q developer managed plan`() throws {
let output = """
Plan: Q Developer Pro
Your plan is managed by admin
Tip: to see context window usage, run /context
"""
let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)
#expect(snapshot.planName == "Q Developer Pro")
#expect(snapshot.creditsPercent == 0)
#expect(snapshot.creditsUsed == 0)
#expect(snapshot.creditsTotal == 0)
#expect(snapshot.bonusCreditsUsed == nil)
#expect(snapshot.resetsAt == nil)
}
@Test
func `parses Q developer free plan`() throws {
let output = """
Plan: Q Developer Free
Your plan is managed by admin
"""
let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)
#expect(snapshot.planName == "Q Developer Free")
#expect(snapshot.creditsPercent == 0)
}
@Test
func `parses new format with ANSI codes`() throws {
let output = """
\u{001B}[38;5;141mPlan: Q Developer Pro\u{001B}[0m
Your plan is managed by admin
"""
let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)
#expect(snapshot.planName == "Q Developer Pro")
}
@Test
func `rejects header only new format without managed marker`() {
let output = """
Plan: Q Developer Pro
Tip: to see context window usage, run /context
"""
let probe = KiroStatusProbe()
#expect(throws: KiroStatusProbeError.self) {
try probe.parse(output: output)
}
}
@Test
func `preserves parsed usage for managed plan with metrics`() throws {
let output = """
Plan: Q Developer Enterprise
Your plan is managed by admin
████████████████████████████████████████████████████ 40%
(20.00 of 50 covered in plan), resets on 03/15
"""
let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)
#expect(snapshot.planName == "Q Developer Enterprise")
#expect(snapshot.creditsPercent == 40)
#expect(snapshot.creditsUsed == 20)
#expect(snapshot.creditsTotal == 50)
#expect(snapshot.resetsAt != nil)
}
@Test
func `parses kiro cli two usage format`() throws {
let output = """
\u{001B}[1mEstimated Usage\u{001B}[0m | resets on 2026-06-01 | \u{001B}[mKIRO FREE\u{001B}[0m
🎁 Bonus credits: 45.53/2000 credits used, expires in 19 days
\u{001B}[1mCredits\u{001B}[0m (0.17 of 50 covered in plan)
████████████████████████████████████████████████████████████████████████████████ 0%
Overages: \u{001B}[1mDisabled\u{001B}[0m
To manage your plan or configure overages navigate to https://app.kiro.dev/account/usage
"""
let probe = KiroStatusProbe()
let snapshot = try probe.parse(
output: output,
accountEmail: "person@example.com",
authMethod: "Google")
#expect(snapshot.planName == "KIRO FREE")
#expect(snapshot.displayPlanName == "Kiro Free")
#expect(snapshot.accountEmail == "person@example.com")
#expect(snapshot.authMethod == "Google")
#expect(snapshot.creditsUsed == 0.17)
#expect(snapshot.creditsTotal == 50)
#expect(snapshot.creditsRemaining == 49.83)
#expect(snapshot.bonusCreditsUsed == 45.53)
#expect(snapshot.bonusCreditsTotal == 2000)
#expect(snapshot.bonusCreditsRemaining == 1954.47)
#expect(snapshot.bonusExpiryDays == 19)
#expect(snapshot.overagesStatus == "Disabled")
#expect(snapshot.manageURL == "https://app.kiro.dev/account/usage")
#expect(snapshot.resetsAt != nil)
}
@Test
func `parses kiro overage credits and estimated cost`() throws {
let output = """
Estimated Usage | resets on 2026-06-01 | KIRO PRO
Credits (1000.00 of 1000 covered in plan)
████████████████████████████████████████████████████████████████████████████████ 100%
Overages: Enabled billed at $0.04 per request
Credits used: 40.29
Est. cost: $1.61 USD
To manage your plan or configure overages navigate to https://app.kiro.dev/account/usage
"""
let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)
#expect(snapshot.planName == "KIRO PRO")
#expect(snapshot.creditsUsed == 1000)
#expect(snapshot.creditsTotal == 1000)
#expect(snapshot.overagesStatus == "Enabled billed at $0.04 per request")
#expect(snapshot.overageCreditsUsed == 40.29)
#expect(snapshot.estimatedOverageCostUSD == 1.61)
}
@Test
func `parses context usage`() throws {
let output = """
Context window: 1.3% used (estimated)
██████████████████████████████████████████████████████████████████████████████ 1.3%
█ Context files 0.5% (estimated)
█ Tools 0.8% (estimated)
█ Kiro responses 0.0% (estimated)
█ Your prompts 0.0% (estimated)
"""
let probe = KiroStatusProbe()
let context = try #require(probe.parseContextUsage(output: output))
#expect(context.totalPercentUsed == 1.3)
#expect(context.contextFilesPercent == 0.5)
#expect(context.toolsPercent == 0.8)
#expect(context.kiroResponsesPercent == 0)
#expect(context.promptsPercent == 0)
}
// MARK: - Snapshot Conversion
@Test
func `converts snapshot to usage snapshot`() throws {
let now = Date()
let resetDate = try #require(Calendar.current.date(byAdding: .day, value: 7, to: now))
let snapshot = KiroUsageSnapshot(
planName: "KIRO PRO",
creditsUsed: 25.0,
creditsTotal: 100.0,
creditsPercent: 25.0,
bonusCreditsUsed: 5.0,
bonusCreditsTotal: 20.0,
bonusExpiryDays: 14,
resetsAt: resetDate,
updatedAt: now)
let usage = snapshot.toUsageSnapshot()
#expect(usage.primary?.usedPercent == 25.0)
#expect(usage.primary?.resetsAt == resetDate)
#expect(usage.secondary?.usedPercent == 25.0) // 5/20 * 100
#expect(usage.loginMethod(for: .kiro) == nil)
#expect(usage.accountOrganization(for: .kiro) == nil)
#expect(usage.kiroUsage?.displayPlanName == "Kiro Pro")
#expect(usage.kiroUsage?.creditsRemaining == 75)
}
@Test
func `converts snapshot without bonus credits`() {
let snapshot = KiroUsageSnapshot(
planName: "KIRO FREE",
creditsUsed: 10.0,
creditsTotal: 50.0,
creditsPercent: 20.0,
bonusCreditsUsed: nil,
bonusCreditsTotal: nil,
bonusExpiryDays: nil,
resetsAt: nil,
updatedAt: Date())
let usage = snapshot.toUsageSnapshot()
#expect(usage.primary?.usedPercent == 20.0)
#expect(usage.secondary == nil)
}
// MARK: - Error Cases
@Test
func `empty output throws parse error`() {
let probe = KiroStatusProbe()
#expect(throws: KiroStatusProbeError.self) {
try probe.parse(output: "")
}
}
@Test
func `warning output throws parse error`() {
let output = """
\u{001B}[38;5;11m⚠️ Warning: Could not retrieve usage information from backend
\u{001B}[38;5;8mError: dispatch failure (io error): an i/o error occurred
"""
let probe = KiroStatusProbe()
#expect(throws: KiroStatusProbeError.self) {
try probe.parse(output: output)
}
}
@Test
func `unrecognized format throws parse error`() {
// Simulates a CLI format change where none of the expected patterns match
let output = """
Welcome to Kiro!
Your account is active.
Usage: unknown format
"""
let probe = KiroStatusProbe()
#expect {
try probe.parse(output: output)
} throws: { error in
guard case let KiroStatusProbeError.parseError(msg) = error else { return false }
return msg.contains("No recognizable usage patterns")
}
}
@Test
func `login prompt throws not logged in`() {
let output = """
Failed to initialize auth portal.
Please try again with: kiro-cli login --use-device-flow
error: OAuth error: All callback ports are in use.
"""
let probe = KiroStatusProbe()
#expect {
try probe.parse(output: output)
} throws: { error in
guard case KiroStatusProbeError.notLoggedIn = error else { return false }
return true
}
}
// MARK: - WhoAmI Validation
@Test
func `whoami not logged in throws`() {
let probe = KiroStatusProbe()
#expect {
try probe.validateWhoAmIOutput(stdout: "Not logged in", stderr: "", terminationStatus: 1)
} throws: { error in
guard case KiroStatusProbeError.notLoggedIn = error else { return false }
return true
}
}
@Test
func `whoami login required throws`() {
let probe = KiroStatusProbe()
#expect {
try probe.validateWhoAmIOutput(stdout: "login required", stderr: "", terminationStatus: 1)
} throws: { error in
guard case KiroStatusProbeError.notLoggedIn = error else { return false }
return true
}
}
@Test
func `whoami empty output with zero status throws`() {
let probe = KiroStatusProbe()
#expect {
try probe.validateWhoAmIOutput(stdout: "", stderr: "", terminationStatus: 0)
} throws: { error in
guard case KiroStatusProbeError.cliFailed = error else { return false }
return true
}
}
@Test
func `whoami non zero status with message throws`() {
let probe = KiroStatusProbe()
#expect {
try probe.validateWhoAmIOutput(stdout: "", stderr: "Connection error", terminationStatus: 1)
} throws: { error in
guard case KiroStatusProbeError.cliFailed = error else { return false }
return true
}
}
@Test
func `whoami success does not throw`() throws {
let probe = KiroStatusProbe()
let account = try probe.validateWhoAmIOutput(
stdout: """
Logged in with Google
Email: user@example.com
""",
stderr: "",
terminationStatus: 0)
#expect(account.authMethod == "Google")
#expect(account.email == "user@example.com")
}
@Test
func `whoami legacy bare email parses account`() throws {
let probe = KiroStatusProbe()
let account = try probe.validateWhoAmIOutput(
stdout: "user@example.com",
stderr: "",
terminationStatus: 0)
#expect(account.authMethod == nil)
#expect(account.email == "user@example.com")
}
}
extension KiroStatusProbeTests {
@Test
func `fetch cancellation while joining account after usage failure is preserved`() async throws {
let accountStarted = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-kiro-failed-usage-account-\(UUID().uuidString).started")
let cliURL = try self.makeCLI(
"""
#!/bin/sh
if [ "$1" = "whoami" ]; then
: > '\(accountStarted.path)'
trap '' TERM
while true; do sleep 1; done
fi
if [ "$1" = "chat" ] && [ "$3" = "/usage" ]; then
exit 1
fi
exit 1
""")
defer {
try? FileManager.default.removeItem(at: cliURL.deletingLastPathComponent())
try? FileManager.default.removeItem(at: accountStarted)
}
let probe = KiroStatusProbe(cliBinaryResolver: { cliURL.path }, accountProbeTimeout: 2.0)
let task = Task { try await probe.fetch() }
defer { task.cancel() }
try await waitForFile(accountStarted)
try await Task.sleep(for: .milliseconds(300))
task.cancel()
await #expect(throws: CancellationError.self) {
try await task.value
}
}
}