10 Commits
Author SHA1 Message Date
kua-agent eb1af5bf18 Merge pull request 'Redline: OneDrive folder transport as alternative to AirDrop (MMDB-2631)' (#23) from feat/redline-onedrive-transport-20260905 into main
Merge pull request #23: Redline: OneDrive folder transport as alternative to AirDrop (MMDB-2631)
2026-09-05 06:17:29 +00:00
kua-agentandClaude Fable 5.1 365220ade8 test: extend UPDATE-SELFTEST — reject-unsigned, staged-signed, atomic-install, revert
Same fake-99.0.0-bundle setup as before, but now drives it through the
hardened UpdateChecker end to end, in-process:

(a) reject-unsigned — the fake bundle, copied then plist-edited without
    re-signing (editing Info.plist after copy invalidates the inherited
    signature on its own — nothing stripped by hand), must be rejected by
    checkNow(): updateAvailable stays nil and statusMessage is the exact
    "Update is not signed by MMD" text.
(b) staged-signed — codesign --force --deep --sign the same bundle
    (SHOTDECK_SELFTEST_SIGN_IDENTITY or the default Apple Development
    identity), re-zip, re-serve the same appcast path; must now stage.
(c) atomic-install — installStaged into a throwaway <tmp>/Applications
    (never real /Applications) pre-populated with a copy of the actually
    running app; asserts the target lands on 99.0.0, Redline.app.previous
    holds the original version, and no replacement-directory cruft is left
    beside them.
(d) revert — revertToPrevious swaps the rollback copy back in; asserts the
    target is back to the original version and .previous now holds 99.0.0.

Caught a real bug while wiring (d): replaceItemAt(target, withItemAt:
previousURL, backupItemName: "Redline.app.previous") self-clobbers, because
the backup name and the withItemAt source resolve to the same path — the
backup write lands before the swap ever reads it, so target ends up
unchanged. Fixed in UpdateChecker by staging previousURL through a throwaway
ditto copy first (same pattern installStaged already used).

Every existing phase (PICKER-SELFTEST, REGION-PERSIST, SEND-TRUTH, and the
final "UPDATE-SELFTEST PASS version=99.0.0") is unchanged and still prints.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 10:00:28 +04:00
kua-agentandClaude Fable 5.1 8d68c836cd test(selftest): ONEDRIVE-SELFTEST sub-step 1c — toggle immediately followed by Send
Adds a third rapid-toggle assertion alongside the existing folder-reconcile
check: chooseTransport(.airDrop) immediately followed by
chooseTransport(.oneDrive), with NO sleep, then an immediate send(anchor: nil)
— proving send() correctly awaits the pending reconcile Task
(SendController.swift/AppModel.swift in this series) rather than racing
ahead with a stale recordUncommented flag. Asserts the freshly-sent,
still-unmarked PDF is never itself reported as an already-returned document.

Also updates composePDFForSend's call site for its new transport: parameter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
2026-09-05 09:55:22 +04:00
kua-agentandClaude Fable 5.1 8121738188 fix: MINOR — ONEDRIVE-SELFTEST resolves the real sync root at runtime; adds relaunch + rapid-toggle sub-steps
realOneDriveSyncRoot was a hardcoded /Users/benjaminhippler/... literal.
Now resolved via OneDriveLocator.syncRoots().first at runtime (MMD-named
root still preferred, matching production); when no OneDrive sync root
exists at all, prints "ONEDRIVE-SELFTEST SKIP no OneDrive sync root" and
exits non-zero — never a false PASS.

Adds two sub-steps to the same phase, both required by the BLOCKER fix's
review: (1) rapid transport toggling (chooseTransport(.airDrop) immediately
followed by chooseTransport(.oneDrive)) must still end with the watcher
watching the OneDrive folder — proves the generation-guarded reconcile in
SettingsView.swift really lets the last choice win. (2) relaunch simulation
— OneDrive still persisted from sub-step 1, a FRESH model built via the
exact same AppDelegate.makeLaunchModel() function real launch uses (now
internal + an appSupportRoot override for this purpose, temp-rooted so this
never touches the real ~/Library/Application Support/Shotdeck), bootstrapped,
then a PDF marked up in place — the relaunched watcher must report it. This
is the exact BLOCKER scenario the review flagged, proven end to end.

Factored the ink-annotation and unmarked-PDF-writing code into
addInkMark(to:)/writeUnmarkedRedlinePDF(to:) so both new sub-steps and the
original markup check share it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
2026-09-05 09:28:08 +04:00
kua-agentandClaude Fable 5.1 80a128b667 selftest: ONEDRIVE-SELFTEST phase, proven against the real OneDrive sync root
New phase chained after UPDATE-SELFTEST (so a full SHOTDECK_PICKER_SELFTEST
chain now prints all five PASS lines) and also runnable standalone via
REDLINE_SELFTEST_PHASE=onedrive, since the harness has no other per-phase
selector.

Points TransportSettings at a NEW "Redline-selftest-<Dubai timestamp>" folder
under the real /Users/.../OneDrive-MMDGROUP sync root (never a fake home tree
— that proves the transport against the actual OneDrive file provider), drives
a seeded session through the OneDrive branch of send(anchor: nil), asserts the
PDF landed, the session archived, and the status starts with "Saved to
OneDrive", then confirms the watcher does NOT report the fresh unmarked PDF
as returned. It then adds a real PDFKit ink annotation to that PDF in place —
what the iPad does — saves it, and confirms the watcher now reports it as
commented. Prints "ONEDRIVE-SELFTEST PASS path=<folder>". Never deletes
anything under OneDrive; the created folder and PDF are left in place.

UserDefaults.standard's transport/oneDriveFolder keys are snapshotted and
restored around the phase, the same pattern runRegionPersistPhase already
uses for CaptureRegion — there is no separate defaults-suite threading
through AppModel/send(), so this is the only way to drive the real send()
path without leaving the real app pointed at the selftest folder afterward.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
2026-09-05 09:01:29 +04:00
kua-agent 36071aed84 Merge remote-tracking branch 'origin/feat/shotdeck-20260830' into fix/send-truth-20260902
# Conflicts:
#	Sources/Shotdeck/PickerSelfTest.swift
2026-09-02 09:17:09 +04:00
kua-agent 5e61cd735c fix: archive only after AirDrop completes; PDF named by send time; Reveal last PDF 2026-09-02 09:12:08 +04:00
kua-agent c01653e9aa feature: built-in auto-update (appcast + sha256 + staged install), version 0.2.0 2026-09-02 09:08:42 +04:00
kua-agentandClaude Fable 5 517a4e4fdc fix: load persisted capture region at launch; REGION-PERSIST selftest phase
Ben-reported: picker opened on every activation. AppModel.init set region = nil and never
read UserDefaults back; saving worked, every launch forgot it. init now loads via
loadPersistedRegion() (decode + isStillValid). Selftest phase 2 writes a known region,
reloads through the same path, asserts the rect, restores the user's stored value.
Coordinator ran it: PICKER-SELFTEST PASS + REGION-PERSIST PASS, 90/90 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 22:20:40 +04:00
kua-agentandClaude Fable 5 f2088bbed8 fix: picker first-mouse acceptance + event-based drag coords; in-process picker selftest
Root cause: RegionPickerView lacked acceptsFirstMouse — as an LSUIElement accessory app
Shotdeck is never active when the hotkey fires, so the user's first click on the overlay
was refused and the drag never started. Also plumbs the monitored event's location through
the controller (hardware-cursor reads made the chain untestable). Adds PickerSelfTest
(SHOTDECK_PICKER_SELFTEST): posts synthetic mouse events through the app's own queue,
asserts the exact CaptureRegion, saves a mid-drag overlay bitmap. Coordinator ran it:
PICKER-SELFTEST PASS rect=(200.0, 729.0, 400.0, 300.0); overlay bitmap shows dim+punch+chip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 21:46:54 +04:00