import XCTest @testable import DSKit /// The contract's "1차 수용 테스트": every fixture must decode through the app's models via the /// shared decoder, with representative VALUE assertions (no-throw alone would miss a silently-missing /// CodingKey). Driven through FixtureDSClient so it exercises client + decoder + models together. final class FixtureDecodeTests: XCTestCase { let client = FixtureDSClient() func testAuthLogin() async throws { let r = try await client.login(username: "x", password: "y", totpCode: nil) XCTAssertEqual(r.tokenType, "bearer") XCTAssertFalse(r.accessToken.isEmpty) } func testAuthMe() async throws { let u = try await client.me() XCTAssertEqual(u.id, 1) XCTAssertEqual(u.username, "hyungi") XCTAssertNotNil(u.lastLoginAt) // fractional-seconds ISO parse } func testDocumentsList() async throws { let r = try await client.documents(DocumentListQuery()) XCTAssertEqual(r.total, 783) XCTAssertEqual(r.items.count, 3) XCTAssertEqual(r.items[2].conversionStatus, "pending") XCTAssertEqual(r.items[0].aiTags?.count, 4) XCTAssertEqual(r.items[1].duplicateCount, 1) XCTAssertEqual(r.items[0].fileFormat, "pdf") XCTAssertEqual(r.items[0].reads, 3) // read_count fallback accessor } func testDocumentDetailCompleted() async throws { let d = try await client.document(id: 4912) XCTAssertEqual(d.mdStatus, "completed") XCTAssertNotNil(d.mdContent) XCTAssertTrue(d.mdIsRenderable) XCTAssertEqual(d.mdExtractionQuality?["page_count"]?.intValue, 14) XCTAssertEqual(d.base.aiInconsistencies?.isEmpty, true) XCTAssertEqual(d.base.title, "ASME Section VIII Div 1 — Impact Test 요건") } func testDocumentDetailPending() async throws { let d = try await client.document(id: 5301) XCTAssertEqual(d.mdStatus, "pending") XCTAssertNil(d.mdContent) XCTAssertFalse(d.mdIsRenderable) XCTAssertNotNil(d.extractedText) XCTAssertNil(d.mdFrontmatter) } func testDocumentContent() async throws { let c = try await client.documentContent(id: 4912) XCTAssertEqual(c.contentLength, 8421) XCTAssertEqual(c.truncated, false) } func testDocumentTree() async throws { let t = try await client.documentTree() XCTAssertEqual(t.count, 6) XCTAssertEqual(t[0].kids.count, 2) XCTAssertEqual(t[0].kids[0].name, "위험성평가") } func testStats() async throws { let s = try await client.categoryCounts() XCTAssertEqual(s.counts["news"], 6182) XCTAssertEqual(s.counts["library"], 391) XCTAssertEqual(s.libraryPendingSuggestions, 0) XCTAssertEqual(s.total, 391 + 229 + 381 + 6182 + 4 + 2) // 파생 접근자 = counts 합 } func testDuplicates() async throws { let d = try await client.duplicates() XCTAssertEqual(d.totalGroups, 2) XCTAssertEqual(d.groups[1].members.count, 3) XCTAssertEqual(d.groups[0].reason, "content_hash") } func testSearch() async throws { let s = try await client.search(q: "충격시험", mode: .hybrid, page: 1, debug: false) XCTAssertEqual(s.results.count, 2) XCTAssertEqual(s.results[0].rerankScore, 0.913) XCTAssertEqual(s.mode, "hybrid") } func testAsk() async throws { let a = try await client.ask(q: "충격시험은 언제 면제되나", limit: nil, backend: nil, debug: false) XCTAssertEqual(a.citations.count, 1) XCTAssertEqual(a.confidence, "high") XCTAssertNil(a.confirmedItems) XCTAssertEqual(a.coveredAspects?.count, 2) XCTAssertEqual(a.backendUsed, "gemma-macmini") } func testMemosList() async throws { let m = try await client.memos(MemoListQuery()) XCTAssertEqual(m.total, 4807) XCTAssertEqual(m.items[0].checkedTaskIndices, [0]) XCTAssertEqual(m.items[1].fileType, "audio") XCTAssertTrue(m.items[1].isAudio) } func testMemoDetail() async throws { let m = try await client.memo(id: 20238) XCTAssertTrue(m.isPinned) XCTAssertNotNil(m.memoTaskState?["0"]?["checked_at"]?.stringValue) } func testDigest() async throws { let d = try await client.digest(date: nil, country: nil) XCTAssertEqual(d.digestDateDisplay, "2026-06-03") // date-only raw display XCTAssertNotNil(d.windowStart) // fractional ISO parse XCTAssertEqual(d.countries.count, 2) XCTAssertEqual(d.countries[0].topics[0].articles.count, 3) } /// JSONValue numeric robustness (B-1 review): whole-valued floats and integers must read correctly /// through the cross-converting accessors regardless of int/double storage. func testJSONValueNumberTrap() throws { let data = Data(#"{"a": 1.0, "b": 0.97, "c": 23, "d": true}"#.utf8) let v = try DSDecoder.make().decode(JSONValue.self, from: data) XCTAssertEqual(v["a"]?.doubleValue, 1.0) XCTAssertEqual(v["b"]?.doubleValue, 0.97) XCTAssertEqual(v["c"]?.intValue, 23) XCTAssertEqual(v["d"]?.boolValue, true) } /// Light round-trip encode (call-shape regression guard): decode -> encode -> decode, compare values. func testAskRoundTrip() async throws { let a = try await client.ask(q: "q", limit: nil, backend: nil, debug: false) let data = try DSEncoder.make().encode(a) let a2 = try DSDecoder.make().decode(AskResponse.self, from: data) XCTAssertEqual(a2.citations.count, a.citations.count) XCTAssertEqual(a2.backendUsed, a.backendUsed) XCTAssertEqual(a2.confidence, a.confidence) } }