import XCTest import Foundation import ForgeColor import ForgeGrade import ForgeLibrary @testable import ForgeExport final class IngestMatchTests: XCTestCase { var mediaDir: String! var dbPath: String! var store: LibraryStore! var day: ForgeLibrary.Day! override func setUpWithError() throws { mediaDir = NSTemporaryDirectory() + "forge-ingest-\(UUID().uuidString)" try FileManager.default.createDirectory(atPath: mediaDir + "/CLIPS", withIntermediateDirectories: true) dbPath = NSTemporaryDirectory() + "forge-ingest-\(UUID().uuidString).db" store = try LibraryStore(path: dbPath) let p = try store.createProject(name: "P") day = try store.createDay(projectID: p.id, label: "Day 03", date: "2026-07-11") } override func tearDown() { try? FileManager.default.removeItem(atPath: mediaDir) try? FileManager.default.removeItem(atPath: dbPath) } func makeShot(clip: String, withGrade: Bool = true) throws { var snapshot: Data? if withGrade { let stack = GradeStack(nodes: [GradeNode(kind: .cdl(CDL( slope: SIMD3(1.2, 1.0, 0.9), offset: SIMD3(0.01, 0, 0), power: .one, saturation: 1.1)))]) snapshot = try GradeSnapshot.encode(stack) } _ = try store.createShot(Shot( dayID: day.id, clipName: clip, cameraSlot: "A-Cam", timecodeIn: "10:00:00:00", timecodeOut: "10:01:00:00", gradeSnapshot: snapshot)) } func makeMedia(_ name: String) throws { try Data("fake clip".utf8).write(to: URL(fileURLWithPath: mediaDir + "/CLIPS/\(name)")) } // Clip file matches shot by basename -> sidecar .cdl + .cube written next to media. func testMatchWritesSidecars() throws { try makeShot(clip: "R001C001") try makeMedia("R001C001.mov") let result = try IngestMatcher.match( mediaRoot: mediaDir, store: store, dayID: day.id, options: IngestMatcher.Options(writeCDL: true, writeCube: true, latticeSize: 17)) XCTAssertEqual(result.matched.count, 1) XCTAssertEqual(result.matched[0].clipName, "R001C001") XCTAssertTrue(FileManager.default.fileExists(atPath: mediaDir + "/CLIPS/R001C001.cdl")) XCTAssertTrue(FileManager.default.fileExists(atPath: mediaDir + "/CLIPS/R001C001.cube")) // CDL sidecar parses back to shot's grade. let cdlText = try String(contentsOfFile: mediaDir + "/CLIPS/R001C001.cdl", encoding: .utf8) let parsed = try CDLExport.parseCDL(cdlText) XCTAssertEqual(parsed.slope.x, 1.2, accuracy: 1e-5) XCTAssertEqual(parsed.saturation, 1.1, accuracy: 1e-5) } // Case-insensitive + extension-agnostic matching. func testMatchIsCaseAndExtensionAgnostic() throws { try makeShot(clip: "R001C002") try makeMedia("r001c002.MXF") let result = try IngestMatcher.match( mediaRoot: mediaDir, store: store, dayID: day.id, options: .init(writeCDL: true, writeCube: false, latticeSize: 17)) XCTAssertEqual(result.matched.count, 1) XCTAssertTrue(FileManager.default.fileExists(atPath: mediaDir + "/CLIPS/r001c002.cdl")) } // Media with no shot record -> unmatched list. func testUnmatchedMediaListed() throws { try makeShot(clip: "R001C003") try makeMedia("R001C003.mov") try makeMedia("R001C099.mov") // no shot let result = try IngestMatcher.match( mediaRoot: mediaDir, store: store, dayID: day.id, options: .init(writeCDL: false, writeCube: false, latticeSize: 17)) XCTAssertEqual(result.matched.count, 1) XCTAssertEqual(result.unmatchedMedia, ["CLIPS/R001C099.mov"]) } // Shot with no media -> missing list. func testShotsWithoutMediaListed() throws { try makeShot(clip: "R001C004") try makeShot(clip: "R001C005") try makeMedia("R001C004.mov") let result = try IngestMatcher.match( mediaRoot: mediaDir, store: store, dayID: day.id, options: .init(writeCDL: false, writeCube: false, latticeSize: 17)) XCTAssertEqual(result.shotsWithoutMedia, ["R001C005"]) } // Shot without grade snapshot matches but writes no sidecars. func testNoGradeNoSidecar() throws { try makeShot(clip: "R001C006", withGrade: false) try makeMedia("R001C006.mov") let result = try IngestMatcher.match( mediaRoot: mediaDir, store: store, dayID: day.id, options: .init(writeCDL: true, writeCube: true, latticeSize: 17)) XCTAssertEqual(result.matched.count, 1) XCTAssertFalse(result.matched[0].sidecarsWritten) XCTAssertFalse(FileManager.default.fileExists(atPath: mediaDir + "/CLIPS/R001C006.cdl")) } // Non-media files ignored. func testNonMediaIgnored() throws { try makeShot(clip: "R001C007") try makeMedia("R001C007.mov") try Data("x".utf8).write(to: URL(fileURLWithPath: mediaDir + "/CLIPS/notes.txt")) try Data("x".utf8).write(to: URL(fileURLWithPath: mediaDir + "/manifest.mhl")) let result = try IngestMatcher.match( mediaRoot: mediaDir, store: store, dayID: day.id, options: .init(writeCDL: false, writeCube: false, latticeSize: 17)) XCTAssertEqual(result.matched.count, 1) XCTAssertTrue(result.unmatchedMedia.isEmpty) } // Existing sidecar not overwritten unless force. func testExistingSidecarPreserved() throws { try makeShot(clip: "R001C008") try makeMedia("R001C008.mov") try Data("existing".utf8).write(to: URL(fileURLWithPath: mediaDir + "/CLIPS/R001C008.cdl")) _ = try IngestMatcher.match( mediaRoot: mediaDir, store: store, dayID: day.id, options: .init(writeCDL: true, writeCube: false, latticeSize: 17)) let content = try String(contentsOfFile: mediaDir + "/CLIPS/R001C008.cdl", encoding: .utf8) XCTAssertEqual(content, "existing") _ = try IngestMatcher.match( mediaRoot: mediaDir, store: store, dayID: day.id, options: .init(writeCDL: true, writeCube: false, latticeSize: 17, force: true)) let after = try String(contentsOfFile: mediaDir + "/CLIPS/R001C008.cdl", encoding: .utf8) XCTAssertTrue(after.contains("ColorDecisionList")) } }