diff --git a/Package.swift b/Package.swift index e6b9706..aceafb4 100644 --- a/Package.swift +++ b/Package.swift @@ -28,5 +28,9 @@ let package = Package( ]), ] ), + .testTarget( + name: "quillTests", + dependencies: ["quill"] + ), ] ) diff --git a/Sources/quill/Transcription/TranscriptionCoordinator.swift b/Sources/quill/Transcription/TranscriptionCoordinator.swift index 5300fbd..67d145d 100644 --- a/Sources/quill/Transcription/TranscriptionCoordinator.swift +++ b/Sources/quill/Transcription/TranscriptionCoordinator.swift @@ -231,7 +231,7 @@ private struct SessionMeta { /// Canonical transcript. Property names are the JSON schema — this struct /// exists to be serialized. -private struct Transcript: Codable { +struct Transcript: Codable { struct Segment: Codable { let speaker: String let start_ms: Int @@ -244,16 +244,19 @@ private struct Transcript: Codable { let created_at: String let segments: [Segment] - /// Write transcript.json and render transcript.md. Both writes are atomic - /// (temp file + rename), so a partially written transcript never exists on - /// disk — resumePending treats presence of transcript.json as "done". + /// Render transcript.md, then write transcript.json as the completion + /// marker. Both writes are atomic (temp file + rename), and writing JSON + /// last ensures resumePending retries if either artifact fails. func write(to dir: URL) throws { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - try encoder.encode(self) - .write(to: dir.appendingPathComponent("transcript.json"), options: .atomic) - try Data(rendered(title: dir.lastPathComponent).utf8) + let json = try encoder.encode(self) + let markdown = Data(rendered(title: dir.lastPathComponent).utf8) + + try markdown .write(to: dir.appendingPathComponent("transcript.md"), options: .atomic) + try json + .write(to: dir.appendingPathComponent("transcript.json"), options: .atomic) } private func rendered(title: String) -> String { diff --git a/Tests/quillTests/Transcription/TranscriptTests.swift b/Tests/quillTests/Transcription/TranscriptTests.swift new file mode 100644 index 0000000..1829587 --- /dev/null +++ b/Tests/quillTests/Transcription/TranscriptTests.swift @@ -0,0 +1,40 @@ +import Foundation +import Testing + +@testable import quill + +struct TranscriptTests { + @Test("A failed Markdown write does not leave the completion marker") + func failedMarkdownWriteDoesNotLeaveCompletionMarker() throws { + let fileManager = FileManager.default + let session = fileManager.temporaryDirectory + .appendingPathComponent("quill-\(UUID().uuidString)", isDirectory: true) + try fileManager.createDirectory(at: session, withIntermediateDirectories: true) + defer { try? fileManager.removeItem(at: session) } + + try fileManager.createDirectory( + at: session.appendingPathComponent("transcript.md", isDirectory: true), + withIntermediateDirectories: true + ) + + let transcript = Transcript( + engine: "parakeet", + model: "test-model", + created_at: "2026-07-28T00:00:00Z", + segments: [] + ) + + do { + try transcript.write(to: session) + Issue.record("Expected writing transcript.md over a directory to fail.") + } catch { + // Expected: transcript.md is a directory. + } + + let completionMarker = session.appendingPathComponent("transcript.json") + #expect( + fileManager.fileExists(atPath: completionMarker.path) == false, + "A failed transcript.md write must leave the session pending." + ) + } +}