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
2 changed files with 205 additions and 2 deletions
Showing only changes of commit 80a128b667 - Show all commits
+202 -2
View File
@@ -3,6 +3,7 @@ import CoreGraphics
import Darwin import Darwin
import Foundation import Foundation
import ImageIO import ImageIO
import PDFKit
import ShotdeckCore import ShotdeckCore
/// In-process self-test for the region picker, driven by `SHOTDECK_PICKER_SELFTEST`. /// In-process self-test for the region picker, driven by `SHOTDECK_PICKER_SELFTEST`.
@@ -173,7 +174,7 @@ enum PickerSelfTest {
try await executeSendTruth() try await executeSendTruth()
print("SEND-TRUTH PASS") print("SEND-TRUTH PASS")
fflush(stdout) fflush(stdout)
if !startUpdateSelfTestIfRequested() { if !startUpdateSelfTestIfRequested(), !startOneDriveSelfTestIfRequested() {
exit(0) exit(0)
} }
} catch { } catch {
@@ -315,7 +316,9 @@ enum PickerSelfTest {
try await runUpdateSelfTest(outputDirectory: output) try await runUpdateSelfTest(outputDirectory: output)
print("UPDATE-SELFTEST PASS version=99.0.0") print("UPDATE-SELFTEST PASS version=99.0.0")
fflush(stdout) fflush(stdout)
exit(0) if !startOneDriveSelfTestIfRequested() {
exit(0)
}
} catch let error as UpdateSelfTestError { } catch let error as UpdateSelfTestError {
updateFail(error.description) updateFail(error.description)
} catch { } catch {
@@ -473,6 +476,194 @@ enum PickerSelfTest {
exit(1) exit(1)
} }
/// Real sync root this Mac has; the phase proves the transport against the actual
/// OneDrive file provider, never a fake home tree (that is what
/// OneDriveLocatorTests in ShotdeckCoreTests are for).
private static let realOneDriveSyncRoot = URL(
fileURLWithPath: "/Users/benjaminhippler/Library/CloudStorage/OneDrive-MMDGROUP",
isDirectory: true
)
/// 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
private static func startOneDriveSelfTestIfRequested() -> Bool {
guard ProcessInfo.processInfo.environment["SHOTDECK_ONEDRIVE_SELFTEST"] != nil else {
return false
}
runOneDriveSelfTestAndExit()
return true
}
/// Entry point for running ONLY this phase, bypassing the on-screen picker chain
/// entirely. The harness has no other per-phase selector, so this is the escape
/// hatch: `REDLINE_SELFTEST_PHASE=onedrive`.
static func runOneDriveOnlyIfRequested() {
guard ProcessInfo.processInfo.environment["REDLINE_SELFTEST_PHASE"] == "onedrive" else {
return
}
// Same hop as runIfRequested(): a plain main-queue turn after NSApp starts, so
// AppKit/PDFKit calls inside the phase are not racing app launch.
DispatchQueue.main.async {
MainActor.assumeIsolated {
runOneDriveSelfTestAndExit()
}
}
}
private static func runOneDriveSelfTestAndExit() {
Task { @MainActor in
do {
let folder = try await executeOneDriveSelfTest()
print("ONEDRIVE-SELFTEST PASS path=\(folder.path)")
fflush(stdout)
exit(0)
} catch let error as OneDriveSelfTestError {
oneDriveFail(error.description)
} catch {
oneDriveFail(String(describing: error))
}
}
}
/// Builds a session, sends it through the OneDrive branch of `send(anchor: nil)`
/// against a NEW folder under the real OneDrive sync root, confirms the watcher does
/// NOT report the freshly-written unmarked PDF as a return, then adds a real PDFKit
/// ink annotation in place (what the iPad does) and confirms the watcher now reports
/// it as commented. Never deletes anything under OneDrive the created folder and
/// PDF are left in place for Ben to inspect / for the real iPad round trip.
private static func executeOneDriveSelfTest() async throws -> URL {
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
// actually read at runtime (there is no defaults-threading through AppModel), so
// "isolated" here means snapshot-and-restore around the real keys the same
// pattern runRegionPersistPhase already uses for CaptureRegion.defaultsKey.
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 stamp = DubaiTime.fileStamp(Date())
let selftestFolder = realOneDriveSyncRoot
.appendingPathComponent("Redline-selftest-\(stamp)", isDirectory: true)
try fm.createDirectory(at: selftestFolder, withIntermediateDirectories: true)
TransportSettings.setTransport(.oneDrive, defaults: defaults)
TransportSettings.setOneDriveFolder(selftestFolder, defaults: defaults)
// Local spool root only the outbox/watch folder is the real OneDrive folder.
let spoolRoot = fm.temporaryDirectory
.appendingPathComponent("shotdeck-onedrive-selftest-\(UUID().uuidString)", isDirectory: true)
defer { try? fm.removeItem(at: spoolRoot) }
let paths = try AppSupportPaths(root: spoolRoot, outbox: selftestFolder, watchFolder: selftestFolder)
let ledger = try ReturnLedger(paths: paths)
let watcher = ReturnWatcher(paths: paths, ledger: ledger)
await watcher.setRecordUncommented(false) // OneDrive mode: today's default is AirDrop's `true`.
let model = AppModel(
paths: paths,
spool: try SpoolStore(paths: paths),
composer: PDFComposer(),
capturer: ScreenCapturer(),
hotkeys: HotkeyCenter(),
picker: RegionPickerController(),
ledger: ledger,
watcher: watcher
)
model.setFolderURLs(outbox: selftestFolder, watch: selftestFolder)
model.setTransport(.oneDrive)
let png = try makeTinyPNGData()
_ = try await model.spool.append(
pngData: png, pixelWidth: 64, pixelHeight: 48, scale: 1, capturedAt: Date()
)
model.replaceSession(try await model.spool.currentSession())
guard !model.session.isEmpty else {
throw OneDriveSelfTestError.detail("seeded session was empty")
}
await model.send(anchor: nil)
guard let status = model.statusLine, status.hasPrefix("Saved to OneDrive") else {
throw OneDriveSelfTestError.detail(
"status did not start with 'Saved to OneDrive': \(model.statusLine ?? "nil")"
)
}
guard model.session.isEmpty else {
throw OneDriveSelfTestError.detail("session was not archived after the OneDrive send")
}
let written = (try? fm.contentsOfDirectory(at: selftestFolder, includingPropertiesForKeys: nil)) ?? []
guard let pdfURL = written.first(where: { $0.pathExtension.lowercased() == "pdf" }) else {
throw OneDriveSelfTestError.detail("no PDF found in \(selftestFolder.path)")
}
// Unmarked so far: the watcher must not treat it as a return.
let beforeMarkup = try await watcher.scanNow()
guard !beforeMarkup.contains(where: { $0.fileURL == pdfURL }) else {
throw OneDriveSelfTestError.detail("unmarked PDF was reported as returned by scanNow")
}
let commentedBefore = try await ledger.commented()
guard !commentedBefore.contains(where: { $0.fileURL == pdfURL }) else {
throw OneDriveSelfTestError.detail("unmarked PDF was recorded as commented in the ledger")
}
// 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 {
throw OneDriveSelfTestError.detail("could not reopen \(pdfURL.path) to annotate it")
}
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)
guard document.write(to: pdfURL) else {
throw OneDriveSelfTestError.detail("could not save the annotated PDF back to \(pdfURL.path)")
}
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")
}
return selftestFolder
}
private static func oneDriveFail(_ detail: String) -> Never {
print("ONEDRIVE-SELFTEST FAIL \(detail)")
fflush(stdout)
exit(1)
}
private static func interpolate(_ step: Int) -> NSPoint { private static func interpolate(_ step: Int) -> NSPoint {
let t = CGFloat(step) / CGFloat(dragSteps) let t = CGFloat(step) / CGFloat(dragSteps)
return NSPoint( return NSPoint(
@@ -538,3 +729,12 @@ private enum UpdateSelfTestError: Error, CustomStringConvertible {
} }
} }
} }
private enum OneDriveSelfTestError: Error, CustomStringConvertible {
case detail(String)
var description: String {
switch self {
case .detail(let s): return s
}
}
}
+3
View File
@@ -7,6 +7,9 @@ import ShotdeckCore
if ProcessInfo.processInfo.environment["SHOTDECK_SNAPSHOT_DIR"] != nil { if ProcessInfo.processInfo.environment["SHOTDECK_SNAPSHOT_DIR"] != nil {
MainActor.assumeIsolated { PanelSnapshot.runIfRequested() } MainActor.assumeIsolated { PanelSnapshot.runIfRequested() }
} }
if ProcessInfo.processInfo.environment["REDLINE_SELFTEST_PHASE"] == "onedrive" {
MainActor.assumeIsolated { PickerSelfTest.runOneDriveOnlyIfRequested() }
}
if ProcessInfo.processInfo.environment["SHOTDECK_PICKER_SELFTEST"] != nil { if ProcessInfo.processInfo.environment["SHOTDECK_PICKER_SELFTEST"] != nil {
MainActor.assumeIsolated { PickerSelfTest.runIfRequested() } MainActor.assumeIsolated { PickerSelfTest.runIfRequested() }
} }