Redline: OneDrive folder transport as alternative to AirDrop (MMDB-2631) #23
@@ -282,3 +282,110 @@ func recordUncommentedFalseSkipsUnmarkedThenRecordsAfterInPlaceMarkup() async th
|
|||||||
#expect(commented.count == 1)
|
#expect(commented.count == 1)
|
||||||
#expect(commented.first?.fileURL.resolvingSymlinksInPath().path == pdfURL.resolvingSymlinksInPath().path)
|
#expect(commented.first?.fileURL.resolvingSymlinksInPath().path == pdfURL.resolvingSymlinksInPath().path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Launch-paths BLOCKER regression (adversarial review, 20260905)
|
||||||
|
//
|
||||||
|
// The bug: AppDelegate.makeLaunchModel() built `paths` via the AirDrop-only
|
||||||
|
// FolderSettings.resolvedAppSupportPaths(), so ReturnWatcher's internal watchFolder
|
||||||
|
// (seeded from paths.watchFolder in its own init) was the AirDrop folder even when
|
||||||
|
// OneDrive was the persisted transport, and bootstrap() never reconciled it before
|
||||||
|
// starting. Net effect: PDFs went to OneDrive but FSEvents kept watching the stale
|
||||||
|
// AirDrop folder for the whole session — marked-up returns were never detected.
|
||||||
|
// The fix: launch paths now come from TransportSettings.resolvedAppSupportPaths()
|
||||||
|
// (transport-aware), and AppModel.bootstrap() unconditionally reconciles the watcher's
|
||||||
|
// folder via updateWatchFolder() before it starts. These two tests characterize the
|
||||||
|
// bug (still reproducible via the old AirDrop-only construction) and prove the fix
|
||||||
|
// (the real launch-construction path, end to end).
|
||||||
|
|
||||||
|
@Test("Launch regression (fix): OneDrive persisted -> transport-aware launch paths -> bootstrap-style reconcile -> a marked PDF is detected")
|
||||||
|
func launchStyleConstructionWithOneDriveTransportDetectsAMarkedReturn() async throws {
|
||||||
|
let suite = try makeTransportDefaultsSuite()
|
||||||
|
defer { tearDownTransportSuite(suite) }
|
||||||
|
let oneDriveFolder = try makeTransportTemporaryDirectory(prefix: "shotdeck-launch-onedrive")
|
||||||
|
defer { try? FileManager.default.removeItem(at: oneDriveFolder) }
|
||||||
|
let appSupportRoot = try makeTransportTemporaryDirectory(prefix: "shotdeck-launch-approot")
|
||||||
|
defer { try? FileManager.default.removeItem(at: appSupportRoot) }
|
||||||
|
|
||||||
|
TransportSettings.setTransport(.oneDrive, defaults: suite.defaults)
|
||||||
|
TransportSettings.setOneDriveFolder(oneDriveFolder, defaults: suite.defaults)
|
||||||
|
|
||||||
|
// Exactly what AppDelegate.makeLaunchModel() now does: build launch paths from the
|
||||||
|
// transport-aware resolver — the fix, NOT FolderSettings.resolvedAppSupportPaths(),
|
||||||
|
// which is AirDrop-only and is the root cause the next test characterizes.
|
||||||
|
let paths = try TransportSettings.resolvedAppSupportPaths(
|
||||||
|
root: appSupportRoot, defaults: suite.defaults, fileManager: .default
|
||||||
|
)
|
||||||
|
#expect(paths.outbox.path == oneDriveFolder.path)
|
||||||
|
#expect(paths.watchFolder.path == oneDriveFolder.path)
|
||||||
|
|
||||||
|
let ledger = try ReturnLedger(paths: paths)
|
||||||
|
let watcher = ReturnWatcher(paths: paths, ledger: ledger)
|
||||||
|
|
||||||
|
// What AppModel.bootstrap() now does, unconditionally, before watcher.start():
|
||||||
|
await watcher.setRecordUncommented(false) // transport == .oneDrive
|
||||||
|
try await watcher.updateWatchFolder(paths.watchFolder)
|
||||||
|
|
||||||
|
let pdfURL = oneDriveFolder.appendingPathComponent("Redline-20260905-100000.pdf")
|
||||||
|
try makePDF(
|
||||||
|
at: pdfURL, pageCount: 1, creator: "Redline",
|
||||||
|
annotations: [(page: 0, annotation: makeAnnotation(
|
||||||
|
.ink, bounds: CGRect(x: 100, y: 100, width: 120, height: 50)
|
||||||
|
))]
|
||||||
|
)
|
||||||
|
|
||||||
|
let found = try await watcher.scanNow()
|
||||||
|
#expect(found.contains(where: {
|
||||||
|
$0.fileURL.resolvingSymlinksInPath().path == pdfURL.resolvingSymlinksInPath().path && $0.isCommented
|
||||||
|
}))
|
||||||
|
|
||||||
|
let commented = try await ledger.commented()
|
||||||
|
#expect(commented.contains(where: {
|
||||||
|
$0.fileURL.resolvingSymlinksInPath().path == pdfURL.resolvingSymlinksInPath().path
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Launch regression (characterizes the bug): AirDrop-only launch paths with no reconcile miss an OneDrive-mode return")
|
||||||
|
func airDropOnlyLaunchPathsWithoutReconcileMissesAMarkedOneDriveReturn() async throws {
|
||||||
|
let suite = try makeTransportDefaultsSuite()
|
||||||
|
defer { tearDownTransportSuite(suite) }
|
||||||
|
let oneDriveFolder = try makeTransportTemporaryDirectory(prefix: "shotdeck-buggy-onedrive")
|
||||||
|
defer { try? FileManager.default.removeItem(at: oneDriveFolder) }
|
||||||
|
// A configured AirDrop watch-folder override, isolated to a temp dir — NOT the real
|
||||||
|
// ~/Downloads, which may already hold real marked-up Redline PDFs from actual use
|
||||||
|
// and would make this test's "found.isEmpty" assertion depend on the state of
|
||||||
|
// Ben's real Downloads folder instead of the isolated fixture under test.
|
||||||
|
let staleAirDropFolder = try makeTransportTemporaryDirectory(prefix: "shotdeck-buggy-airdrop-stale")
|
||||||
|
defer { try? FileManager.default.removeItem(at: staleAirDropFolder) }
|
||||||
|
let appSupportRoot = try makeTransportTemporaryDirectory(prefix: "shotdeck-buggy-approot")
|
||||||
|
defer { try? FileManager.default.removeItem(at: appSupportRoot) }
|
||||||
|
|
||||||
|
TransportSettings.setTransport(.oneDrive, defaults: suite.defaults)
|
||||||
|
TransportSettings.setOneDriveFolder(oneDriveFolder, defaults: suite.defaults)
|
||||||
|
FolderSettings.setWatchFolder(staleAirDropFolder, defaults: suite.defaults)
|
||||||
|
|
||||||
|
// The BUG's exact construction: FolderSettings.resolvedAppSupportPaths() ignores
|
||||||
|
// the persisted transport entirely and always resolves the AirDrop folders.
|
||||||
|
let buggyPaths = try FolderSettings.resolvedAppSupportPaths(root: appSupportRoot, defaults: suite.defaults)
|
||||||
|
#expect(buggyPaths.watchFolder.path == staleAirDropFolder.path)
|
||||||
|
#expect(buggyPaths.watchFolder.path != oneDriveFolder.path)
|
||||||
|
|
||||||
|
let ledger = try ReturnLedger(paths: buggyPaths)
|
||||||
|
let watcher = ReturnWatcher(paths: buggyPaths, ledger: ledger)
|
||||||
|
// The old bootstrap(): recordUncommented was set, but there was NO
|
||||||
|
// updateWatchFolder() call before start() to reconcile the folder.
|
||||||
|
await watcher.setRecordUncommented(false)
|
||||||
|
|
||||||
|
let pdfURL = oneDriveFolder.appendingPathComponent("Redline-20260905-100100.pdf")
|
||||||
|
try makePDF(
|
||||||
|
at: pdfURL, pageCount: 1, creator: "Redline",
|
||||||
|
annotations: [(page: 0, annotation: makeAnnotation(
|
||||||
|
.ink, bounds: CGRect(x: 100, y: 100, width: 120, height: 50)
|
||||||
|
))]
|
||||||
|
)
|
||||||
|
|
||||||
|
// The watcher is still pointed at the stale (configured-AirDrop) watch folder, so
|
||||||
|
// scanning it — NOT the OneDrive folder the PDF actually landed in — finds nothing.
|
||||||
|
// This is the exact BLOCKER the fix above closes.
|
||||||
|
let found = try await watcher.scanNow()
|
||||||
|
#expect(found.isEmpty)
|
||||||
|
}
|
||||||
|
|||||||
@@ -90,6 +90,38 @@ func oneDriveFolderUnavailableErrorDescriptionContainsThePath() throws {
|
|||||||
#expect(description.contains(path))
|
#expect(description.contains(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func isWritableDirectoryTrueForAnOrdinaryWritableDirectory() throws {
|
||||||
|
let dir = try makeTransportTemporaryDirectory(prefix: "shotdeck-writable")
|
||||||
|
defer { try? FileManager.default.removeItem(at: dir) }
|
||||||
|
#expect(OneDriveLocator.isWritableDirectory(at: dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func isWritableDirectoryFalseForAnExistingButUnwritableDirectory() throws {
|
||||||
|
let dir = try makeTransportTemporaryDirectory(prefix: "shotdeck-unwritable")
|
||||||
|
defer {
|
||||||
|
// Restore perms BEFORE removal — an unwritable dir can't otherwise be cleaned up.
|
||||||
|
try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: dir.path)
|
||||||
|
try? FileManager.default.removeItem(at: dir)
|
||||||
|
}
|
||||||
|
#expect(OneDriveLocator.isWritableDirectory(at: dir)) // sanity check before chmod
|
||||||
|
|
||||||
|
try FileManager.default.setAttributes([.posixPermissions: 0o500], ofItemAtPath: dir.path)
|
||||||
|
#expect(!OneDriveLocator.isWritableDirectory(at: dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func isWritableDirectoryFalseForAPlainFileAndForANonexistentPath() throws {
|
||||||
|
let dir = try makeTransportTemporaryDirectory(prefix: "shotdeck-writable-check-parent")
|
||||||
|
defer { try? FileManager.default.removeItem(at: dir) }
|
||||||
|
let filePath = dir.appendingPathComponent("plain-file.txt")
|
||||||
|
FileManager.default.createFile(atPath: filePath.path, contents: Data("x".utf8))
|
||||||
|
|
||||||
|
#expect(!OneDriveLocator.isWritableDirectory(at: filePath))
|
||||||
|
#expect(!OneDriveLocator.isWritableDirectory(at: dir.appendingPathComponent("does-not-exist")))
|
||||||
|
}
|
||||||
|
|
||||||
struct TransportDefaultsSuite {
|
struct TransportDefaultsSuite {
|
||||||
let name: String
|
let name: String
|
||||||
let defaults: UserDefaults
|
let defaults: UserDefaults
|
||||||
|
|||||||
Reference in New Issue
Block a user