Redline: OneDrive folder transport as alternative to AirDrop (MMDB-2631) #23
@@ -27,5 +27,14 @@ let package = Package(
|
|||||||
dependencies: ["ShotdeckCore"],
|
dependencies: ["ShotdeckCore"],
|
||||||
swiftSettings: [.swiftLanguageMode(.v6)]
|
swiftSettings: [.swiftLanguageMode(.v6)]
|
||||||
),
|
),
|
||||||
|
// Exercises the real Shotdeck-app-target wiring (AppDelegate.makeLaunchModel(),
|
||||||
|
// AppModel.bootstrap()) via @testable import — logic ShotdeckCoreTests cannot
|
||||||
|
// reach because it only depends on ShotdeckCore, not the Shotdeck executable
|
||||||
|
// target itself. See ReturnWatcherLaunchWiringTests.swift.
|
||||||
|
.testTarget(
|
||||||
|
name: "ShotdeckTests",
|
||||||
|
dependencies: ["Shotdeck", "ShotdeckCore"],
|
||||||
|
swiftSettings: [.swiftLanguageMode(.v6)]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
import PDFKit
|
||||||
|
import Testing
|
||||||
|
import ShotdeckCore
|
||||||
|
@testable import Shotdeck
|
||||||
|
|
||||||
|
/// Coverage gap closed (adversarial review, round 3): 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.
|
||||||
|
@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() }
|
||||||
|
|
||||||
|
#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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user