import XCTest import Foundation @testable import ForgeProxy final class WatchFolderTests: XCTestCase { var dir: String! override func setUpWithError() throws { dir = NSTemporaryDirectory() + "forge-watch-\(UUID().uuidString)" try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true) } override func tearDown() { try? FileManager.default.removeItem(atPath: dir) } // New stable file enqueued exactly once. func testNewFileEnqueuedOnce() async throws { let watcher = WatchFolder( path: dir, extensions: ["mov"], pollInterval: .milliseconds(30), stabilityChecks: 2) var enqueued = [String]() let stream = await watcher.start() try Data("clip".utf8).write(to: URL(fileURLWithPath: dir + "/C001.mov")) let collector = Task { for await path in stream { enqueued.append(path) } } try await Task.sleep(for: .milliseconds(400)) await watcher.stop() _ = await collector.result XCTAssertEqual(enqueued.count, 1) XCTAssertTrue(enqueued[0].hasSuffix("C001.mov")) } // Growing file (partial write) not enqueued until size stable. func testGrowingFileWaitsForStability() async throws { let watcher = WatchFolder( path: dir, extensions: ["mov"], pollInterval: .milliseconds(40), stabilityChecks: 2) let path = dir + "/GROW.mov" try Data("start".utf8).write(to: URL(fileURLWithPath: path)) let stream = await watcher.start() actor Seen { var paths = [String]() var at: [ContinuousClock.Instant] = [] func add(_ p: String) { paths.append(p) at.append(.now) } } let seen = Seen() let collector = Task { for await p in stream { await seen.add(p) } } // Keep growing for ~200ms. for i in 0..<5 { try await Task.sleep(for: .milliseconds(40)) let h = FileHandle(forWritingAtPath: path)! h.seekToEndOfFile() h.write(Data("more\(i)".utf8)) try h.close() } let growEnd = ContinuousClock.now try await Task.sleep(for: .milliseconds(400)) await watcher.stop() _ = await collector.result let paths = await seen.paths let times = await seen.at XCTAssertEqual(paths.count, 1) // Enqueue happened after growth stopped. XCTAssertGreaterThan(times[0], growEnd) } // Extension filter. func testExtensionFilter() async throws { let watcher = WatchFolder(path: dir, extensions: ["mov"], pollInterval: .milliseconds(30), stabilityChecks: 1) try Data("x".utf8).write(to: URL(fileURLWithPath: dir + "/note.txt")) try Data("y".utf8).write(to: URL(fileURLWithPath: dir + "/C002.mov")) let stream = await watcher.start() var enqueued = [String]() let collector = Task { for await p in stream { enqueued.append(p) } } try await Task.sleep(for: .milliseconds(300)) await watcher.stop() _ = await collector.result XCTAssertEqual(enqueued.count, 1) XCTAssertTrue(enqueued[0].hasSuffix(".mov")) } // Already-processed file skipped across watcher runs via skip list. func testSkipListPersistsAcrossRuns() async throws { try Data("x".utf8).write(to: URL(fileURLWithPath: dir + "/C003.mov")) let skipList = ProcessedSkipList() let w1 = WatchFolder(path: dir, extensions: ["mov"], pollInterval: .milliseconds(30), stabilityChecks: 1, skipList: skipList) let s1 = await w1.start() var first = [String]() let c1 = Task { for await p in s1 { first.append(p) } } try await Task.sleep(for: .milliseconds(250)) await w1.stop() _ = await c1.result XCTAssertEqual(first.count, 1) // Second watcher with same skip list: nothing new. let w2 = WatchFolder(path: dir, extensions: ["mov"], pollInterval: .milliseconds(30), stabilityChecks: 1, skipList: skipList) let s2 = await w2.start() var second = [String]() let c2 = Task { for await p in s2 { second.append(p) } } try await Task.sleep(for: .milliseconds(250)) await w2.stop() _ = await c2.result XCTAssertTrue(second.isEmpty) } }