import AppKit import Foundation import PDFKit import Testing import ShotdeckCore @testable import Shotdeck /// Coverage gap closed (adversarial review, rounds 3 and 4): the Core-level regression /// tests in ShotdeckCoreTests hand-replicate what `AppDelegate.makeLaunchModel()` and /// `AppModel.bootstrap()` do, rather than calling them — so a future revert of /// `makeLaunchModel()` back to the AirDrop-only resolver, or a dropped /// `updateWatchFolder` call inside `bootstrap()`, would NOT fail `swift test`. This /// test goes through the real, unmodified call sites in the `Shotdeck` executable /// target via `@testable import`, which `ShotdeckCoreTests` cannot reach (it only /// depends on `ShotdeckCore`) — hence this separate `ShotdeckTests` target. /// /// Round 4 correction: the first version of this test asserted only /// `model.watchFolderURL`, which `AppModel.init` computes independently via /// `TransportSettings.effectiveFolders()` — so it stayed correct (and the test kept /// passing) even when `makeLaunchModel()` was reverted to the AirDrop-only resolver, /// because `bootstrap()`'s own unconditional `updateWatchFolder` reconcile papered /// over the reverted resolver. That made the "verified this catches the blocker" /// claim in the previous round's commit message empirically false. This version /// asserts `model.paths`/the watcher's `currentWatchFolder` BEFORE `bootstrap()` runs, /// which actually depends on what `makeLaunchModel()` built — see this file's git /// history (or the round-4 commit message) for the verbatim before/after /// `swift test --filter` output proving it now discriminates correctly. @MainActor @Test("Real wiring: AppDelegate.makeLaunchModel() + AppModel.bootstrap() detect a marked OneDrive return") func realLaunchModelAndBootstrapDetectAMarkedOneDriveReturn() async throws { let fm = FileManager.default // UserDefaults.standard is the ONLY defaults instance makeLaunchModel()/bootstrap() // actually read — there is no defaults-threading through AppModel/AppDelegate (the // same reasoning documented in PickerSelfTest.swift's ONEDRIVE-SELFTEST phase). // "Isolated" here means snapshot-and-restore around the real keys, not a separate // UserDefaults(suiteName:) instance that these real, unmodified call sites would // never actually consult. let defaults = UserDefaults.standard let previousTransport = defaults.string(forKey: TransportSettings.transportDefaultsKey) let previousFolder = defaults.string(forKey: TransportSettings.oneDriveFolderDefaultsKey) defer { if let previousTransport { defaults.set(previousTransport, forKey: TransportSettings.transportDefaultsKey) } else { defaults.removeObject(forKey: TransportSettings.transportDefaultsKey) } if let previousFolder { defaults.set(previousFolder, forKey: TransportSettings.oneDriveFolderDefaultsKey) } else { defaults.removeObject(forKey: TransportSettings.oneDriveFolderDefaultsKey) } } let oneDriveFolderRaw = fm.temporaryDirectory .appendingPathComponent("shotdeck-real-wiring-onedrive-\(UUID().uuidString)", isDirectory: true) try fm.createDirectory(at: oneDriveFolderRaw, withIntermediateDirectories: true) defer { try? fm.removeItem(at: oneDriveFolderRaw) } // FileManager's directory enumeration (inside the real ReturnWatcher/AppSupportPaths // call sites this test exercises) can canonicalize /var -> /private/var for a path // that actually exists; resolve here so every comparison below agrees. let oneDriveFolder = oneDriveFolderRaw.resolvingSymlinksInPath() let appSupportRoot = fm.temporaryDirectory .appendingPathComponent("shotdeck-real-wiring-approot-\(UUID().uuidString)", isDirectory: true) defer { try? fm.removeItem(at: appSupportRoot) } TransportSettings.setTransport(.oneDrive, defaults: defaults) TransportSettings.setOneDriveFolder(oneDriveFolder, defaults: defaults) // Best-effort: keeps AppModel.bootstrap()'s real update-check schedule (a real // HTTP GET after 10s, plus a RunLoop timer) from starting during this test. // ProcessInfo.processInfo.environment on Darwin reads `environ` fresh each call, // so a setenv() here is visible to bootstrap()'s own check immediately. setenv("SHOTDECK_ONEDRIVE_SELFTEST", "1", 1) defer { unsetenv("SHOTDECK_ONEDRIVE_SELFTEST") } // The REAL, unmodified call sites — not a reimplementation. This is exactly what // launching Redline with OneDrive as the persisted transport does. let model = AppDelegate.makeLaunchModel(appSupportRoot: appSupportRoot) // bootstrap() registers a REAL, process-wide Carbon global hotkey (capture combo, // e.g. Option-Shift-2). Carbon registrations are not scoped to this test/model — // they must be released before this test ends, or ShotdeckCoreTests' // HotkeyCenterCarbonTests (a separate test target, same test process) can find the // combo already taken / the global hotkey table in an unexpected state. defer { model.hotkeys.unregisterAll() } // PRE-bootstrap assertions — this is the actual proof of the launch RESOLVER // (AppDelegate.makeLaunchModel() -> TransportSettings.resolvedAppSupportPaths()), // independent of bootstrap()'s own reconcile. `model.watchFolderURL` alone does // NOT prove this: AppModel.init computes it separately via // TransportSettings.effectiveFolders(), so it would read as correct even if // makeLaunchModel's `paths` were built by the AirDrop-only resolver — which is // exactly how the first version of this test was empirically shown to be vacuous // for the launch-resolver path (see this commit's message). `model.paths` is // `internal` on AppModel, so @testable import already exposes it without any // production API change; `currentWatchFolder` is the one new (internal-facing, // `public` on the actor) seam added to ReturnWatcher for this purpose. #expect(model.paths.watchFolder.path == oneDriveFolder.path) #expect(model.paths.outbox.path == oneDriveFolder.path) let seededWatchFolder = await model.watcher.currentWatchFolder #expect(seededWatchFolder.path == oneDriveFolder.path) #expect(model.transport == .oneDrive) #expect(model.watchFolderURL.path == oneDriveFolder.path) await model.bootstrap() // Drop a marked-up Redline PDF into the folder in place — what OneDrive syncing // down an already-marked copy after a relaunch looks like. let pdfURL = oneDriveFolder.appendingPathComponent("Redline-realwiring-\(UUID().uuidString).pdf") let document = PDFDocument() let page = PDFPage() page.setBounds(CGRect(x: 0, y: 0, width: 612, height: 792), for: .mediaBox) document.insert(page, at: 0) document.documentAttributes = [ PDFDocumentAttribute.creatorAttribute: "Redline", PDFDocumentAttribute.subjectAttribute: UUID().uuidString, ] let ink = PDFAnnotation( bounds: CGRect(x: 20, y: 20, width: 60, height: 60), forType: .ink, withProperties: nil ) let stroke = NSBezierPath() stroke.move(to: NSPoint(x: 20, y: 20)) stroke.line(to: NSPoint(x: 80, y: 80)) ink.add(stroke) page.addAnnotation(ink) let written = document.write(to: pdfURL) #expect(written) guard written else { return } // .resolvingSymlinksInPath().path — not plain URL equality — matching how the rest // of the suite compares a temp-dir-derived expected URL against a returned one. let expectedPath = pdfURL.resolvingSymlinksInPath().path let found = try await model.watcher.scanNow() #expect(found.first(where: { $0.fileURL.resolvingSymlinksInPath().path == expectedPath })?.isCommented == true) let commented = try await model.ledger.commented() #expect(commented.contains(where: { $0.fileURL.resolvingSymlinksInPath().path == expectedPath })) await model.watcher.stop() }