native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443import Foundationimport ProwlCLISharedimport Testing
@testable import Prowl
struct HandoffStoreTests { init() async throws { _ = try await GitExecutableResolver.shared.resolve() }
// MARK: - Helpers
private func makeTempRoot() throws -> URL { let url = FileManager.default.temporaryDirectory .appending(path: "handoff-tests", directoryHint: .isDirectory) .appending(path: UUID().uuidString, directoryHint: .isDirectory) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) return url }
private func remove(_ url: URL) { try? FileManager.default.removeItem(at: url) }
private func runGit(_ arguments: [String], in directory: URL) throws -> String { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/git") process.arguments = ["-C", directory.path(percentEncoded: false)] + arguments let stdout = Pipe() let stderr = Pipe() process.standardOutput = stdout process.standardError = stderr try process.run() let output = stdout.fileHandleForReading.readDataToEndOfFile() let errorOutput = stderr.fileHandleForReading.readDataToEndOfFile() process.waitUntilExit() guard process.terminationStatus == 0 else { throw NSError( domain: "HandoffStoreTests.Git", code: Int(process.terminationStatus), userInfo: [NSLocalizedDescriptionKey: String(data: errorOutput, encoding: .utf8) ?? "git failed"] ) } return String(data: output, encoding: .utf8) ?? "" }
private let fixedDate = Date(timeIntervalSince1970: 1_760_000_000)
private let validBriefing = """ # Handoff
## Objective Ship it.
## Current State Green.
## Next Steps 1. Review. """
// MARK: - parseShortstat (pure)
@Test func parseShortstatExtractsInsertionsAndDeletions() { let (insertions, deletions) = HandoffStore.parseShortstat( " 3 files changed, 120 insertions(+), 14 deletions(-)" ) #expect(insertions == 120) #expect(deletions == 14) }
@Test func parseShortstatHandlesInsertionsOnly() { let (insertions, deletions) = HandoffStore.parseShortstat(" 1 file changed, 5 insertions(+)") #expect(insertions == 5) #expect(deletions == 0) }
@Test func parseShortstatHandlesEmpty() { let (insertions, deletions) = HandoffStore.parseShortstat("") #expect(insertions == 0) #expect(deletions == 0) }
// MARK: - validatedBriefing (pure)
@Test func validatedBriefingAcceptsPlainDocument() { #expect(HandoffStore.validatedBriefing(from: validBriefing) == validBriefing + "\n") }
@Test func validatedBriefingUnwrapsCodeFenceAndDropsPreamble() { let reply = """ Sure! Here's the updated handoff:
```markdown # Handoff
## Objective Ship it.
## Current State Green.
## Next Steps 1. Review. ``` """ let artifact = HandoffStore.validatedBriefing(from: reply) #expect(artifact?.hasPrefix("# Handoff") == true) #expect(artifact?.contains("```") == false) #expect(artifact?.contains("Sure!") == false) }
@Test func validatedBriefingDropsChatterAfterTheClosingFence() { let reply = """ ```markdown # Handoff
## Objective Ship the HUD.
## Current State Done, pending review.
## Next Steps 1. Address review feedback. ```
Let me know if you need anything else! """ let artifact = HandoffStore.validatedBriefing(from: reply) #expect(artifact?.hasSuffix("1. Address review feedback.\n") == true) #expect(artifact?.contains("Let me know") == false) }
@Test func validatedBriefingKeepsEmbeddedCodeBlocksIntact() { // A fence-wrapped reply whose document embeds its own code block: the // embedded pair closes before the wrapper's closing fence, so the last // fence line is the correct cut point and the block survives. let reply = """ ```markdown # Handoff
## Objective Ship the HUD.
## Current State Done.
## Next Steps 1. Run: ``` make test ``` ``` """ let artifact = HandoffStore.validatedBriefing(from: reply) #expect(artifact?.contains("make test") == true) }
@Test func validatedBriefingNeverCutsAtAFenceInsideAnUnwrappedDocument() { // No wrapper fence: an embedded code block's fence lines are body // content, never a truncation point. let reply = """ # Handoff
## Objective Ship the HUD.
## Current State Done.
## Next Steps 1. Run: ``` make test ``` 2. Push the branch. """ let artifact = HandoffStore.validatedBriefing(from: reply) #expect(artifact?.contains("make test") == true) #expect(artifact?.contains("2. Push the branch.") == true) }
@Test func validatedBriefingRejectsUnusableText() { #expect(HandoffStore.validatedBriefing(from: "") == nil) #expect(HandoffStore.validatedBriefing(from: "I could not update the file.") == nil) // Missing required sections. #expect(HandoffStore.validatedBriefing(from: "# Handoff\n\n## Objective\nOnly this.") == nil) }
// MARK: - writeBriefing
@Test func writeBriefingCreatesCurrentArtifact() throws { let root = try makeTempRoot() defer { remove(root) } let store = HandoffStore(rootURL: root)
try store.writeBriefing(validBriefing + "\n", now: fixedDate)
let content = try String(contentsOf: store.currentURL, encoding: .utf8) #expect(content == validBriefing + "\n") // First-ever write: nothing to archive. let archived = try FileManager.default.contentsOfDirectory( at: store.archiveDirectory, includingPropertiesForKeys: nil ) #expect(archived.isEmpty) }
@Test func writeBriefingArchivesReplacedArtifact() throws { let root = try makeTempRoot() defer { remove(root) } let store = HandoffStore(rootURL: root) let previous = "# Handoff\n\n## Objective\nEarlier notes worth keeping.\n" try FileManager.default.createDirectory(at: store.handoffDirectory, withIntermediateDirectories: true) try previous.write(to: store.currentURL, atomically: true, encoding: .utf8)
try store.writeBriefing(validBriefing + "\n", now: fixedDate)
let archived = try FileManager.default.contentsOfDirectory( at: store.archiveDirectory, includingPropertiesForKeys: nil ) let backup = try #require(archived.first { $0.lastPathComponent.contains("-replaced-current") }) #expect(archived.count == 1) #expect(try String(contentsOf: backup, encoding: .utf8) == previous) #expect(try String(contentsOf: store.currentURL, encoding: .utf8) == validBriefing + "\n") }
// MARK: - layout
@Test func layoutSelfIgnoresHandoffInGitRepository() throws { let root = try makeTempRoot() defer { remove(root) } _ = try runGit(["init", "--quiet"], in: root) let store = HandoffStore(rootURL: root)
try store.ensureLayout()
let ignore = try String(contentsOf: store.ignoreURL, encoding: .utf8) #expect(ignore == "*\n") #expect(try runGit(["status", "--porcelain"], in: root).isEmpty) }
@Test func layoutNeverSeedsCurrentArtifact() throws { // current.md exists iff a validated briefing produced it — no template. let root = try makeTempRoot() defer { remove(root) } let store = HandoffStore(rootURL: root)
try store.ensureLayout() _ = try store.save(outgoingAgent: "codex", note: nil, now: fixedDate)
#expect(!store.hasCurrentArtifact) }
// MARK: - save (filesystem; non-git root)
@Test func saveKeepsNonASCIIChangedFileNamesReadable() throws { let root = try makeTempRoot() defer { remove(root) } _ = try runGit(["init", "--quiet"], in: root) try "draft".write(to: root.appending(path: "交接记录.md"), atomically: true, encoding: .utf8) let store = HandoffStore(rootURL: root)
let result = try store.save(outgoingAgent: "codex", note: nil, now: fixedDate)
#expect(result.changedFiles.contains { $0.contains("交接记录.md") }) let context = try String(contentsOf: store.contextURL, encoding: .utf8) #expect(context.contains("交接记录.md")) }
@Test func saveWritesAppendixForNonGitRoot() throws { let root = try makeTempRoot() defer { remove(root) } let store = HandoffStore(rootURL: root)
let result = try store.save(outgoingAgent: "codex", note: "wip", now: fixedDate)
let context = try String(contentsOf: store.contextURL, encoding: .utf8) #expect(context.contains("Outgoing agent (detected): codex")) #expect(context.contains("(not a git repo)")) #expect(result.outgoingAgent == "codex") // A non-git root contributes no git repos summary count. #expect(result.totalChangedFiles == 0) }
@Test func saveWritesSessionContextExcerpt() throws { let root = try makeTempRoot() defer { remove(root) } let store = HandoffStore(rootURL: root) let sessionContext = HandoffStore.SessionContext( agent: "codex", sessionID: "codex-session", paneID: "pane-123", paneTitle: "codex", source: "terminal-scrollback", confidence: "fallback", transcriptPath: "/tmp/codex.jsonl", excerptText: "Implemented handoff session context.\nNext: run tests." )
let result = try store.save( outgoingAgent: "codex", sessionContext: sessionContext, note: nil, now: fixedDate )
let session = try #require(result.sessionContext) #expect(session.excerptPath?.hasPrefix("handoff/sessions/") == true) let excerptPath = try #require(session.excerptPath?.replacing("handoff/", with: "")) let excerptURL = store.handoffDirectory.appending(path: excerptPath) let excerpt = try String(contentsOf: excerptURL, encoding: .utf8) #expect(excerpt.contains("# Handoff Session Context")) #expect(excerpt.contains("Session ID: codex-session")) #expect(excerpt.contains("Native transcript: /tmp/codex.jsonl")) #expect(excerpt.contains("Implemented handoff session context."))
let context = try String(contentsOf: store.contextURL, encoding: .utf8) #expect(context.contains("Session Context:")) #expect(context.contains("Session ID: codex-session")) #expect(context.contains(".prowl/handoff/sessions/")) }
@Test func saveWritesUniqueSessionContextExcerptsForSameTimestamp() throws { let root = try makeTempRoot() defer { remove(root) } let store = HandoffStore(rootURL: root) let sessionContext = HandoffStore.SessionContext( agent: "codex", paneID: "pane-123", paneTitle: "codex", source: "terminal-scrollback", confidence: "fallback", excerptText: "first excerpt" )
let first = try store.save( outgoingAgent: "codex", sessionContext: sessionContext, note: nil, now: fixedDate ) let second = try store.save( outgoingAgent: "codex", sessionContext: sessionContext, note: nil, now: fixedDate )
let firstPath = try #require(first.sessionContext?.excerptPath) let secondPath = try #require(second.sessionContext?.excerptPath) #expect(firstPath != secondPath) #expect(secondPath.hasSuffix("-2.md"))
let firstURL = store.handoffDirectory.appending( path: firstPath.replacingOccurrences(of: "handoff/", with: "") ) let secondURL = store.handoffDirectory.appending( path: secondPath.replacingOccurrences(of: "handoff/", with: "") ) #expect(FileManager.default.fileExists(atPath: firstURL.path(percentEncoded: false))) #expect(FileManager.default.fileExists(atPath: secondURL.path(percentEncoded: false))) } @Test func reserveFileURLIsExclusiveAcrossConcurrentCalls() async throws { let root = try makeTempRoot() defer { remove(root) } let directory = root.appending(path: "sessions", directoryHint: .isDirectory) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let reserved = try await withThrowingTaskGroup(of: URL.self, returning: [URL].self) { group in for _ in 0..<40 { group.addTask { try HandoffStore.reserveFileURL(in: directory, stem: "same", fileExtension: "md") } } var urls: [URL] = [] for try await url in group { urls.append(url) } return urls }
#expect(Set(reserved).count == 40) #expect(reserved.allSatisfy { FileManager.default.fileExists(atPath: $0.path(percentEncoded: false)) }) }
@Test func saveTwiceReplacesGeneratedContext() throws { let root = try makeTempRoot() defer { remove(root) } let store = HandoffStore(rootURL: root)
_ = try store.save(outgoingAgent: "codex", note: nil, now: fixedDate) _ = try store.save(outgoingAgent: "claude", note: nil, now: fixedDate)
let content = try String(contentsOf: store.contextURL, encoding: .utf8) #expect(content.contains("Outgoing agent (detected): claude")) #expect(!content.contains("Outgoing agent (detected): codex")) }
@Test func saveNeverTouchesCurrentArtifact() throws { let root = try makeTempRoot() defer { remove(root) } let store = HandoffStore(rootURL: root) try store.writeBriefing(validBriefing + "\n", now: fixedDate)
_ = try store.save(outgoingAgent: "claude", note: nil, now: fixedDate)
let updated = try String(contentsOf: store.currentURL, encoding: .utf8) #expect(updated == validBriefing + "\n") let context = try String(contentsOf: store.contextURL, encoding: .utf8) #expect(context.contains("Outgoing agent (detected): claude")) }
// MARK: - Log
@Test func appendLogPreservesConcurrentEntries() async throws { let root = try makeTempRoot() defer { remove(root) } let store = HandoffStore(rootURL: root) let date = fixedDate
try await withThrowingTaskGroup(of: Void.self) { group in for index in 0..<40 { group.addTask { try store.appendLog("event=\(index)", now: date) } } try await group.waitForAll() }
let log = try String(contentsOf: store.logURL, encoding: .utf8) let entries = log.split(separator: "\n").filter { $0.hasPrefix("- ") } #expect(entries.count == 40) for index in 0..<40 { #expect(entries.contains { $0.contains("event=\(index)") }) } }
}