Commit Graph
21 Commits
Author SHA1 Message Date
Claude Fable 5 706726a3d4 fix(test): LaunchWiringTests was empirically vacuous for the makeLaunchModel path
The reviewer patched main.swift back to FolderSettings.resolvedAppSupportPaths()
(the AirDrop-only resolver) and `swift test --filter LaunchWiringTests` STILL
PASSED, because the only thing the test asserted — model.watchFolderURL — is
computed independently by AppModel.init() via TransportSettings.effectiveFolders(),
not from the `paths` makeLaunchModel() built. bootstrap()'s own unconditional
updateWatchFolder reconcile then papered over the reverted resolver, so the
test only ever proved the bootstrap reconcile, never the launch resolver
itself. The "verified this catches the blocker" claim in the previous
commit was empirically false.

Fix: added ReturnWatcher.currentWatchFolder (public var, actor-isolated —
the folder a watcher is CURRENTLY seeded to scan, readable without calling
scanNow()/updateWatchFolder first). The test now asserts, BEFORE
bootstrap() runs: model.paths.watchFolder/outbox (already internal-visible
via @testable import, no production API change needed there) equal the
OneDrive folder, AND the watcher's currentWatchFolder equals it too — both
of which genuinely depend on what makeLaunchModel() built.

Verified properly this time (both outputs below are verbatim from
`swift test --filter LaunchWiringTests`, main.swift's makeLaunchModel()
temporarily reverted to FolderSettings.resolvedAppSupportPaths() then
restored — the revert itself is not part of this commit):

FAILURE (reverted resolver):
    Expectation failed: (model.paths.watchFolder.path -> "/Users/benjaminhippler/Downloads")
      == (oneDriveFolder.path -> ".../shotdeck-real-wiring-onedrive-<uuid>")
    Expectation failed: (model.paths.outbox.path -> "/Users/benjaminhippler/Desktop")
      == (oneDriveFolder.path -> ".../shotdeck-real-wiring-onedrive-<uuid>")
    Expectation failed: (seededWatchFolder.path -> "/Users/benjaminhippler/Downloads")
      == (oneDriveFolder.path -> ".../shotdeck-real-wiring-onedrive-<uuid>")
    Test ... failed after 0.324 seconds with 3 issues.

PASS (resolver restored):
    Test "Real wiring: AppDelegate.makeLaunchModel() + AppModel.bootstrap()
      detect a marked OneDrive return" passed after 0.295 seconds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
2026-09-05 10:10:14 +04:00
Claude Fable 5 e1f569cc3e fix(core): probeWritable(at:) always cleans up the probe file, even on partial failure
Two leak paths: (a) AtomicFile.write renames the temp file onto the probe
path and THEN fsyncs the containing directory — if that last fsync throws,
the probe file already exists on disk but the old code returned false
without ever attempting removal; (b) if the explicit removeItem call itself
threw, there was no retry, so a transient File Provider removal failure left
the file behind permanently.

Fix: an unconditional `defer` now checks fileExists and retries removeItem
regardless of which branch returned early. The function only reports true
when the explicit write, fsync (inside AtomicFile.write), AND removal all
succeeded AND the file is confirmed gone afterward.

