import Testing import Foundation @testable import SharedModels /// Invariants that the shipped `course_data.json` must satisfy for the /// Complete the Sentence quiz to work for every card in every course. /// /// These tests read the repo's `course_data.json` from a fixed relative path. /// They act as the pass/fail oracle for the content gap-fill work: they fail /// before the gap-fill pass is complete and pass once every card has at least /// three examples and at least one of them yields a resolvable blank. @Suite("Content coverage — course_data.json") struct ContentCoverageTests { // Repo-relative path from this test file to the bundled data file. // SharedModels/Tests/SharedModelsTests/ContentCoverageTests.swift // → ../../../../Conjuga/course_data.json private static let courseDataPath: String = { let here = URL(fileURLWithPath: #filePath) return here .deletingLastPathComponent() // SharedModelsTests .deletingLastPathComponent() // Tests .deletingLastPathComponent() // SharedModels .deletingLastPathComponent() // Conjuga (repo package parent) .appendingPathComponent("Conjuga/course_data.json") .path }() struct CardRef { let courseName: String let weekNumber: Int let deckTitle: String let front: String let back: String let examples: [[String: String]] } /// Load every card in course_data.json. static func loadAllCards() throws -> [CardRef] { let url = URL(fileURLWithPath: courseDataPath) let data = try Data(contentsOf: url) guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let courses = json["courses"] as? [[String: Any]] else { Issue.record("course_data.json is not in the expected shape") return [] } var cards: [CardRef] = [] for course in courses { let cname = course["course"] as? String ?? "" let weeks = course["weeks"] as? [[String: Any]] ?? [] for week in weeks { let wnum = week["week"] as? Int ?? -1 let decks = week["decks"] as? [[String: Any]] ?? [] for deck in decks { let title = deck["title"] as? String ?? "" let rawCards = deck["cards"] as? [[String: Any]] ?? [] for raw in rawCards { let front = raw["front"] as? String ?? "" let back = raw["back"] as? String ?? "" let examples = (raw["examples"] as? [[String: String]]) ?? [] cards.append(CardRef( courseName: cname, weekNumber: wnum, deckTitle: title, front: front, back: back, examples: examples )) } } } } return cards } private static func vocabCard(from ref: CardRef) -> VocabCard { var exES: [String] = [] var exEN: [String] = [] var exBlanks: [String] = [] for ex in ref.examples { if let es = ex["es"] { exES.append(es) exEN.append(ex["en"] ?? "") exBlanks.append(ex["blank"] ?? "") } } return VocabCard( front: ref.front, back: ref.back, deckId: "\(ref.courseName)_w\(ref.weekNumber)_\(ref.deckTitle)", examplesES: exES, examplesEN: exEN, examplesBlanks: exBlanks ) } @Test("course_data.json exists and parses") func fileExists() throws { let cards = try Self.loadAllCards() #expect(cards.count > 0, "Expected at least one card in course_data.json") } @Test("Every card has at least three example sentences") func everyCardHasThreeExamples() throws { let cards = try Self.loadAllCards() var failures: [String] = [] for ref in cards { if ref.examples.count < 3 { failures.append("[\(ref.courseName) / w\(ref.weekNumber) / \(ref.deckTitle)] '\(ref.front)' has \(ref.examples.count) examples") } } if !failures.isEmpty { let head = Array(failures.prefix(10)).joined(separator: "\n") Issue.record("\(failures.count) cards have fewer than 3 examples. First 10:\n\(head)") } #expect(failures.isEmpty) } @Test("Every card yields a resolvable SentenceQuizEngine question") func everyCardHasBlankableSentence() throws { let cards = try Self.loadAllCards() var failures: [String] = [] for ref in cards { let card = Self.vocabCard(from: ref) if !SentenceQuizEngine.hasValidSentence(for: card) { failures.append("[\(ref.courseName) / w\(ref.weekNumber) / \(ref.deckTitle)] '\(ref.front)'") } } if !failures.isEmpty { let head = Array(failures.prefix(15)).joined(separator: "\n") Issue.record("\(failures.count) cards have no resolvable sentence for Complete the Sentence. First 15:\n\(head)") } #expect(failures.isEmpty) } @Test("Every generated question has a non-empty blank word and display template") func questionIntegrity() throws { let cards = try Self.loadAllCards() var failures: [String] = [] for ref in cards { let card = Self.vocabCard(from: ref) // Try to build a question from each resolvable index deterministically for idx in SentenceQuizEngine.resolvableIndices(for: card) { guard let q = SentenceQuizEngine.buildQuestion(for: card, exampleIndex: idx) else { failures.append("[\(ref.courseName) / '\(ref.front)'] index \(idx) returned nil despite being resolvable") continue } if q.blankWord.isEmpty { failures.append("[\(ref.courseName) / '\(ref.front)'] index \(idx) has empty blankWord") } if !q.displayTemplate.contains(SentenceQuizEngine.blankMarker) { failures.append("[\(ref.courseName) / '\(ref.front)'] index \(idx) display template missing blank marker") } if q.displayTemplate == q.sentenceES { failures.append("[\(ref.courseName) / '\(ref.front)'] index \(idx) display template unchanged from sentence") } } } if !failures.isEmpty { let head = Array(failures.prefix(10)).joined(separator: "\n") Issue.record("\(failures.count) question integrity failures. First 10:\n\(head)") } #expect(failures.isEmpty) } }