import XCTest @testable import Ten31Transcripts final class RecapTests: XCTestCase { private func entry(_ off: Double, _ end: Double, _ who: String, _ text: String) -> RecapAnalyzer.Entry { .init(offset: off, end: end, speaker: who, text: text) } // MARK: - Parsing func testParseSectionsHandlesStringIndices() { let json = #"{"sections":[{"title":"Intro","summary":"hi","startIndex":"0","endIndex":3},{"title":"Topic","summary":"x","startIndex":4,"endIndex":9}]}"# let secs = RecapAnalyzer.parseSections(json) XCTAssertEqual(secs.count, 2) XCTAssertEqual(secs[0].title, "Intro") XCTAssertEqual(secs[0].startIndex, 0) XCTAssertEqual(secs[1].endIndex, 9) } func testParseSectionsStripsCodeFence() { let json = "```json\n{\"sections\":[{\"title\":\"A\",\"summary\":\"\",\"startIndex\":0,\"endIndex\":1}]}\n```" XCTAssertEqual(RecapAnalyzer.parseSections(json).count, 1) } func testParseExtras() { let json = #""" {"tldr":{"summary":"They discussed the roadmap.","primary_speakers":["Grant","Caitlyn"]}, "decisions":[{"statement":"Ship dual-channel","agreed_by":["Grant"],"supporting_offset":72}], "action_items":[{"description":"Send the doc","owner":"Caitlyn","due_hint":"by Friday","supporting_offset":120}], "open_questions":[{"question":"What about Teams?","raised_by":"Grant","answered":false}], "key_quotes":[{"speaker":"Caitlyn","offset":73,"quote":"Go Bitcoin","why_notable":"sets the tone"}]} """# let x = RecapAnalyzer.parseExtras(json) XCTAssertNotNil(x) XCTAssertEqual(x?.tldr.primarySpeakers, ["Grant", "Caitlyn"]) XCTAssertEqual(x?.decisions.first?.supportingOffset, 72) XCTAssertEqual(x?.actionItems.first?.owner, "Caitlyn") XCTAssertEqual(x?.actionItems.first?.dueHint, "by Friday") XCTAssertEqual(x?.openQuestions.first?.question, "What about Teams?") XCTAssertEqual(x?.keyQuotes.first?.quote, "Go Bitcoin") } func testParseExtrasDropsNullStrings() { // owner/raised_by "null" or empty must become nil, not a literal "null". let json = #"{"tldr":{"summary":"s","primary_speakers":[]},"action_items":[{"description":"do it","owner":"null","due_hint":""}],"decisions":[],"open_questions":[],"key_quotes":[]}"# let x = RecapAnalyzer.parseExtras(json) XCTAssertNil(x?.actionItems.first?.owner) XCTAssertNil(x?.actionItems.first?.dueHint) } // MARK: - Stitch / windows func testStitchDropsContainedAndTrimsOverlap() { let secs = [ TopicSection(title: "A", summary: "", startIndex: 0, endIndex: 5), TopicSection(title: "B-contained", summary: "", startIndex: 2, endIndex: 4), TopicSection(title: "C-overlap", summary: "", startIndex: 4, endIndex: 9), ] let out = RecapAnalyzer.stitch(secs) XCTAssertEqual(out.map { $0.title }, ["A", "C-overlap"]) XCTAssertEqual(out[0].startIndex, 0); XCTAssertEqual(out[0].endIndex, 5) XCTAssertEqual(out[1].startIndex, 6); XCTAssertEqual(out[1].endIndex, 9) // trimmed front } func testPlanWindowsSingleForShortCall() { let entries = (0..<10).map { entry(Double($0 * 10), Double($0 * 10 + 5), "A", "x") } // ~100s let w = RecapAnalyzer.planWindows(entries) XCTAssertEqual(w.count, 1) XCTAssertEqual(w[0].startIdx, 0); XCTAssertEqual(w[0].endIdx, 9) } func testPlanWindowsMultipleForLongCall() { // 40 entries at 60s spacing → ~39 min, over the 25-min cutoff. let entries = (0..<40).map { entry(Double($0 * 60), Double($0 * 60 + 30), "A", "x") } let w = RecapAnalyzer.planWindows(entries) XCTAssertGreaterThan(w.count, 1) XCTAssertEqual(w.first?.startIdx, 0) XCTAssertEqual(w.last?.endIdx, 39) // last window reaches the end for i in 1..")) XCTAssertTrue(html.contains("Go Bitcoin")) } }