New test: a FileManager subclass whose removeItem(at:) throws on its first
call (AtomicFile.write itself never touches this injected FileManager — it
uses raw Darwin/POSIX calls, not FileManager, so this only intercepts the
explicit removal + the defer's retry) asserts the function returns false AND
no probe file remains — verified this actually needs the defer by
temporarily removing it and confirming the same test then fails with a
leftover ".redline-probe-<uuid>" file (see this branch's history for the
discarded revert). The directory-fsync failure path has no injectable seam
(raw fsync(2) on an already-open fd, not parameterized by any FileManager or
other substitutable dependency, and not reproducible via chmod or other
standard test techniques) — documented in the test rather than simulated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
2026-09-05 10:10:00 +04:00
kua-agentandClaude Fable 5.1 723cd7dcea fix(core): File Provider write probe — OneDriveLocator.probeWritable(at:)
isWritableDirectory (permissions bits) is not enough: a OneDrive
Files-On-Demand directory whose provider domain is signed out can report as
existing and POSIX-writable while an actual write fails. probeWritable
writes a small ".redline-probe-<uuid>" file into the folder via
AtomicFile.write (open+write+fsync+rename+directory-fsync), then removes it;
any failure at write, fsync, or removal means false.

Three unit tests: an ordinary writable directory (true, and no probe file
left behind), a chmod 500 directory (false; permissions restored in
teardown), and a plain file path (false).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
2026-09-05 09:54:58 +04:00
kua-agentandClaude Fable 5.1 20de467e87 fix(core): transport-aware launch paths + writable-folder check (adversarial review)
BLOCKER fix, Core half: adds TransportSettings.resolvedAppSupportPaths(),
the transport-aware equivalent of the AirDrop-only
FolderSettings.resolvedAppSupportPaths() — launch code must use this one so
the ReturnWatcher it feeds is never seeded with a stale AirDrop folder while
OneDrive is the persisted transport. Both now share a single
AppSupportPaths.standardRoot() helper for the ~/Library/Application
Support/Shotdeck root, instead of computing it three separate times.

MAJOR fix, Core half: adds OneDriveLocator.isWritableDirectory(at:) — exists
+ isDirectory is not enough; an existing-but-unwritable folder (permissions
revoked) must be treated as unavailable, not silently attempted and surfaced
as a generic PDF-composition failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
2026-09-05 09:27:32 +04:00
kua-agentandClaude Fable 5.1 299d55e884 feature(core): OneDrive transport settings, locator, and watcher recordUncommented flag
Adds SendTransport/TransportSettings (UserDefaults-backed, mirrors FolderSettings)
and OneDriveLocator, which finds a OneDrive-* sync root under
~/Library/CloudStorage and resolves the Redline send/watch folder inside it
(MMD-named roots preferred). TransportSettings.effectiveFolders() is the one
function that combines the transport choice with FolderSettings/OneDriveLocator.

ReturnWatcher gains recordUncommented (default true, today's AirDrop behaviour):
when false, a document with zero human marks is neither recorded into the
ledger nor returned by scanNow. This is needed because in OneDrive mode the
outbox and watch folder are the same folder, so a freshly written, unmarked
PDF must not be treated as a return — only a later, actually marked-up save
of the same file should be.

Adds ShotdeckError.oneDriveFolderUnavailable(path:) for when the OneDrive
folder is missing or unwritable at send time.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
2026-09-05 09:01:04 +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 d05bd735b5 rename: user-facing product name Shotdeck -> Redline; legacy PDFs still recognized 2026-09-01 22:25:15 +04:00
kua-agent 8dc97d02f2 Merge pull request 'WP-5b: FSEvents ReturnWatcher' (#7) from wp5b/return-watcher-20260831 into feat/shotdeck-20260830 2026-08-31 11:33:58 +00:00
kua-agent 23b53e8cd2 Merge pull request 'WP-3b: ScreenCaptureKit capturer + region picker overlay' (#6) from wp3b/capture-picker-20260831 into feat/shotdeck-20260830 2026-08-31 11:29:33 +00:00
kua-agent ce63d0295f Merge pull request 'WP-1: durable SpoolStore with crash reconciliation and removed/' (#4) from wp1/spool-store-20260831 into feat/shotdeck-20260830 2026-08-31 11:29:26 +00:00
kua-agent 49b1a9ea83 WP-5b: FSEvents ReturnWatcher per SPEC-A1c with review corrections 2026-08-31 15:23:05 +04:00
kua-agent 3e0db398ca Merge pull request 'WP-5a: AnnotationInspector + ReturnLedger' (#5) from wp5a/inspector-ledger-20260831 into feat/shotdeck-20260830 2026-08-31 11:16:52 +00:00
kua-agent 15021e9dda WP-3b: ScreenCaptureKit capturer + region picker overlay per SPEC-A2a 2026-08-31 15:08:05 +04:00
kua-agent 7379e4fbd7 Merge pull request 'WP-3a: CaptureRegion geometry + Carbon HotkeyCenter' (#1) from wp3a/region-hotkey-20260831 into feat/shotdeck-20260830 2026-08-31 11:01:09 +00:00
kua-agent 96e31d17e0 Merge pull request 'WP-2: PDF composer + PageLayout (two-pass, atomic write, zero annotations)' (#3) from wp2/pdf-composer-20260831 into feat/shotdeck-20260830 2026-08-31 11:01:03 +00:00
kua-agent 3dfb703834 WP-5a: AnnotationInspector + ReturnLedger per SPEC-A1c with review corrections 2026-08-31 14:58:34 +04:00
kua-agent 4c4d3d6633 WP-1: durable SpoolStore with crash reconciliation and removed/ per SPEC-A1a 2026-08-31 14:58:03 +04:00
kua-agent 1b5816aa38 WP-2: PDF composer + PageLayout per SPEC-A1b (two-pass, atomic write, zero annotations) 2026-08-31 14:42:00 +04:00
kua-agent 31721b431c WP-0b: AtomicFile, Log, FolderSettings foundation per SPEC-A1a; drop stale hotkey string 2026-08-31 14:40:21 +04:00
kua-agent dc8fa0a2fe WP-3a: CaptureRegion geometry + Carbon HotkeyCenter per SPEC-A2a 2026-08-31 14:39:34 +04:00
kua-agent 97d21be470 WP-0: SwiftPM scaffold, shared model, signed app bundle script 2026-08-30 20:56:37 +04:00