import XCTest @testable import Ten31Transcripts final class SpeakerEditingTests: XCTestCase { private func seg(_ s: Double, _ e: Double, _ who: String, _ t: String) -> SpeakersFile.Segment { .init(start: s, end: e, speaker: who, text: t) } func testReplaceSpeakerRenamesAll() { let segs = [seg(0, 1, "A", "x"), seg(1, 2, "B", "y"), seg(2, 3, "A", "z")] let out = SpeakerEditing.replaceSpeaker("A", with: "Alice", in: segs) XCTAssertEqual(out.map { $0.speaker }, ["Alice", "B", "Alice"]) } func testReplaceSpeakerMergesOntoExisting() { let segs = [seg(0, 1, "A", "x"), seg(1, 2, "B", "y")] let out = SpeakerEditing.replaceSpeaker("B", with: "A", in: segs) // merge B→A XCTAssertEqual(SpeakerEditing.orderedSpeakers(out), ["A"]) } func testReassignSingleSegment() { let segs = [seg(0, 1, "A", "x"), seg(1, 2, "A", "y")] let out = SpeakerEditing.reassign(1, to: "B", in: segs) XCTAssertEqual(out.map { $0.speaker }, ["A", "B"]) } func testNetNameMapComposesChains() { let net = SpeakerEditing.netNameMap(originals: ["A", "B", "C"], ops: [("A", "B"), ("B", "C")]) XCTAssertEqual(net["A"], "C") XCTAssertEqual(net["B"], "C") XCTAssertNil(net["C"]) } func testRemapStructuredAndWordBoundaryText() { let result = RecapResult( sections: [TopicSection(title: "Grant intro", summary: "Grant and Unknown_0 talk; Grantham stays.", startIndex: 0, endIndex: 1)], extras: RecapExtras(tldr: "Grant led.", primarySpeakers: ["Grant"], sections: [ RenderedSection(title: "Decisions", kind: .items, items: [RecapItem(text: "Unknown_0 sends doc", who: "Unknown_0", when: 1, note: nil)]), RenderedSection(title: "Takeaways", kind: .bullets, bullets: ["Unknown_0 and Grant agree"]), ])) let map = ["Unknown_0": "Caitlyn", "Grant": "Grant Gilliam"] let out = SpeakerEditing.remap(result, names: map) XCTAssertEqual(out.sections[0].title, "Grant Gilliam intro") XCTAssertEqual(out.sections[0].summary, "Grant Gilliam and Caitlyn talk; Grantham stays.") // word boundary keeps "Grantham" XCTAssertEqual(out.extras?.tldr, "Grant Gilliam led.") XCTAssertEqual(out.extras?.primarySpeakers, ["Grant Gilliam"]) XCTAssertEqual(out.extras?.sections[0].items.first?.who, "Caitlyn") XCTAssertEqual(out.extras?.sections[0].items.first?.text, "Caitlyn sends doc") XCTAssertEqual(out.extras?.sections[1].bullets, ["Caitlyn and Grant Gilliam agree"]) } @MainActor func testEditModelRenameSavesArtifactsAndLearnsVoice() throws { let dir = FileManager.default.temporaryDirectory.appendingPathComponent("edit_\(UUID().uuidString)") try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: dir) } let speakers = SpeakersFile(sessionId: "s", app: "meet", durationSec: 6, speakers: [.init(name: "Grant", source: "mic_channel", overlapConfidence: nil, matchSimilarity: nil), .init(name: "Unknown_0", source: "unmatched", overlapConfidence: nil, matchSimilarity: nil)], segments: [seg(0, 2, "Grant", "hi"), seg(3, 5, "Unknown_0", "hello there")], models: [:]) try speakers.write(to: dir.appendingPathComponent("speakers.json")) try RecapFile(title: "Meet call", result: RecapResult(sections: [TopicSection(title: "Intro", summary: "Unknown_0 greets the room.", startIndex: 0, endIndex: 1)], extras: nil)) .write(to: dir.appendingPathComponent("recap.json")) try JSONSerialization.data(withJSONObject: ["Unknown_0": [0.5, 0.6], "Grant": [0.1, 0.2]]) .write(to: dir.appendingPathComponent("cluster_fingerprints.json")) let store = VoiceprintStore(fileURL: dir.appendingPathComponent("voiceprints.json")) let model = try XCTUnwrap(RecapEditModel(folder: dir, voiceprints: store, baseURL: "https://localhost:1", skipTLS: true, templates: RecapTemplate.builtIns, defaultTemplateId: RecapTemplate.builtIns.first!.id)) model.rename("Unknown_0", to: "Caitlyn") XCTAssertTrue(model.speakers.contains("Caitlyn")) XCTAssertFalse(model.speakers.contains("Unknown_0")) model.save() let reloaded = try JSONDecoder().decode(SpeakersFile.self, from: Data(contentsOf: dir.appendingPathComponent("speakers.json"))) XCTAssertTrue(reloaded.segments.contains { $0.speaker == "Caitlyn" }) XCTAssertFalse(reloaded.segments.contains { $0.speaker == "Unknown_0" }) XCTAssertTrue(FileManager.default.fileExists(atPath: dir.appendingPathComponent("recap.html").path)) XCTAssertTrue(FileManager.default.fileExists(atPath: dir.appendingPathComponent("transcript.md").path)) // The renamed Unknown taught the store a voice for "Caitlyn". XCTAssertEqual(store.knownVoiceprints()["Caitlyn"], [0.5, 0.6]) // recap.json summary remapped Unknown_0 → Caitlyn. let rf = try XCTUnwrap(RecapFile.read(from: dir.appendingPathComponent("recap.json"))) XCTAssertTrue(rf.result.sections[0].summary.contains("Caitlyn")) } }