native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
23 kB · 436 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437import Foundationimport ProwlCLISharedimport Testing
@testable import supacode
struct WorkflowRunStoreTests { nonisolated private static let now = Date(timeIntervalSince1970: 1_760_000_000)
private func makeRoot() throws -> URL { let url = FileManager.default.temporaryDirectory .appending(path: "workflow-run-store-tests", directoryHint: .isDirectory) .appending(path: UUID().uuidString, directoryHint: .isDirectory) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) return url }
private func makeRun(root: URL, status: WorkflowRunStatus = .running) throws -> WorkflowRun { let definition = try #require(WorkflowDocumentParser.parse(WorkflowRunMachineTests.handoff).definition) let started = try WorkflowRunMachine.start( WorkflowRunStartRequest( definition: definition, runID: UUID(), context: WorkflowRunContext( scope: .repo(repositoryID: "repo-1"), definitionPath: "/x/review.yaml", worktree: WorkflowRunWorktree( id: "wt", name: "feature", branch: "feat/x", path: root.path(percentEncoded: false))), bindings: [ "source": .current(WorkflowRunMachineTests.authorPane), "receiver": .launch(WorkflowRunMachineTests.reviewerProfile, pane: nil), ]), now: { Self.now }, makeToken: { "SECRET-TOKEN" }) var machine = started.machine _ = machine.apply(.roleIdle(ordinal: 1)) _ = machine.apply(.injectionSucceeded(ordinal: 1, dispatchID: "dispatch-1")) if case .needsAttention = status { _ = machine.apply(.watchdog(ordinal: 1, .attention(.idleWithoutDelivery))) } if status == .cancelled { _ = machine.apply(.user(.cancel)) } return machine.run }
@Test(arguments: [1, 16]) func largeActionRecordsRemainExportableAndExpire(mebibytes: Int) throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let storage = WorkflowHistoryStorage(baseURL: root.appending(path: "history")) var run = try makeRun(root: root) run.status = .completed run.finishedAt = Self.now let json = Data(("\"" + String(repeating: "x", count: mebibytes * 1024 * 1024 - 2) + "\"").utf8) run.actionOutputs["large"] = ["result": try JSONDecoder().decode(WorkflowJSONValue.self, from: json)] let directory = storage.directory(root: root, createdAt: Self.now, runID: run.id) let store = WorkflowRunStore(rootURL: root, directory: directory, storage: storage) try store.ensureLayout(runID: run.id) try store.writeRecord(WorkflowRunRecord(run: run)) let history = WorkflowHistory(storage: storage) let later = Self.now.addingTimeInterval(31 * 86400) #expect(try history.preview(now: later).candidates.map(\.id) == [run.id]) let zip = root.appending(path: "export.zip") try history.export(directory, to: zip) #expect(try history.cleanup(candidates: [run.id], now: later).removed == [run.id]) #expect(FileManager.default.fileExists(atPath: zip.path)) }
@Test func layoutCreatesPersonalHistoryWithoutProjectRuntimeData() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let store = WorkflowRunStore(rootURL: root) let runID = UUID() try store.ensureLayout(runID: runID) let runs = store.directory(for: runID).deletingLastPathComponent() #expect(!FileManager.default.fileExists(atPath: root.appending(path: ".prowl").path)) for name in ["prompts", "deliveries", "skills"] { var isDirectory: ObjCBool = false let path = runs.appending(path: runID.uuidString).appending(path: name).path(percentEncoded: false) #expect(FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) && isDirectory.boolValue) } }
@Test func recordRoundTripsWithoutTokensOrEnvironment() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let store = WorkflowRunStore(rootURL: root) let run = try makeRun( root: root, status: .needsAttention( WorkflowAttention( reason: .idleWithoutDelivery, stepID: "brief", role: "source", ordinal: 1, actions: [], message: ""))) try store.ensureLayout(runID: run.id) let record = WorkflowRunRecord(run: run) try store.writeRecord(record)
let url = store.directory(for: run.id).appending(path: "run.json") let json = try String(contentsOf: url, encoding: .utf8) #expect(json.contains("\"skipped_deliveries\"")) #expect(!json.contains("\"skipped_outputs\"")) let retiredRecord = json.replacing("\"skipped_deliveries\"", with: "\"skipped_outputs\"") #expect(throws: (any Error).self) { try WorkflowRunRecord.makeDecoder().decode(WorkflowRunRecord.self, from: Data(retiredRecord.utf8)) } #expect(json.contains("\"delivery\" : \"brief\"")) #expect(!json.contains("\"output\"")) #expect(!json.contains("SECRET-TOKEN")) #expect(json.contains("\"dispatch_id\" : \"dispatch-1\"")) #expect(json.contains("\"version\" : 1")) #expect(json.contains("\"scope\" : \"repo:repo-1\"")) #expect(json.contains("\"state\" : \"needs_attention\"")) #expect(json.contains("\"reason\" : \"idle_without_delivery\"")) #expect(json.contains("\"agent\" : \"pi\"")) #expect(!json.contains("PROWL_"))
let decoded = try store.readRecord(runID: run.id) #expect(decoded == record) #expect(decoded.inputs == []) #expect(decoded.bindings["receiver"]?.profile == WorkflowRunMachineTests.reviewerProfile) #expect(decoded.bindings["source"]?.pane == WorkflowRunMachineTests.authorPane) #expect(decoded.invocations.first?.activation?.delivery == "brief") #expect(decoded.run.status.attention?.actions == [.nudge, .keepWaiting, .skip, .cancel]) #expect(decoded.run.status.attention?.issues == nil) }
@Test func recordCarriesTheIssueCodesOfAProvisionalDelivery() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } var run = try makeRun(root: root) let attention = WorkflowAttention( reason: .deliveryIssues([.missingSections(["## Objective"]), .verdictMissing(allowed: ["a", "b"])]), stepID: "brief", role: "source", ordinal: 1, actions: [.acceptWithVerdict, .askAgain, .skip, .cancel], message: "m") run.status = .needsAttention(attention) let record = WorkflowRunRecord(run: run) #expect(record.run.status.attention?.reason == "delivery_issues") #expect(record.run.status.attention?.issues == ["missing_sections", "verdict_missing"]) let store = WorkflowRunStore(rootURL: root) try store.ensureLayout(runID: run.id) try store.writeRecord(record) #expect(try store.readRecord(runID: run.id) == record) }
@Test func recordKeepsInputNamesButNeverInputValues() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let definition = try #require(WorkflowDocumentParser.parse(WorkflowRunMachineTests.adversarialReview).definition) let started = try WorkflowRunMachine.start( WorkflowRunStartRequest( definition: definition, runID: UUID(), context: WorkflowRunContext( scope: .user, definitionPath: nil, worktree: WorkflowRunWorktree(id: "wt", name: "w", branch: "b", path: root.path(percentEncoded: false))), bindings: [ "author": .current(WorkflowRunMachineTests.authorPane), "reviewer": .launch(WorkflowRunMachineTests.reviewerProfile, pane: nil), ], inputs: ["focus": "hunter2-secret"]), now: { Self.now }) let store = WorkflowRunStore(rootURL: root) try store.ensureLayout(runID: started.machine.run.id) try store.writeRecord(WorkflowRunRecord(run: started.machine.run)) let json = try String( contentsOf: store.directory(for: started.machine.run.id).appending(path: "run.json"), encoding: .utf8) #expect(!json.contains("hunter2-secret")) #expect(try store.readRecord(runID: started.machine.run.id).inputs == ["focus", "max_rounds", "mode"]) }
@Test func symlinkedLogAndRecordLeavesAreRefused() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let store = WorkflowRunStore(rootURL: root) let runID = UUID() try store.ensureLayout(runID: runID) let victim = root.appending(path: "victim.txt") try "original\n".write(to: victim, atomically: true, encoding: .utf8) let log = store.directory(for: runID).appending(path: "log.md") try FileManager.default.createSymbolicLink(at: log, withDestinationURL: victim) #expect(throws: WorkflowRunStoreError.self) { try store.appendLog(runID: runID, line: "hi", now: Self.now) } #expect(try String(contentsOf: victim, encoding: .utf8) == "original\n") let record = store.directory(for: runID).appending(path: "run.json") try FileManager.default.createSymbolicLink(at: record, withDestinationURL: victim) #expect(throws: WorkflowRunStoreError.self) { try store.readRecord(runID: runID) } let run = try makeRun(root: root) try store.ensureLayout(runID: run.id) try FileManager.default.createSymbolicLink( at: store.directory(for: run.id).appending(path: "run.json"), withDestinationURL: victim) #expect(throws: WorkflowRunStoreError.self) { try store.writeRecord(WorkflowRunRecord(run: run)) } #expect(try String(contentsOf: victim, encoding: .utf8) == "original\n") }
@Test func hardLinkedLogCannotModifyAnExternalFile() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let store = WorkflowRunStore(rootURL: root) let runID = UUID() try store.ensureLayout(runID: runID) let victim = root.appending(path: "victim.txt") try Data("original".utf8).write(to: victim) try FileManager.default.linkItem(at: victim, to: store.directory(for: runID).appending(path: "log.md")) #expect(throws: WorkflowRunStoreError.self) { try store.appendLog(runID: runID, line: "changed", now: Self.now) } #expect(try String(contentsOf: victim, encoding: .utf8) == "original") }
@Test func recordDecodingToleratesUnknownKeys() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let store = WorkflowRunStore(rootURL: root) let run = try makeRun(root: root) try store.ensureLayout(runID: run.id) try store.writeRecord(WorkflowRunRecord(run: run)) let url = store.directory(for: run.id).appending(path: "run.json") var json = try String(contentsOf: url, encoding: .utf8) json = json.replacing("\"version\" : 1", with: "\"version\" : 1,\n \"future_key\" : { \"nested\" : true }") try json.write(to: url, atomically: true, encoding: .utf8) #expect(try store.readRecord(runID: run.id).run.id == run.id) }
@Test func deliveriesAreVersionedAndTheLatestViewIsReplacedAtomically() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let store = WorkflowRunStore(rootURL: root) let runID = UUID() try store.ensureLayout(runID: runID) let first = try store.writeDelivery(runID: runID, name: "findings", ordinal: 2, body: "round 1\n") let second = try store.writeDelivery(runID: runID, name: "findings", ordinal: 4, body: "round 2\n") #expect(first.versioned.lastPathComponent == "findings.2.md") #expect(second.versioned.lastPathComponent == "findings.4.md") #expect(first.latest == second.latest) #expect(try String(contentsOf: first.versioned, encoding: .utf8) == "round 1\n") #expect(try String(contentsOf: second.latest, encoding: .utf8) == "round 2\n") let leftovers = try FileManager.default.contentsOfDirectory( atPath: second.latest.deletingLastPathComponent().path(percentEncoded: false)) let firstSnapshot = WorkflowRunPaths.submissionURL( runDirectory: store.directory(for: runID), name: "findings", ordinal: 2, body: "round 1\n") let secondSnapshot = WorkflowRunPaths.submissionURL( runDirectory: store.directory(for: runID), name: "findings", ordinal: 4, body: "round 2\n") #expect( Set(leftovers) == Set([ "findings.2.md", "findings.4.md", "findings.md", firstSnapshot.lastPathComponent, secondSnapshot.lastPathComponent, ])) try store.writeDelivery(runID: runID, name: "findings", ordinal: 2, body: "corrected\n") #expect(try String(contentsOf: firstSnapshot, encoding: .utf8) == "round 1\n") #expect(try String(contentsOf: first.versioned, encoding: .utf8) == "corrected\n") }
@Test func instructionsAndLogAreWrittenUnderTheRun() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let store = WorkflowRunStore(rootURL: root) let runID = UUID() try store.ensureLayout(runID: runID) let instruction = try store.writePrompt(runID: runID, stepID: "brief", ordinal: 1, text: "Do it.\n") #expect(instruction.lastPathComponent == "brief.1.md") #expect(try String(contentsOf: instruction, encoding: .utf8) == "Do it.\n") try store.appendLog(runID: runID, line: "started", now: Self.now) try store.appendLog(runID: runID, line: "two\nlines", now: Self.now) let log = try String(contentsOf: store.directory(for: runID).appending(path: "log.md"), encoding: .utf8) #expect( log == "# Workflow run \(runID.uuidString)\n\n- 2025-10-09T08:53:20Z started\n- 2025-10-09T08:53:20Z two lines\n") }
@Test func unsafeSlugsAreRejectedBeforeTouchingTheDisk() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let store = WorkflowRunStore(rootURL: root) let runID = UUID() try store.ensureLayout(runID: runID) #expect(throws: WorkflowRunStoreError.unsafePath("../escape")) { try store.writePrompt(runID: runID, stepID: "../escape", ordinal: 1, text: "x") } #expect(throws: WorkflowRunStoreError.unsafePath("Bad Name")) { try store.writeDelivery(runID: runID, name: "Bad Name", ordinal: 1, body: "x") } #expect(throws: WorkflowRunStoreError.unsafePath("brief")) { try store.writePrompt(runID: runID, stepID: "brief", ordinal: 0, text: "x") } }
@Test func symlinkedRunDirectoryLeafIsRejected() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let store = WorkflowRunStore(rootURL: root) let runID = UUID() try FileManager.default.createDirectory( at: store.directory(for: runID).deletingLastPathComponent(), withIntermediateDirectories: true) let elsewhere = root.appending(path: "elsewhere", directoryHint: .isDirectory) try FileManager.default.createDirectory(at: elsewhere, withIntermediateDirectories: true) try FileManager.default.createSymbolicLink(at: store.directory(for: runID), withDestinationURL: elsewhere) #expect(throws: (any Error).self) { try store.ensureLayout(runID: runID) } #expect(try FileManager.default.contentsOfDirectory(atPath: elsewhere.path(percentEncoded: false)).isEmpty) }
@Test func projectLocalRuntimeLinksAreNeitherReadNorModified() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let elsewhere = root.appending(path: "elsewhere") try FileManager.default.createDirectory(at: elsewhere, withIntermediateDirectories: true) try FileManager.default.createSymbolicLink(at: root.appending(path: ".prowl"), withDestinationURL: elsewhere) let store = WorkflowRunStore(rootURL: root) try store.ensureLayout(runID: UUID()) #expect(try FileManager.default.contentsOfDirectory(atPath: elsewhere.path).isEmpty) }
@Test func restartScanSkipsForeignVersionsWithoutDecodingThem() throws { let clean = try makeRoot() defer { try? FileManager.default.removeItem(at: clean) } let store = WorkflowRunStore(rootURL: clean) let foreignID = UUID() try store.ensureLayout(runID: foreignID) try "{\"version\": 2, \"run\": {\"status\": {\"state\": \"running\"}}, \"future\": true}".write( to: store.directory(for: foreignID).appending(path: "run.json"), atomically: true, encoding: .utf8) let headerlessID = UUID() try store.ensureLayout(runID: headerlessID) try "{\"version\": 1}".write( to: store.directory(for: headerlessID).appending(path: "run.json"), atomically: true, encoding: .utf8) let result = try store.markInterruptedRuns(now: { Self.now }) #expect(result.interrupted.isEmpty) #expect( result.unreadable == [store.directory(for: headerlessID).appending(path: "run.json").path(percentEncoded: false)]) #expect( try String(contentsOf: store.directory(for: foreignID).appending(path: "run.json"), encoding: .utf8).contains( "\"future\": true")) }
@Test func aLinkedRunDirectoryIsNeverReadEither() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let store = WorkflowRunStore(rootURL: root) let genuine = try makeRun(root: root) try store.ensureLayout(runID: genuine.id) try store.writeRecord(WorkflowRunRecord(run: genuine)) let external = root.appending(path: "external", directoryHint: .isDirectory) try FileManager.default.createDirectory(at: external, withIntermediateDirectories: true) try FileManager.default.copyItem( at: store.directory(for: genuine.id).appending(path: "run.json"), to: external.appending(path: "run.json")) let linkedID = UUID() try FileManager.default.createSymbolicLink(at: store.directory(for: linkedID), withDestinationURL: external) #expect(throws: (any Error).self) { try store.readRecord(runID: linkedID) } #expect(try store.markInterruptedRuns(now: { Self.now }).unreadable.count == 1) }
@Test func symlinkedDeliveriesDirectoryIsRejected() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let store = WorkflowRunStore(rootURL: root) let runID = UUID() try store.ensureLayout(runID: runID) let deliveries = store.directory(for: runID).appending(path: "deliveries", directoryHint: .isDirectory) try FileManager.default.removeItem(at: deliveries) let elsewhere = root.appending(path: "elsewhere", directoryHint: .isDirectory) try FileManager.default.createDirectory(at: elsewhere, withIntermediateDirectories: true) try FileManager.default.createSymbolicLink(at: deliveries, withDestinationURL: elsewhere) #expect(throws: WorkflowRunStoreError.self) { try store.writeDelivery(runID: runID, name: "findings", ordinal: 1, body: "x") } #expect(try FileManager.default.contentsOfDirectory(atPath: elsewhere.path(percentEncoded: false)).isEmpty) }
@Test func skillIsMaterializedFromTheBundleDirectory() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let bundle = root.appending(path: "bundle/skills/prowl.adversarial-reviewer", directoryHint: .isDirectory) try FileManager.default.createDirectory(at: bundle, withIntermediateDirectories: true) try "---\nname: reviewer\ndescription: Review.\n---\n# Review\n".write( to: bundle.appending(path: "SKILL.md"), atomically: true, encoding: .utf8) try "checklist".write(to: bundle.appending(path: "checklist.md"), atomically: true, encoding: .utf8) let skill = BundledSkill( id: "prowl.adversarial-reviewer", name: "reviewer", description: "Review.", audience: .workflow, directoryURL: bundle) let store = WorkflowRunStore(rootURL: root) let runID = UUID() try store.ensureLayout(runID: runID) let destination = try store.materializeSkill(runID: runID, skill: skill) #expect( destination == store.directory(for: runID).appending(path: "skills/prowl.adversarial-reviewer", directoryHint: .isDirectory) ) #expect(try String(contentsOf: destination.appending(path: "SKILL.md"), encoding: .utf8).hasSuffix("# Review\n")) #expect( FileManager.default.fileExists(atPath: destination.appending(path: "checklist.md").path(percentEncoded: false))) _ = try store.materializeSkill(runID: runID, skill: skill) let missing = BundledSkill( id: "prowl.nope", name: "n", description: "d", audience: .workflow, directoryURL: root.appending(path: "nope")) #expect(throws: WorkflowRunStoreError.skillMissing("prowl.nope")) { try store.materializeSkill(runID: runID, skill: missing) } }
@Test func interruptedScanFlipsOnlyNonTerminalRunsAndReportsUnreadableOnes() throws { let root = try makeRoot() defer { try? FileManager.default.removeItem(at: root) } let store = WorkflowRunStore(rootURL: root) #expect( try store.markInterruptedRuns(now: { Self.now }) == WorkflowInterruptedRuns(interrupted: [], unreadable: []))
let running = try makeRun(root: root) let attention = try makeRun( root: root, status: .needsAttention( WorkflowAttention(reason: .blocked, stepID: "brief", role: nil, ordinal: nil, actions: [], message: ""))) let cancelled = try makeRun(root: root, status: .cancelled) for run in [running, attention, cancelled] { try store.ensureLayout(runID: run.id) try store.writeRecord(WorkflowRunRecord(run: run)) } let brokenID = UUID() try store.ensureLayout(runID: brokenID) try "{ not json".write( to: store.directory(for: brokenID).appending(path: "run.json"), atomically: true, encoding: .utf8) let later = Self.now.addingTimeInterval(60) let result = try store.markInterruptedRuns(now: { later }) #expect(Set(result.interrupted) == [running.id, attention.id]) #expect( result.unreadable == [store.directory(for: brokenID).appending(path: "run.json").path(percentEncoded: false)]) let flipped = try store.readRecord(runID: running.id) #expect(flipped.run.status.state == "interrupted") #expect(flipped.run.finishedAt == later) #expect(flipped.invocations.count == 1) #expect(try store.readRecord(runID: cancelled.id).run.status.state == "cancelled") let log = try String(contentsOf: store.directory(for: running.id).appending(path: "log.md"), encoding: .utf8) #expect(log.contains("marked interrupted")) #expect(try store.markInterruptedRuns(now: { later }).interrupted.isEmpty) }}
extension String { fileprivate func trimmingSuffix(_ suffix: String) -> String { hasSuffix(suffix) ? String(dropLast(suffix.count)) : self }}