7cc38e8a4a
라이브 결선 첫 실로그인에서 decode 실패(Key 'total' not found) 진단:
서버 /documents/stats/category-counts 는 Pydantic response model 없는
raw dict 반환({counts:{category:n}, library_pending_suggestions}) — 초기
계약 추출('실 Pydantic 에서 추출')이 이 엔드포인트에선 shape 을 합성
(total/by_domain/review_pending/pipeline_failed = 실재하지 않음).
- CategoryCounts 모델 = 실측 shape + total 파생 접근자(counts 합)
- fixture 2사본(contract/fixtures + DSKit Resources) = CAPTURED_LIVE 재캡처
- DashboardView 스켈레톤 정합(카테고리 분포 + 한국어 라벨, 본격 재설계는 FU-E)
- CONTRACT.md 해당 행 정정 주석
전 엔드포인트 라이브 shape 전수 대조(토큰 생성 후 11종 curl + shape_diff):
stats 외 진짜 drift 0 — documents/tree·search·memos·digest·auth_me·detail·
content 일치. original_filename/duplicate_* 부재 = S1 미배포(optional 이라
무해, 배포 시 해소) / md_frontmatter·memo_task_state = JSONValue 오픈 shape
데이터 차이(무해) / duplicates 422 = S1 라우트 미배포(예상).
검증: swift test 82/82 + shape_diff (shape identical) + xcodebuild PASS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
140 lines
5.7 KiB
Swift
140 lines
5.7 KiB
Swift
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)
|
|
}
|
|
}
|