native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203import Foundationimport Testing
@testable import Prowl
/// Composition tests for the screen detection pipeline.////// `ScreenHeuristicsTests` and the fixture corpus prove `screen -> AgentScreenDetection`,/// and `AgentStateMachineTests` proves `event -> AgentStateDecision`, but each stage is/// driven by a hand-built input of its own type, so nothing verifies that a screen reaches/// the status the sidebar and `prowl list` report. These tests start from a screen and run/// the stages production runs on each poll: the agent's screen profile,/// `AgentDetectionCoordinator`, and `PaneAgentState`. Most cases take the screen path of a/// pane with no process generation; one supplies a generation and native status through/// the coordinator's provider sample.@MainActorstruct AgentDetectionPipelineTests { private final class Clock { var now: TimeInterval = 100 }
private let clock: Clock private let coordinator: AgentDetectionCoordinator
init() { let clock = Clock() self.clock = clock coordinator = AgentDetectionCoordinator(time: { clock.now }) }
/// Runs one captured screen through detection, coordination, and pane state, as /// `WorktreeTerminalState` does on each poll. private func report( _ screen: String, agent: DetectedAgent = .claude, through coordinator: AgentDetectionCoordinator? = nil, process: AgentProcessGeneration? = nil ) async -> PaneAgentState? { let detection = agent.detectScreen(in: screen) let decision = await (coordinator ?? self.coordinator).observe( agent: agent, process: process, screen: detection, screenContentID: detection.state == .blocked ? screen.hashValue : nil, capturedAt: clock.now, configRoot: nil ) guard let decision else { return nil } return PaneAgentState(detectedAgent: agent, fallbackState: detection.state, state: decision.state) }
@Test func longRunningTurnIsReportedBusyEndToEnd() async throws { // Claude Code 2.1.220: a turn past the minute mark shows a compound elapsed counter. let screen = """ ⏺ Update(docs/guide.md) ⎿ Updated docs/guide.md with 12 additions
● Actioning… (28m 34s · ↓ 47.7k tokens) ───────── ❯ ───────── ⏵⏵ auto mode on (shift+tab to cycle) · ← for agents """
let pane = try #require(await report(screen)) #expect(pane.fallbackState == .working) #expect(pane.displayState == .working) #expect(pane.isBusy) }
/// Claude Code 2.1.220: the main turn has finished while background agents still run, /// so the only live signal is the agent switcher below the composer. private static let backgroundAgentsScreen = """ ⏺ Waiting for the background agents to report.
✻ Waiting for 2 background agents to finish ───────── ❯ ───────── [Opus 5 (1M context)] | ############-------- 60% | $XX.XX 🟢 ⏵⏵ auto mode on (shift+tab to cycle) · ← for agents
⏺ main ◯ Explore Survey the parser 1m 6s · ↓ 28.6k tokens ◯ general-purpose Review the tests 4m 29s · ↓ 178.9k tokens """
/// The same pane after every background agent has finished: the switcher is gone. private static let finishedScreen = """ ⏺ All three agents returned.
───────── ❯ ───────── [Opus 5 (1M context)] | -------------------- 0% | $XX.XX 🟢 ⏵⏵ auto mode on (shift+tab to cycle) · ← for agents """
@Test func liveBackgroundAgentsAreReportedBusyEndToEnd() async throws { let pane = try #require(await report(Self.backgroundAgentsScreen)) #expect(pane.fallbackState == .working) #expect(pane.displayState == .working) #expect(pane.isBusy) }
@Test func finishedBackgroundAgentsReleaseTheBusyPane() async throws { // Negative control for the two tests above, through one coordinator: the pane that // was busy on the switcher must stop being busy once the switcher is gone. let busy = try #require(await report(Self.backgroundAgentsScreen)) #expect(busy.isBusy)
clock.now += 5 let pane = try #require(await report(Self.finishedScreen)) #expect(pane.fallbackState == .idle) #expect(pane.state == .idle) #expect(pane.displayState == .idle) #expect(!pane.isBusy) }
@Test func nativeStatusDecidesALivePaneOverItsScreen() async throws { // A live pane has a process generation, so after the screen the coordinator samples // the Claude runtime provider. While native status is available it decides the state: // each step pairs native status with a screen that reads the opposite way. final class Native { var state: AgentRawState = .working var updatedAt: TimeInterval = 1 } let native = Native() let clock = clock let live = AgentDetectionCoordinator( sample: { _, _ in [.native(AgentNativeSnapshot(sessionID: "session", state: native.state, statusUpdatedAt: native.updatedAt))] }, time: { clock.now } ) let generation = AgentProcessGeneration(pid: 42, startedAt: Date(timeIntervalSince1970: 1))
let working = try #require(await report(Self.finishedScreen, through: live, process: generation)) #expect(working.fallbackState == .idle) #expect(working.displayState == .working) #expect(working.isBusy)
native.state = .idle native.updatedAt = 2 clock.now += 1 let idle = try #require(await report(Self.backgroundAgentsScreen, through: live, process: generation)) #expect(idle.fallbackState == .working) #expect(idle.displayState == .idle) #expect(!idle.isBusy) }
@Test func blockedPromptReplacesAWorkingTurnAtOnce() async throws { let working = """ ● Running the test suite… (2m 28s · ↓ 13.2k tokens) ───────── ❯ ───────── """ let prompt = """ Do you want to proceed? ❯ 1. Yes 2. No Esc to cancel · Tab to amend """
let before = try #require(await report(working)) #expect(before.displayState == .working)
clock.now += 0.5 let pane = try #require(await report(prompt)) #expect(pane.fallbackState == .blocked) #expect(pane.displayState == .blocked) #expect(pane.isBusy) }
@Test func transcriptViewerKeepsALongTurnBusy() async throws { // Opening the transcript viewer mid-turn hides the live status area, so the screen // classifies as unknown. The pane keeps its last classified state until the screen // classifies again. let viewer = try String( contentsOf: AgentScreenFixtureCorpus.root.appending(path: "claude/2.1.270/unknown/scrolled-transcript.txt"), encoding: .utf8 ) let working = """ ● Running the test suite… (2m 28s · ↓ 13.2k tokens) ───────── ❯ ───────── """
let before = try #require(await report(working)) #expect(before.displayState == .working)
clock.now += 2 let pane = try #require(await report(viewer)) #expect(pane.fallbackState == .unknown) #expect(pane.displayState == .working) #expect(pane.isBusy) }}