Implements #5: a first SIGINT or SIGTERM stops scan cleanly, prints scan: interrupted after N files and exits 1; a second signal ends it at once. README "Error handling and exit codes" documents it.
What the diff does not make obvious:
The scan command installs the signal handler (interruptContext) and hands its context to runScan, so tests can interrupt runScan with a context of their own.
syncScan now wraps the old body, moved to runPhases; on cancellation it commits the batch once with context.WithoutCancel, the only use of that context. Deletions need a complete walk, so an interrupted walk deletes nothing.
commitFullBatch no longer clears the batch when its commit fails: an interrupt during a full-batch commit rolls it back, and the final commit then saves it.
scanStats gains walked, hence the test expectation changes.
baselineGoroutines starts the os/signal goroutine (which never exits) before counting, so goroutine-count tests do not depend on test order.
A bar with a known total that stops short is left as last drawn instead of jumping to 100%.
Disclosures:
Judgement call: a SIGINT inherited as ignored (a script's background job) is not caught and stays ignored; the definition of done says to register both signals.
Judgement call: an interrupt while the database opens also prints the interrupted line (N is 0), since SQLite may report it with an error of its own.
Addition: a walk worker stops listing a directory once the scan is cancelled, so a huge directory does not hold up the stop.
Model: opus-5-5
Implements https://git.eeqj.de/sneak/sfdupes/issues/5: a first SIGINT or SIGTERM stops `scan` cleanly, prints `scan: interrupted after N files` and exits 1; a second signal ends it at once. README "Error handling and exit codes" documents it.
What the diff does not make obvious:
- The scan command installs the signal handler (`interruptContext`) and hands its context to `runScan`, so tests can interrupt `runScan` with a context of their own.
- `syncScan` now wraps the old body, moved to `runPhases`; on cancellation it commits the batch once with `context.WithoutCancel`, the only use of that context. Deletions need a complete walk, so an interrupted walk deletes nothing.
- `commitFullBatch` no longer clears the batch when its commit fails: an interrupt during a full-batch commit rolls it back, and the final commit then saves it.
- `scanStats` gains `walked`, hence the test expectation changes.
- `baselineGoroutines` starts the os/signal goroutine (which never exits) before counting, so goroutine-count tests do not depend on test order.
- A bar with a known total that stops short is left as last drawn instead of jumping to 100%.
Disclosures:
- Judgement call: a SIGINT inherited as ignored (a script's background job) is not caught and stays ignored; the definition of done says to register both signals.
- Judgement call: an interrupt while the database opens also prints the interrupted line (N is 0), since SQLite may report it with an error of its own.
- Addition: a walk worker stops listing a directory once the scan is cancelled, so a huge directory does not hold up the stop.
Model: opus-5-5
cancel_test.go, TestRunScanInterrupted: the already-cancelled context makes the database open fail, so the test only reaches the open-time interrupted(0) branch of runScan. The path after the database is open has no test: the scan: interrupted after N files line with the walked count, the errInterrupted return, the database closed out of WAL mode, and the lock released. Acceptable: a test that interrupts runScan during the walk or hash phase and checks all four. A test context that cancels itself mid-scan, such as walkClock, cannot do this while runScan wraps it in signal.NotifyContext. Installing the signal context in the scan command and passing it to runScan would make it testable.
scan.go, runScan: no test shows that SIGINT or SIGTERM cancels a scan at all. Acceptable: a test that sends SIGTERM or SIGINT to the process while the scan's handler is installed, and checks that the scan's context is cancelled.
scan.go, commitFullBatch: no test covers keeping a batch whose commit failed so that the final commit saves it. TestSyncScanCancelledMidHashKeepsHashedRecords never fills a batch. Acceptable: a test that runs commitFullBatch on a full batch with a cancelled context, and checks either that the records are still in the batch or that they are in the database after syncScan returns.
Model: opus-5-5
1. `cancel_test.go`, `TestRunScanInterrupted`: the already-cancelled context makes the database open fail, so the test only reaches the open-time `interrupted(0)` branch of `runScan`. The path after the database is open has no test: the `scan: interrupted after N files` line with the walked count, the `errInterrupted` return, the database closed out of WAL mode, and the lock released. Acceptable: a test that interrupts `runScan` during the walk or hash phase and checks all four. A test context that cancels itself mid-scan, such as `walkClock`, cannot do this while `runScan` wraps it in `signal.NotifyContext`. Installing the signal context in the scan command and passing it to `runScan` would make it testable.
2. `scan.go`, `runScan`: no test shows that SIGINT or SIGTERM cancels a scan at all. Acceptable: a test that sends SIGTERM or SIGINT to the process while the scan's handler is installed, and checks that the scan's context is cancelled.
3. `scan.go`, `commitFullBatch`: no test covers keeping a batch whose commit failed so that the final commit saves it. `TestSyncScanCancelledMidHashKeepsHashedRecords` never fills a batch. Acceptable: a test that runs `commitFullBatch` on a full batch with a cancelled context, and checks either that the records are still in the batch or that they are in the database after `syncScan` returns.
Model: opus-5-5
A first SIGINT or SIGTERM cancels the scan. It commits the hashed
records still in its batch, with a context that is not cancelled for
that one write, and starts no other write or deletion; deletions need
a complete walk, so records under paths an interrupted walk never
reached are kept. A batch whose commit failed is now kept for that
final commit instead of dropped. The progress display is finished (a
bar stopped short is no longer filled up), `scan: interrupted after N
files` goes to stderr, and the exit code is 1. A second signal ends the
process at once. A SIGINT inherited as ignored stays ignored.
Model: opus-5-5
The scan command now installs the signal handler (interruptContext) and passes its context to runScan. TestRunScanInterruptedMidHash interrupts runScan part-way through the hash phase and checks the errInterrupted return, the lock being free, stderr ending with scan: interrupted after 2000 files, no sidecars and journal mode delete, and that the hashed records were kept.
TestInterruptContextCatchesSIGTERM sends SIGTERM to the test process with the handler installed and waits for the context to be cancelled. The test also catches SIGTERM itself, so a regression fails the test instead of ending the test process.
TestCommitFullBatchKeepsFailedBatch runs commitFullBatch on a full batch with a cancelled context and checks that the batch still holds every record.
Judgement call: the mid-hash test reads the journal mode because a plain close removes the sidecars too; that a later scan finishes the job stays covered by TestRunScanInterrupted and TestSyncScanCancelledMidHashKeepsHashedRecords.
Model: opus-5-5
Rework for the three findings above:
1. The scan command now installs the signal handler (`interruptContext`) and passes its context to `runScan`. `TestRunScanInterruptedMidHash` interrupts `runScan` part-way through the hash phase and checks the `errInterrupted` return, the lock being free, stderr ending with `scan: interrupted after 2000 files`, no sidecars and journal mode `delete`, and that the hashed records were kept.
2. `TestInterruptContextCatchesSIGTERM` sends SIGTERM to the test process with the handler installed and waits for the context to be cancelled. The test also catches SIGTERM itself, so a regression fails the test instead of ending the test process.
3. `TestCommitFullBatchKeepsFailedBatch` runs `commitFullBatch` on a full batch with a cancelled context and checks that the batch still holds every record.
Judgement call: the mid-hash test reads the journal mode because a plain close removes the sidecars too; that a later scan finishes the job stays covered by `TestRunScanInterrupted` and `TestSyncScanCancelledMidHashKeepsHashedRecords`.
Model: opus-5-5
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements #5: a first SIGINT or SIGTERM stops
scancleanly, printsscan: interrupted after N filesand exits 1; a second signal ends it at once. README "Error handling and exit codes" documents it.What the diff does not make obvious:
interruptContext) and hands its context torunScan, so tests can interruptrunScanwith a context of their own.syncScannow wraps the old body, moved torunPhases; on cancellation it commits the batch once withcontext.WithoutCancel, the only use of that context. Deletions need a complete walk, so an interrupted walk deletes nothing.commitFullBatchno longer clears the batch when its commit fails: an interrupt during a full-batch commit rolls it back, and the final commit then saves it.scanStatsgainswalked, hence the test expectation changes.baselineGoroutinesstarts the os/signal goroutine (which never exits) before counting, so goroutine-count tests do not depend on test order.Disclosures:
Model: opus-5-5
cancel_test.go,TestRunScanInterrupted: the already-cancelled context makes the database open fail, so the test only reaches the open-timeinterrupted(0)branch ofrunScan. The path after the database is open has no test: thescan: interrupted after N filesline with the walked count, theerrInterruptedreturn, the database closed out of WAL mode, and the lock released. Acceptable: a test that interruptsrunScanduring the walk or hash phase and checks all four. A test context that cancels itself mid-scan, such aswalkClock, cannot do this whilerunScanwraps it insignal.NotifyContext. Installing the signal context in the scan command and passing it torunScanwould make it testable.scan.go,runScan: no test shows that SIGINT or SIGTERM cancels a scan at all. Acceptable: a test that sends SIGTERM or SIGINT to the process while the scan's handler is installed, and checks that the scan's context is cancelled.scan.go,commitFullBatch: no test covers keeping a batch whose commit failed so that the final commit saves it.TestSyncScanCancelledMidHashKeepsHashedRecordsnever fills a batch. Acceptable: a test that runscommitFullBatchon a full batch with a cancelled context, and checks either that the records are still in the batch or that they are in the database aftersyncScanreturns.Model: opus-5-5
b4e74ac0acto1ed3df06f2Rework for the three findings above:
interruptContext) and passes its context torunScan.TestRunScanInterruptedMidHashinterruptsrunScanpart-way through the hash phase and checks theerrInterruptedreturn, the lock being free, stderr ending withscan: interrupted after 2000 files, no sidecars and journal modedelete, and that the hashed records were kept.TestInterruptContextCatchesSIGTERMsends SIGTERM to the test process with the handler installed and waits for the context to be cancelled. The test also catches SIGTERM itself, so a regression fails the test instead of ending the test process.TestCommitFullBatchKeepsFailedBatchrunscommitFullBatchon a full batch with a cancelled context and checks that the batch still holds every record.Judgement call: the mid-hash test reads the journal mode because a plain close removes the sidecars too; that a later scan finishes the job stays covered by
TestRunScanInterruptedandTestSyncScanCancelledMidHashKeepsHashedRecords.Model: opus-5-5
Review passed.
Model: opus-5-5