Redline: OneDrive folder transport as alternative to AirDrop (MMDB-2631) #23

Merged
kua-agent merged 20 commits from feat/redline-onedrive-transport-20260905 into main 2026-09-05 06:17:29 +00:00
Showing only changes of commit 8121738188 - Show all commits
+129 -41
View File
@@ -223,7 +223,7 @@ enum PickerSelfTest {
sendTruthFail("seeded session was empty") sendTruthFail("seeded session was empty")
} }
let pending = try await model.composePDFForSend() let pending = try await model.composePDFForSend(outbox: model.outboxURL)
guard fm.fileExists(atPath: pending.fileURL.path) else { guard fm.fileExists(atPath: pending.fileURL.path) else {
sendTruthFail("PDF was not written") sendTruthFail("PDF was not written")
} }
@@ -476,18 +476,13 @@ enum PickerSelfTest {
exit(1) exit(1)
} }
/// Real sync root this Mac has; the phase proves the transport against the actual /// Phase 5: proves the OneDrive transport end to end against a REAL sync root
/// OneDrive file provider, never a fake home tree (that is what /// resolved at runtime via `OneDriveLocator.syncRoots()`, never a hardcoded path, so
/// OneDriveLocatorTests in ShotdeckCoreTests are for). /// this runs correctly on any Mac/account that has OneDrive signed in (MMD-named
private static let realOneDriveSyncRoot = URL( /// root preferred, same as production). Triggered by `SHOTDECK_ONEDRIVE_SELFTEST`
fileURLWithPath: "/Users/benjaminhippler/Library/CloudStorage/OneDrive-MMDGROUP", /// when chained after PICKER/SEND-TRUTH/UPDATE-SELFTEST the exact pattern
isDirectory: true /// `startUpdateSelfTestIfRequested` uses for its own env var. Returns true when the
) /// async phase was scheduled (it calls `exit` itself).
/// Phase 5: proves the OneDrive transport end to end against the real sync root.
/// Triggered by `SHOTDECK_ONEDRIVE_SELFTEST` when chained after PICKER/SEND-TRUTH/
/// UPDATE-SELFTEST the exact pattern `startUpdateSelfTestIfRequested` uses for its
/// own env var. Returns true when the async phase was scheduled (it calls `exit` itself).
@discardableResult @discardableResult
private static func startOneDriveSelfTestIfRequested() -> Bool { private static func startOneDriveSelfTestIfRequested() -> Bool {
guard ProcessInfo.processInfo.environment["SHOTDECK_ONEDRIVE_SELFTEST"] != nil else { guard ProcessInfo.processInfo.environment["SHOTDECK_ONEDRIVE_SELFTEST"] != nil else {
@@ -514,9 +509,16 @@ enum PickerSelfTest {
} }
private static func runOneDriveSelfTestAndExit() { private static func runOneDriveSelfTestAndExit() {
// Never a false PASS: no real OneDrive sync root on this machine/account is a
// SKIP (still non-zero exit), not silently treated as passing.
guard let syncRoot = OneDriveLocator.syncRoots().first else {
print("ONEDRIVE-SELFTEST SKIP no OneDrive sync root")
fflush(stdout)
exit(1)
}
Task { @MainActor in Task { @MainActor in
do { do {
let folder = try await executeOneDriveSelfTest() let folder = try await executeOneDriveSelfTest(syncRoot: syncRoot)
print("ONEDRIVE-SELFTEST PASS path=\(folder.path)") print("ONEDRIVE-SELFTEST PASS path=\(folder.path)")
fflush(stdout) fflush(stdout)
exit(0) exit(0)
@@ -528,19 +530,30 @@ enum PickerSelfTest {
} }
} }
/// Builds a session, sends it through the OneDrive branch of `send(anchor: nil)` /// Sub-step 1: builds a session, sends it through the OneDrive branch of
/// against a NEW folder under the real OneDrive sync root, confirms the watcher does /// `send(anchor: nil)` against a NEW folder under `syncRoot`, confirms the watcher
/// NOT report the freshly-written unmarked PDF as a return, then adds a real PDFKit /// does NOT report the freshly-written unmarked PDF as a return, then adds a real
/// ink annotation in place (what the iPad does) and confirms the watcher now reports /// PDFKit ink annotation in place (what the iPad does) and confirms the watcher now
/// it as commented. Never deletes anything under OneDrive the created folder and /// reports it as commented.
/// PDF are left in place for Ben to inspect / for the real iPad round trip. ///
private static func executeOneDriveSelfTest() async throws -> URL { /// Sub-step 1b: rapid transport toggling (chooseTransport(.airDrop) immediately
/// followed by chooseTransport(.oneDrive), no await between them) must still end
/// with the watcher pointed at the OneDrive folder proves the generation-guarded
/// reconcile in chooseTransport/chooseOneDriveFolder (SettingsView.swift) really
/// does let the last choice win instead of an earlier, superseded call applying its
/// stale folder after a later one already won.
///
/// Sub-step 2: relaunch simulation the exact BLOCKER scenario this phase exists to
/// catch. OneDrive is still persisted in defaults from sub-step 1; builds a FRESH
/// model the same way the real app launches (`AppDelegate.makeLaunchModel()` itself,
/// not a reimplementation), bootstraps it, then marks a PDF in the folder in place
/// the relaunched watcher must report it. A temp app-support root keeps this off the
/// real ~/Library/Application Support/Shotdeck.
///
/// Never deletes anything under OneDrive the created folder and PDFs are left in
/// place for Ben to inspect / for the real iPad round trip.
private static func executeOneDriveSelfTest(syncRoot: URL) async throws -> URL {
let fm = FileManager.default let fm = FileManager.default
guard fm.fileExists(atPath: realOneDriveSyncRoot.path) else {
throw OneDriveSelfTestError.detail(
"real OneDrive sync root not found at \(realOneDriveSyncRoot.path)"
)
}
// UserDefaults.standard is the ONLY defaults instance send()/TransportSettings // UserDefaults.standard is the ONLY defaults instance send()/TransportSettings
// actually read at runtime (there is no defaults-threading through AppModel), so // actually read at runtime (there is no defaults-threading through AppModel), so
@@ -563,8 +576,7 @@ enum PickerSelfTest {
} }
let stamp = DubaiTime.fileStamp(Date()) let stamp = DubaiTime.fileStamp(Date())
let selftestFolder = realOneDriveSyncRoot let selftestFolder = syncRoot.appendingPathComponent("Redline-selftest-\(stamp)", isDirectory: true)
.appendingPathComponent("Redline-selftest-\(stamp)", isDirectory: true)
try fm.createDirectory(at: selftestFolder, withIntermediateDirectories: true) try fm.createDirectory(at: selftestFolder, withIntermediateDirectories: true)
TransportSettings.setTransport(.oneDrive, defaults: defaults) TransportSettings.setTransport(.oneDrive, defaults: defaults)
@@ -629,8 +641,77 @@ enum PickerSelfTest {
} }
// What the iPad does: mark it up in place with a real ink annotation, then save. // What the iPad does: mark it up in place with a real ink annotation, then save.
guard let document = PDFDocument(url: pdfURL), let page = document.page(at: 0) else { try addInkMark(to: pdfURL)
throw OneDriveSelfTestError.detail("could not reopen \(pdfURL.path) to annotate it")
let afterMarkup = try await watcher.scanNow()
guard let recorded = afterMarkup.first(where: { $0.fileURL == pdfURL }), recorded.isCommented else {
throw OneDriveSelfTestError.detail("annotated PDF was not reported as commented by scanNow")
}
let commentedAfter = try await ledger.commented()
guard commentedAfter.contains(where: { $0.fileURL == pdfURL }) else {
throw OneDriveSelfTestError.detail("annotated PDF was not recorded in the ledger as commented")
}
// Sub-step 1b: rapid toggle race see the doc comment above this function.
model.chooseTransport(.airDrop)
model.chooseTransport(.oneDrive) // immediately superseding the call above
// The generation guard itself is what's under test, not this wait it just
// gives the (already-guarded) reconcile Task a moment to settle either way.
try await Task.sleep(for: .milliseconds(500))
guard model.transport == .oneDrive else {
throw OneDriveSelfTestError.detail(
"rapid toggle: model.transport ended as \(model.transport), expected .oneDrive"
)
}
let racePDFURL = selftestFolder.appendingPathComponent("Redline-race-\(stamp).pdf")
try writeUnmarkedRedlinePDF(to: racePDFURL)
try addInkMark(to: racePDFURL)
let raceFound = try await model.watcher.scanNow()
guard raceFound.first(where: { $0.fileURL == racePDFURL })?.isCommented == true else {
throw OneDriveSelfTestError.detail(
"rapid toggle: watcher did not end up watching \(selftestFolder.path) — an earlier, superseded chooseTransport call won"
)
}
// Sub-step 2: relaunch simulation see the doc comment above this function.
let relaunchAppSupportRoot = fm.temporaryDirectory
.appendingPathComponent("shotdeck-onedrive-relaunch-\(UUID().uuidString)", isDirectory: true)
defer { try? fm.removeItem(at: relaunchAppSupportRoot) }
let relaunchModel = AppDelegate.makeLaunchModel(appSupportRoot: relaunchAppSupportRoot)
guard relaunchModel.transport == .oneDrive else {
throw OneDriveSelfTestError.detail(
"relaunch: model transport was \(relaunchModel.transport), expected .oneDrive"
)
}
guard relaunchModel.watchFolderURL.path == selftestFolder.path else {
throw OneDriveSelfTestError.detail(
"relaunch: model watchFolderURL was \(relaunchModel.watchFolderURL.path), expected \(selftestFolder.path) — this is the exact BLOCKER this phase guards against"
)
}
await relaunchModel.bootstrap()
let relaunchPDFURL = selftestFolder.appendingPathComponent("Redline-relaunch-\(stamp).pdf")
try writeUnmarkedRedlinePDF(to: relaunchPDFURL)
try addInkMark(to: relaunchPDFURL)
let relaunchFound = try await relaunchModel.watcher.scanNow()
guard relaunchFound.first(where: { $0.fileURL == relaunchPDFURL })?.isCommented == true else {
throw OneDriveSelfTestError.detail(
"relaunch: watcher did not report the marked PDF at \(relaunchPDFURL.path) as returned — it was watching the wrong folder after relaunch"
)
}
await relaunchModel.watcher.stop()
return selftestFolder
}
/// Adds a real PDFKit ink annotation to the PDF at `url` in place and saves it
/// exactly what the iPad does when marking up a page.
private static func addInkMark(to url: URL) throws {
guard let document = PDFDocument(url: url), let page = document.page(at: 0) else {
throw OneDriveSelfTestError.detail("could not reopen \(url.path) to annotate it")
} }
let ink = PDFAnnotation( let ink = PDFAnnotation(
bounds: CGRect(x: 20, y: 20, width: 60, height: 60), bounds: CGRect(x: 20, y: 20, width: 60, height: 60),
@@ -642,20 +723,27 @@ enum PickerSelfTest {
stroke.line(to: NSPoint(x: 80, y: 80)) stroke.line(to: NSPoint(x: 80, y: 80))
ink.add(stroke) ink.add(stroke)
page.addAnnotation(ink) page.addAnnotation(ink)
guard document.write(to: pdfURL) else { guard document.write(to: url) else {
throw OneDriveSelfTestError.detail("could not save the annotated PDF back to \(pdfURL.path)") throw OneDriveSelfTestError.detail("could not save the annotated PDF back to \(url.path)")
} }
}
let afterMarkup = try await watcher.scanNow() /// Writes a fresh, unmarked, single-page "Redline"-creator PDF straight to `url`
guard let recorded = afterMarkup.first(where: { $0.fileURL == pdfURL }), recorded.isCommented else { /// standing in for a PDF that has just landed in the watch folder, before any
throw OneDriveSelfTestError.detail("annotated PDF was not reported as commented by scanNow") /// human mark. Used by the rapid-toggle and relaunch sub-steps, which don't need to
/// exercise send()/composePDFForSend() again (sub-step 1 already does).
private static func writeUnmarkedRedlinePDF(to url: URL) throws {
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,
]
guard document.write(to: url) else {
throw OneDriveSelfTestError.detail("could not write \(url.path)")
} }
let commentedAfter = try await ledger.commented()
guard commentedAfter.contains(where: { $0.fileURL == pdfURL }) else {
throw OneDriveSelfTestError.detail("annotated PDF was not recorded in the ledger as commented")
}
return selftestFolder
} }
private static func oneDriveFail(_ detail: String) -> Never { private static func oneDriveFail(_ detail: String) -> Never {