From 78cc7d5b1a8255732b2d3cda47752341e14690dd Mon Sep 17 00:00:00 2001 From: kua-agent Date: Sat, 5 Sep 2026 09:01:21 +0400 Subject: [PATCH] test: OneDrive transport settings/locator unit tests + ReturnWatcher recordUncommended coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TransportSettingsTests: default airDrop, set/get round-trip, garbage stored value falls back to airDrop, oneDriveFolder store/reset, effectiveFolders for both transports, oneDriveFolderUnavailable's errorDescription contains the path. OneDriveLocatorTests: fake home tree under Library/CloudStorage — syncRoots returns only real OneDrive-* directories (ignores a same-named plain file and a GoogleDrive-* one), MMD-named root sorts first; no CloudStorage dir means empty roots and a nil defaultRedlineFolder; resolveOneDriveFolder prefers an existing stored override and falls back to the default when the stored path no longer exists. All against temp dirs, never the real home. ReturnWatcherTests: recordUncommended defaults to true and still records an unmarked PDF (existing AirDrop tests are unaffected); with it set false, an unmarked PDF is neither recorded nor returned by scanNow, and marking it up in place with a real PDFKit ink annotation then re-scanning does record it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp --- .../OneDriveLocatorTests.swift | 125 ++++++++++++++++++ .../ReturnWatcherTests.swift | 52 ++++++++ .../TransportSettingsTests.swift | 114 ++++++++++++++++ 3 files changed, 291 insertions(+) create mode 100644 Tests/ShotdeckCoreTests/OneDriveLocatorTests.swift create mode 100644 Tests/ShotdeckCoreTests/TransportSettingsTests.swift diff --git a/Tests/ShotdeckCoreTests/OneDriveLocatorTests.swift b/Tests/ShotdeckCoreTests/OneDriveLocatorTests.swift new file mode 100644 index 0000000..ed843ab --- /dev/null +++ b/Tests/ShotdeckCoreTests/OneDriveLocatorTests.swift @@ -0,0 +1,125 @@ +import Foundation +import Testing +import ShotdeckCore + +@Test +func syncRootsFindsOneDriveDirsMMDFirstIgnoresNonDirsAndOtherProviders() throws { + let home = try makeFakeHome() + defer { try? FileManager.default.removeItem(at: home) } + let cloudStorage = home.appendingPathComponent("Library/CloudStorage", isDirectory: true) + try FileManager.default.createDirectory(at: cloudStorage, withIntermediateDirectories: true) + + try FileManager.default.createDirectory( + at: cloudStorage.appendingPathComponent("OneDrive-Flowmaster", isDirectory: true), + withIntermediateDirectories: true + ) + try FileManager.default.createDirectory( + at: cloudStorage.appendingPathComponent("OneDrive-MMDGROUP", isDirectory: true), + withIntermediateDirectories: true + ) + try FileManager.default.createDirectory( + at: cloudStorage.appendingPathComponent("GoogleDrive-x", isDirectory: true), + withIntermediateDirectories: true + ) + // A plain FILE (not a directory) named like a OneDrive root must be ignored. + FileManager.default.createFile( + atPath: cloudStorage.appendingPathComponent("OneDrive-notadir").path, + contents: Data("not a directory".utf8) + ) + + let roots = OneDriveLocator.syncRoots(home: home, fileManager: .default) + + #expect(roots.map(\.lastPathComponent) == ["OneDrive-MMDGROUP", "OneDrive-Flowmaster"]) +} + +@Test +func syncRootsEmptyAndDefaultFolderNilWithNoCloudStorageDirectory() throws { + let home = try makeFakeHome() + defer { try? FileManager.default.removeItem(at: home) } + // No Library/CloudStorage created at all. + + let roots = OneDriveLocator.syncRoots(home: home, fileManager: .default) + #expect(roots.isEmpty) + + let defaultFolder = OneDriveLocator.defaultRedlineFolder(home: home, fileManager: .default) + #expect(defaultFolder == nil) +} + +@Test +func defaultRedlineFolderIsFirstSyncRootPlusRedline() throws { + let home = try makeFakeHome() + defer { try? FileManager.default.removeItem(at: home) } + let cloudStorage = home.appendingPathComponent("Library/CloudStorage", isDirectory: true) + try FileManager.default.createDirectory( + at: cloudStorage.appendingPathComponent("OneDrive-MMDGROUP", isDirectory: true), + withIntermediateDirectories: true + ) + try FileManager.default.createDirectory( + at: cloudStorage.appendingPathComponent("OneDrive-Flowmaster", isDirectory: true), + withIntermediateDirectories: true + ) + + let defaultFolder = try #require( + OneDriveLocator.defaultRedlineFolder(home: home, fileManager: .default) + ) + // Derive "expected" from syncRoots() itself (already covered by its own dedicated + // test) rather than hand-building the path string — FileManager's directory + // enumeration can canonicalize /var -> /private/var and the two constructions + // otherwise disagree on that even for a URL that already exists. + let expectedRoot = try #require(OneDriveLocator.syncRoots(home: home, fileManager: .default).first) + let expected = expectedRoot.appendingPathComponent("Redline", isDirectory: true) + #expect(defaultFolder.path == expected.path) +} + +@Test +func resolveOneDriveFolderPrefersAnExistingStoredOverride() throws { + let home = try makeFakeHome() + defer { try? FileManager.default.removeItem(at: home) } + let cloudStorage = home.appendingPathComponent("Library/CloudStorage", isDirectory: true) + try FileManager.default.createDirectory( + at: cloudStorage.appendingPathComponent("OneDrive-MMDGROUP", isDirectory: true), + withIntermediateDirectories: true + ) + + let suite = try makeTransportDefaultsSuite() + defer { tearDownTransportSuite(suite) } + let override = try makeTransportTemporaryDirectory(prefix: "shotdeck-onedrive-override") + defer { try? FileManager.default.removeItem(at: override) } + TransportSettings.setOneDriveFolder(override, defaults: suite.defaults) + + let resolved = OneDriveLocator.resolveOneDriveFolder( + defaults: suite.defaults, home: home, fileManager: .default + ) + #expect(resolved?.path == override.path) +} + +@Test +func resolveOneDriveFolderIgnoresAStoredPathThatNoLongerExists() throws { + let home = try makeFakeHome() + defer { try? FileManager.default.removeItem(at: home) } + let cloudStorage = home.appendingPathComponent("Library/CloudStorage", isDirectory: true) + try FileManager.default.createDirectory( + at: cloudStorage.appendingPathComponent("OneDrive-MMDGROUP", isDirectory: true), + withIntermediateDirectories: true + ) + + let suite = try makeTransportDefaultsSuite() + defer { tearDownTransportSuite(suite) } + let goneOverride = try makeTransportTemporaryDirectory(prefix: "shotdeck-onedrive-gone") + TransportSettings.setOneDriveFolder(goneOverride, defaults: suite.defaults) + try FileManager.default.removeItem(at: goneOverride) + + let resolved = OneDriveLocator.resolveOneDriveFolder( + defaults: suite.defaults, home: home, fileManager: .default + ) + let expectedRoot = try #require(OneDriveLocator.syncRoots(home: home, fileManager: .default).first) + let expected = expectedRoot.appendingPathComponent("Redline", isDirectory: true) + #expect(resolved?.path == expected.path) +} + +private func makeFakeHome() throws -> URL { + let home = FileManager.default.temporaryDirectory + .appendingPathComponent("shotdeck-fake-home-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: home, withIntermediateDirectories: true) + return home +} diff --git a/Tests/ShotdeckCoreTests/ReturnWatcherTests.swift b/Tests/ShotdeckCoreTests/ReturnWatcherTests.swift index 95bce13..d396d1f 100644 --- a/Tests/ShotdeckCoreTests/ReturnWatcherTests.swift +++ b/Tests/ShotdeckCoreTests/ReturnWatcherTests.swift @@ -230,3 +230,55 @@ func w27_fsEventsCallbackFiresOnRealArrival() async throws { await watcher.stop() } } + +@Test("recordUncommended defaults to true: an unmarked PDF is still recorded (AirDrop behaviour unchanged)") +func recordUncommendedDefaultTrueRecordsAnUnmarkedPDF() async throws { + let (paths, cleanup) = try makeCasePaths() + defer { try? FileManager.default.removeItem(at: cleanup) } + + let ledger = try ReturnLedger(paths: paths) + let watcher = ReturnWatcher(paths: paths, ledger: ledger) + let pdfURL = paths.watchFolder.appendingPathComponent("Redline-20260905-090000.pdf") + + try makePDF(at: pdfURL, pageCount: 1, creator: "Redline", annotations: []) + + let found = try await watcher.scanNow() + #expect(found.count == 1) + #expect(found.first?.isCommented == false) + + let all = try await ledger.all() + #expect(all.count == 1) +} + +@Test("recordUncommended=false: an unmarked PDF is not recorded or returned; marking it up in place gets it recorded") +func recordUncommendedFalseSkipsUnmarkedThenRecordsAfterInPlaceMarkup() async throws { + let (paths, cleanup) = try makeCasePaths() + defer { try? FileManager.default.removeItem(at: cleanup) } + + let ledger = try ReturnLedger(paths: paths) + let watcher = ReturnWatcher(paths: paths, ledger: ledger) + await watcher.setRecordUncommented(false) + let pdfURL = paths.watchFolder.appendingPathComponent("Redline-20260905-091500.pdf") + + // OneDrive mode: the PDF is freshly written here (by "send"), unmarked so far. + try makePDF(at: pdfURL, pageCount: 1, creator: "Redline", annotations: []) + + let beforeMarkup = try await watcher.scanNow() + #expect(beforeMarkup.isEmpty) + let allBefore = try await ledger.all() + #expect(allBefore.isEmpty) + + // What the iPad does: mark it up in place, in the SAME folder, then save. + let document = try #require(PDFDocument(url: pdfURL)) + let page = try #require(document.page(at: 0)) + page.addAnnotation(makeAnnotation(.ink, bounds: CGRect(x: 100, y: 100, width: 120, height: 50))) + #expect(document.write(to: pdfURL)) + + let afterMarkup = try await watcher.scanNow() + #expect(afterMarkup.count == 1) + #expect(afterMarkup.first?.isCommented == true) + + let commented = try await ledger.commented() + #expect(commented.count == 1) + #expect(commented.first?.fileURL.resolvingSymlinksInPath().path == pdfURL.resolvingSymlinksInPath().path) +} diff --git a/Tests/ShotdeckCoreTests/TransportSettingsTests.swift b/Tests/ShotdeckCoreTests/TransportSettingsTests.swift new file mode 100644 index 0000000..5de12d2 --- /dev/null +++ b/Tests/ShotdeckCoreTests/TransportSettingsTests.swift @@ -0,0 +1,114 @@ +import Foundation +import Testing +import ShotdeckCore + +@Test +func transportDefaultsToAirDropWhenUnset() throws { + let suite = try makeTransportDefaultsSuite() + defer { tearDownTransportSuite(suite) } + + #expect(TransportSettings.transport(defaults: suite.defaults) == .airDrop) +} + +@Test +func setTransportRoundTrips() throws { + let suite = try makeTransportDefaultsSuite() + defer { tearDownTransportSuite(suite) } + + TransportSettings.setTransport(.oneDrive, defaults: suite.defaults) + #expect(TransportSettings.transport(defaults: suite.defaults) == .oneDrive) + + TransportSettings.setTransport(.airDrop, defaults: suite.defaults) + #expect(TransportSettings.transport(defaults: suite.defaults) == .airDrop) +} + +@Test +func garbageStoredTransportFallsBackToAirDrop() throws { + let suite = try makeTransportDefaultsSuite() + defer { tearDownTransportSuite(suite) } + + suite.defaults.set("not-a-real-transport", forKey: TransportSettings.transportDefaultsKey) + #expect(TransportSettings.transport(defaults: suite.defaults) == .airDrop) +} + +@Test +func oneDriveFolderStoreAndReset() throws { + let suite = try makeTransportDefaultsSuite() + defer { tearDownTransportSuite(suite) } + let folder = try makeTransportTemporaryDirectory(prefix: "shotdeck-onedrive-folder") + defer { try? FileManager.default.removeItem(at: folder) } + + #expect(TransportSettings.storedOneDriveFolderPath(defaults: suite.defaults) == nil) + + TransportSettings.setOneDriveFolder(folder, defaults: suite.defaults) + #expect(TransportSettings.storedOneDriveFolderPath(defaults: suite.defaults) == folder.path) + + TransportSettings.resetOneDriveFolder(defaults: suite.defaults) + #expect(TransportSettings.storedOneDriveFolderPath(defaults: suite.defaults) == nil) +} + +@Test +func effectiveFoldersForAirDropMatchesFolderSettings() throws { + let suite = try makeTransportDefaultsSuite() + defer { tearDownTransportSuite(suite) } + let outbox = try makeTransportTemporaryDirectory(prefix: "shotdeck-effective-outbox") + defer { try? FileManager.default.removeItem(at: outbox) } + FolderSettings.setOutbox(outbox, defaults: suite.defaults) + + let effective = TransportSettings.effectiveFolders(defaults: suite.defaults) + let expected = FolderSettings.resolve(defaults: suite.defaults) + + #expect(effective.transport == .airDrop) + #expect(effective.outbox.path == expected.outbox.path) + #expect(effective.watch.path == expected.watch.path) +} + +@Test +func effectiveFoldersForOneDriveWithAResolvableFolderUsesItForBoth() throws { + let suite = try makeTransportDefaultsSuite() + defer { tearDownTransportSuite(suite) } + let folder = try makeTransportTemporaryDirectory(prefix: "shotdeck-effective-onedrive") + defer { try? FileManager.default.removeItem(at: folder) } + + TransportSettings.setTransport(.oneDrive, defaults: suite.defaults) + TransportSettings.setOneDriveFolder(folder, defaults: suite.defaults) + + let effective = TransportSettings.effectiveFolders(defaults: suite.defaults) + + #expect(effective.transport == .oneDrive) + #expect(effective.outbox.path == folder.path) + #expect(effective.watch.path == folder.path) + #expect(effective.outbox.path == effective.watch.path) +} + +@Test +func oneDriveFolderUnavailableErrorDescriptionContainsThePath() throws { + let path = "/Users/example/Library/CloudStorage/OneDrive-Example/Redline" + let error = ShotdeckError.oneDriveFolderUnavailable(path: path) + let description = try #require(error.errorDescription) + #expect(!description.isEmpty) + #expect(description.contains(path)) +} + +struct TransportDefaultsSuite { + let name: String + let defaults: UserDefaults +} + +func makeTransportDefaultsSuite() throws -> TransportDefaultsSuite { + let name = "shotdeck-transport-test-\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: name)) + defaults.removePersistentDomain(forName: name) + return TransportDefaultsSuite(name: name, defaults: defaults) +} + +func tearDownTransportSuite(_ suite: TransportDefaultsSuite) { + suite.defaults.removePersistentDomain(forName: suite.name) +} + +func makeTransportTemporaryDirectory(prefix: String) throws -> URL { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("\(prefix)-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + return url +}