import CoreGraphics import Foundation import ImageIO import Testing import ShotdeckCore @Test func recordTwelvePDFsKeepsTenNewestAndDeletesOldestCopies() async throws { let (root, paths) = try makeHistoryPaths() defer { try? FileManager.default.removeItem(at: root) } let store = try HistoryStore(paths: paths) let sources = root.appendingPathComponent("user-sources", isDirectory: true) try FileManager.default.createDirectory(at: sources, withIntermediateDirectories: true) var recorded: [HistoryEntry] = [] for i in 0..<12 { let source = sources.appendingPathComponent("source-\(i).pdf") try writeDummyPDF(to: source, marker: "pdf-\(i)") let sentAt = Date(timeIntervalSince1970: 1_800_000_000 + TimeInterval(i)) let entry = try await store.recordSentPDF( sourceURL: source, sessionID: UUID(), pageCount: i + 1, sentAt: sentAt ) recorded.append(entry) } let listed = await store.listPDFs() #expect(listed.count == 10) #expect(listed.map(\.id) == recorded.suffix(10).reversed().map(\.id)) #expect(listed.map(\.sentAt) == recorded.suffix(10).reversed().map(\.sentAt)) let pdfsDir = paths.root.appendingPathComponent("history/pdfs", isDirectory: true) for entry in recorded.prefix(2) { #expect(!FileManager.default.fileExists(atPath: pdfsDir.appendingPathComponent(entry.fileName).path)) } for entry in recorded.suffix(10) { #expect(FileManager.default.fileExists(atPath: pdfsDir.appendingPathComponent(entry.fileName).path)) } } @Test func pruneImagesKeepsThirtyNewestAcrossThreeArchivedSessionsAndLeavesOpenSessionAlone() async throws { let (root, paths) = try makeHistoryPaths() defer { try? FileManager.default.removeItem(at: root) } let spool = try SpoolStore(paths: paths) let base = Date(timeIntervalSince1970: 1_800_100_000) var captures: [(id: UUID, capturedAt: Date, sessionID: UUID)] = [] // 5 oldest + 15 + 15 = 35 archived PNGs. The oldest session is emptied by prune. let perSession = [5, 15, 15] var index = 0 for count in perSession { let sessionID = try await spool.currentSession().id for _ in 0.. (root: URL, paths: AppSupportPaths) { let root = FileManager.default.temporaryDirectory .appendingPathComponent("shotdeck-history-\(UUID().uuidString)", isDirectory: true) let paths = try AppSupportPaths( root: root.appendingPathComponent("root", isDirectory: true), outbox: root.appendingPathComponent("outbox", isDirectory: true), watchFolder: root.appendingPathComponent("watch", isDirectory: true) ) return (root, paths) } private func writeDummyPDF(to url: URL, marker: String) throws { try Data("%PDF-1.4\n%\(marker)\n%%EOF\n".utf8).write(to: url) } private func makeHistoryPNGData( width: Int, height: Int, red: CGFloat, green: CGFloat, blue: CGFloat ) throws -> Data { let colorSpace = CGColorSpaceCreateDeviceRGB() guard let context = CGContext( data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) else { throw HistoryFixtureError.pngGenerationFailed } context.setFillColor(red: red, green: green, blue: blue, alpha: 1) context.fill(CGRect(x: 0, y: 0, width: width, height: height)) guard let image = context.makeImage() else { throw HistoryFixtureError.pngGenerationFailed } let buffer = NSMutableData() guard let destination = CGImageDestinationCreateWithData(buffer, "public.png" as CFString, 1, nil) else { throw HistoryFixtureError.pngGenerationFailed } CGImageDestinationAddImage(destination, image, nil) guard CGImageDestinationFinalize(destination) else { throw HistoryFixtureError.pngGenerationFailed } return buffer as Data } private func pngFiles(under root: URL) -> [URL] { let fm = FileManager.default guard let enumerator = fm.enumerator( at: root, includingPropertiesForKeys: [.isRegularFileKey], options: [] ) else { return [] } var urls: [URL] = [] for case let url as URL in enumerator { let isFile = (try? url.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) ?? false if isFile, url.pathExtension.lowercased() == "png" { urls.append(url) } } return urls } private func assertManifestsMatchDisk(archiveRoot: URL) throws { let fm = FileManager.default let sessions = (try fm.contentsOfDirectory( at: archiveRoot, includingPropertiesForKeys: [.isDirectoryKey], options: [] )).filter { ((try? $0.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false) && UUID(uuidString: $0.lastPathComponent) != nil } let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 for dir in sessions { let manifestURL = dir.appendingPathComponent("session.json") #expect(fm.fileExists(atPath: manifestURL.path)) let session = try decoder.decode(CaptureSession.self, from: Data(contentsOf: manifestURL)) for capture in session.captures { let url = dir.appendingPathComponent(capture.fileName) #expect(fm.fileExists(atPath: url.path)) } let topLevelPNGs = (try fm.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil, options: [])) .filter { $0.pathExtension.lowercased() == "png" } let manifestNames = Set(session.captures.map(\.fileName)) #expect(Set(topLevelPNGs.map(\.lastPathComponent)) == manifestNames) } } private enum HistoryFixtureError: Error { case pngGenerationFailed }