diff --git a/AIStudyApp/AIStudyAppTests/MaterialPathResolverTests.swift b/AIStudyApp/AIStudyAppTests/MaterialPathResolverTests.swift index a9db5af..9850aff 100644 --- a/AIStudyApp/AIStudyAppTests/MaterialPathResolverTests.swift +++ b/AIStudyApp/AIStudyAppTests/MaterialPathResolverTests.swift @@ -3,12 +3,38 @@ import XCTest final class MaterialPathResolverTests: XCTestCase { - // MARK: - materialTypeFrom (infer from apiType + title) + // MARK: - resolveKnowledgeSource path - func test_materialTypeFrom_knowledge_source_with_md_title_returns_markdown() { - let result = MaterialPathResolver.resolve(materialId: "m1", apiType: "knowledge_source", title: "note.md") - // Path won't exist, but we test the type inference via the file system cascade - XCTAssertNil(result) // File doesn't exist + func test_resolveKnowledgeSource_finds_file_in_knowledge_sources_dir() { + let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let sourceId = "ks-test-\(UUID().uuidString)" + let ksDir = docs.appendingPathComponent("knowledge_sources/\(sourceId)") + try? FileManager.default.createDirectory(at: ksDir, withIntermediateDirectories: true) + let fileURL = ksDir.appendingPathComponent("content.md") + defer { + try? FileManager.default.removeItem(at: fileURL) + try? FileManager.default.removeItem(at: ksDir) + } + + FileManager.default.createFile(atPath: fileURL.path, contents: "test".data(using: .utf8)) + + let result = MaterialPathResolver.resolve(materialId: sourceId, apiType: "knowledge_source", title: "content.md") + XCTAssertNotNil(result) + XCTAssertEqual(result?.filePath, fileURL.path) + XCTAssertEqual(result?.materialType, .markdown) + } + + func test_resolveKnowledgeSource_returns_nil_for_empty_dir() { + let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let sourceId = "ks-empty-\(UUID().uuidString)" + let ksDir = docs.appendingPathComponent("knowledge_sources/\(sourceId)") + try? FileManager.default.createDirectory(at: ksDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: ksDir) } + + // Empty directory → no files → falls through to Documents/Caches + let result = MaterialPathResolver.resolve(materialId: sourceId, apiType: "knowledge_source", title: "note.md") + // Directory exists but is empty → resolveKnowledgeSource returns nil → falls through + XCTAssertNil(result) // No file in Documents or Caches either } // MARK: - resolve returns nil for nonexistent file @@ -26,11 +52,9 @@ final class MaterialPathResolverTests: XCTestCase { func test_resolve_finds_file_in_documents() { let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - let fileId = "test-resolve-\(UUID().uuidString)" + let fileId = "test-docs-\(UUID().uuidString)" let fileURL = docs.appendingPathComponent(fileId) defer { try? FileManager.default.removeItem(at: fileURL) } - - // Create a temp file in Documents FileManager.default.createFile(atPath: fileURL.path, contents: "test".data(using: .utf8)) let result = MaterialPathResolver.resolve(materialId: fileId, apiType: nil, title: nil) @@ -45,7 +69,6 @@ final class MaterialPathResolverTests: XCTestCase { let fileId = "test-cache-\(UUID().uuidString)" let fileURL = cache.appendingPathComponent(fileId) defer { try? FileManager.default.removeItem(at: fileURL) } - FileManager.default.createFile(atPath: fileURL.path, contents: "test".data(using: .utf8)) let result = MaterialPathResolver.resolve(materialId: fileId, apiType: nil, title: nil) @@ -53,28 +76,25 @@ final class MaterialPathResolverTests: XCTestCase { XCTAssertEqual(result?.filePath, fileURL.path) } - // MARK: - materialType inference via file cascade + // MARK: - materialType inference from title extension - func test_resolve_infers_type_from_title_extension() { + func test_resolve_infers_markdown_from_title() { let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - let fileId = "test-type-\(UUID().uuidString)" + let fileId = "test-md-\(UUID().uuidString)" let fileURL = docs.appendingPathComponent(fileId) defer { try? FileManager.default.removeItem(at: fileURL) } - FileManager.default.createFile(atPath: fileURL.path, contents: "test".data(using: .utf8)) - // .md title → markdown - let result = MaterialPathResolver.resolve(materialId: fileId, apiType: "knowledge_source", title: "doc.md") + let result = MaterialPathResolver.resolve(materialId: fileId, apiType: "temporary_file", title: "note.md") XCTAssertNotNil(result) XCTAssertEqual(result?.materialType, .markdown) } - func test_resolve_pdf_title_infers_pdf_type() { + func test_resolve_infers_pdf_from_title() { let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let fileId = "test-pdf-\(UUID().uuidString)" let fileURL = docs.appendingPathComponent(fileId) defer { try? FileManager.default.removeItem(at: fileURL) } - FileManager.default.createFile(atPath: fileURL.path, contents: "test".data(using: .utf8)) let result = MaterialPathResolver.resolve(materialId: fileId, apiType: "temporary_file", title: "book.pdf") @@ -82,12 +102,49 @@ final class MaterialPathResolverTests: XCTestCase { XCTAssertEqual(result?.materialType, .pdf) } - func test_resolve_unknown_apiType_defaults_to_unknown_materialType() { + func test_resolve_infers_text_from_title() { let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - let fileId = "test-unknown-\(UUID().uuidString)" + let fileId = "test-txt-\(UUID().uuidString)" let fileURL = docs.appendingPathComponent(fileId) defer { try? FileManager.default.removeItem(at: fileURL) } + FileManager.default.createFile(atPath: fileURL.path, contents: "test".data(using: .utf8)) + let result = MaterialPathResolver.resolve(materialId: fileId, apiType: "knowledge_source", title: "notes.txt") + XCTAssertNotNil(result) + XCTAssertEqual(result?.materialType, .text) + } + + func test_resolve_infers_epub_from_title() { + let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let fileId = "test-epub-\(UUID().uuidString)" + let fileURL = docs.appendingPathComponent(fileId) + defer { try? FileManager.default.removeItem(at: fileURL) } + FileManager.default.createFile(atPath: fileURL.path, contents: "test".data(using: .utf8)) + + let result = MaterialPathResolver.resolve(materialId: fileId, apiType: "temporary_file", title: "book.epub") + XCTAssertNotNil(result) + XCTAssertEqual(result?.materialType, .epub) + } + + func test_resolve_infers_image_from_title() { + let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let fileId = "test-img-\(UUID().uuidString)" + let fileURL = docs.appendingPathComponent(fileId) + defer { try? FileManager.default.removeItem(at: fileURL) } + FileManager.default.createFile(atPath: fileURL.path, contents: "test".data(using: .utf8)) + + for ext in ["png", "jpg", "jpeg", "webp"] { + let r = MaterialPathResolver.resolve(materialId: fileId, apiType: "knowledge_source", title: "photo.\(ext)") + XCTAssertNotNil(r) + XCTAssertEqual(r?.materialType, .image, "ext .\(ext) should infer .image") + } + } + + func test_resolve_unknown_apiType_defaults_to_unknown() { + let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let fileId = "test-unk-\(UUID().uuidString)" + let fileURL = docs.appendingPathComponent(fileId) + defer { try? FileManager.default.removeItem(at: fileURL) } FileManager.default.createFile(atPath: fileURL.path, contents: "test".data(using: .utf8)) let result = MaterialPathResolver.resolve(materialId: fileId, apiType: "other_type", title: "file.bin")