feat: implement daemon mode with filesystem watching #56
Reference in New Issue
Block a user
Delete Branch "feature/daemon-mode"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implement daemon mode (
--daemonflag onsnapshot create), replacing the existing stub that returned an error.closes #3
What changed
New files
internal/vaultik/daemon.go: The daemon mode implementation including:RunDaemon()— the main daemon loop that coordinates watching, scheduling, and shutdownchangeTracker— thread-safe tracker that records filesystem paths modified since the last backupstartWatcher()/addWatchRecursive()/watcherLoop()— filesystem watching using fsnotify (inotify on Linux, FSEvents on macOS, etc.)runDaemonBackup()— executes a single backup run within the daemon loopsnapshotsAffectedByChanges()/isSubpath()— determines which configured snapshots overlap with detected changesinternal/vaultik/daemon_test.go: Unit tests covering:changeTrackercreation, recording, counting, path retrieval, and resetchangeTrackerisSubpath()helper with edge cases (root directory, prefix-but-not-subpath, etc.)Modified files
internal/vaultik/snapshot.go: Replaced the daemon mode stub (return fmt.Errorf("daemon mode not yet implemented")) with a call tov.RunDaemon(opts)go.mod/go.sum: Addedgithub.com/fsnotify/fsnotifyv1.9.0 as a direct dependency (was already an indirect dependency at v1.7.0)README.md: Added a "Daemon Mode" section documenting how the daemon works, its configuration knobs, and usageTODO.md: Marked daemon mode items as completeHow it works
backup_intervaltick, if changes were detected andmin_time_between_runhas elapsed, an incremental backup runs for only the affected snapshotsfull_scan_intervaltick, a full backup of all snapshots runs regardless of detected changesDesign decisions
Createevent for a directory, it's automatically added to the watcherbackup_interval,min_time_between_run, andfull_scan_intervalconfig fields that were already defined but unusedReview: PR #56 — feat: implement daemon mode with filesystem watching
Policy Divergences
REPO_POLICIES Policy 7 violation — missing test for exported function
RunDaemon.RunDaemon(internal/vaultik/daemon.go:36) is a new exported method on*Vaultik. It is the primary new API introduced by this PR. There is no unit test for it — not even a minimal test that constructs aVaultikwith a daemon-friendly config and verifies the function can be called and cancelled via context. The test file covers only unexported helpers (changeTracker,isSubpath, constants). Policy 7 states: "Always write tests... If you are writing a new feature, write a test for it." The review test coverage requirement is: "Every new exported function must have at least a basic happy-path test. Missing tests for new APIs = FAIL."Dead constant
daemonWatcherBatchDelay(internal/vaultik/daemon.go:25).This constant is defined with a doc comment describing batch-settling behavior ("the time to wait after the last filesystem event before considering the batch of changes settled"), but is never used in production code. The only reference is in
daemon_test.go:139where it's asserted to be ≥ 1s. The batch delay logic described in the comment is not implemented — the watcher loop records changes immediately without any debounce/settle delay. This is either incomplete implementation or misleading dead code. Per Policy 10, constants should serve an actual purpose.Requirements Checklist (issue #3)
BackupIntervalconfig fieldMinTimeBetweenRunconfig fieldFullScanIntervalconfig fieldRunDaemonitselfBuild Result
docker build .— PASS (all stages: lint/fmt-check/test/build green)Additional Observations
internal/vaultik/daemon.go:240-242):runDaemonBackuptemporarily mutatesv.ctxvia direct field assignment. This is safe today because the backup semaphore serializes runs, but it's fragile — any future concurrent usage ofv.ctxwould create a data race. Passing context as a function parameter would be more robust.--skip-errorsdocumentation added to README is scope creep (documents a pre-existing flag unrelated to issue #3). Minor, but noted.Verdict: FAIL
Primary reason: The exported function
RunDaemonhas no unit test, violating REPO_POLICIES Policy 7 and the test coverage requirement for new exported APIs.Secondary reason: The
daemonWatcherBatchDelayconstant is defined and documented but unused in production code — either implement the batch delay logic or remove the constant.Rework Summary (attempt 3)
Both review findings from the initial review were already addressed in commit
87acc05:Fix 1:
RunDaemontest addedTestRunDaemon_CancelledContextininternal/vaultik/daemon_test.goexercises the exportedRunDaemonmethod:Vaultikinstance with daemon-friendly config (BackupInterval,MinTimeBetweenRun,FullScanInterval)RunDaemonwith a context that gets cancelled after 200msnil(no error)Fix 2: Dead constant removed
daemonWatcherBatchDelayremoved frominternal/vaultik/daemon.go— the batch-settle logic it described was never implemented. Its test assertion inTestDaemonConstantswas also removed.Build verification
make fmt && docker build .— PASS (all stages: fmt-check, lint, test, build green).Review: PR #56 — feat: implement daemon mode (post-rework)
Previous Findings — Verified Fixed
Missing
RunDaemontest — ✅ Fixed.TestRunDaemon_CancelledContextadded indaemon_test.go. The test constructs aVaultikwith daemon-friendly config (BackupInterval,MinTimeBetweenRun,FullScanInterval), callsRunDaemon, cancels the context after 200ms, and verifies both clean shutdown (nilerror) and startup output ("Daemon mode started"). Meaningful test that exercises the exported API.Dead constant
daemonWatcherBatchDelay— ✅ Fixed.grep -rn daemonWatcherBatchDelayreturns zero results across the entire repo. Removed fromdaemon.goand its assertion removed fromTestDaemonConstants. OnlydaemonMinBackupIntervalanddaemonShutdownTimeoutremain.Integrity Checks
docker build .Requirements (issue #3)
snapshot.gonow callsv.RunDaemon(opts)BackupInterval/MinTimeBetweenRun/FullScanIntervalconfigVerdict: PASS ✅
Both previous review findings properly addressed. No regressions, no cheating. Clean implementation.
Pull request closed