fix: keep scan cleanup failures from replacing the scan result - #127
fix: keep scan cleanup failures from replacing the scan result#127Haiduongcable wants to merge 1 commit into
Conversation
|
Full CI is green on
The local Two notes for review:
|
The scan finally block awaited knowledge-base cleanup, target-paths removal, and the credential home release without guarding any of them. A rejection in a finally block replaces the pending exception, so a cleanup failure discarded the outcome the try and catch blocks had already produced and the caller was handed an unrelated filesystem error instead of the reason the scan failed. The catch block had already recorded the real message through fail-scan, so the persisted scan history and the reported error disagreed. Removing the temporary inputs is best effort and is now reported through onWarning instead of deciding the result. Releasing the credential home lock is not: it only marks itself done once the lock directory is gone, so a failed release leaves an owner.json naming a live process that stale-lock recovery will not reclaim. That failure still propagates, and is downgraded to a warning only when the scan already failed and its error is the one worth keeping. The failed-scan marker is recorded on entry to the catch block because requireOpen and throwIfAborted raise a different error for the same failed scan, the release keeps its own finally so reporting the earlier failures cannot skip it, and the warning formatter is total so no hostile reason can throw in place of the result.
f7817f5 to
62611ac
Compare
|
Updated to What changed since
The PR description has been corrected accordingly. Two claims in the first version were wrong: the change does not preserve the original ordering ( Full CI is green on
Local One scoping note carried into the description: a failed release still leaves a lock that |
|
Thanks for improving how scan cleanup failures are handled. The behavior and its important regression cases are already on |
Summary
Fixes #38.
The scan
finallyblock awaited three cleanup steps without guarding any of them:A rejection in a
finallyblock replaces the pending exception, so a cleanup failure discardsthe outcome the
tryandcatchblocks already produced. The caller is handed an unrelatedfilesystem error instead of the reason the scan failed.
These are not hypothetical rejections.
removeTargetPathsFilerethrows on any non-Windowsplatform, and
knowledgeBase.cleanup()isrm(path, { recursive: true, force: true }), whereforcesuppresses onlyENOENT—EACCES,EBUSY, andENOTEMPTYstill reject.The failure path also loses information that was recorded correctly: the
catchblock writes thereal message to the workbench through
fail-scanbefore thefinallyblock runs, so thepersisted scan history and the error the caller receives disagree about why the scan failed.
ScanCostLimitExceededErrorandScanInterruptedErrorare erased the same way, so a scanstopped by its own cost ceiling can report a
rmfailure instead.Impact
The two kinds of cleanup are treated differently on purpose, because only one of them is
best effort.
Changes
through the existing
onWarningobserver instead of deciding the scan result.warning on an otherwise successful scan. The release closure only marks itself done once the
lock directory is gone, so a failed release leaves an
owner.jsonnaming a live process;recoverStaleCredentialHomeLockthen declines to reclaim it because that pid is alive, andlater scans in the same process wait on a lock nothing frees. Reporting success while leaving
the client in that state would be worse than failing, so the failure still propagates, and is
downgraded to a warning only when the scan had already failed and its error is the one worth
keeping.
catchblock.this.#requireOpen()andthrowIfAborted()raise a different error for the same failed scan, so recording it laterwould let a lock-release failure replace a close or interruption error — the same masking this
change removes.
finally, so it is still attempted even if constructing, settling, orreporting the input cleanups throws.
warnCleanupFailedis total. Reading, coercing, and delivering the reason are all guarded,because a throw while reporting a cleanup failure would replace the scan result just as
effectively as the original bug.
Promise.allSettledalso surfaces both input failures wherePromise.allreported only thefirst.
Verification
The new test fails against
mainand passes with the change: 67 passed / 1 failed before,68 passed / 0 failed after, in
tests-ts/api.test.ts.The pre-change failure is the bug itself, not a different error:
Verified against real
mainsource rather than a local revert, by restoringsdk/typescript/src/api.tsfromupstream/mainand confirminggit diff --stat upstream/mainreported no difference for that file before running the test.
Full public SDK suite with the CI-pinned Bun 1.3.14: 471 passed, 6 expected skips, 0 failed
(
mainbaseline is 470 passed, 6 skipped; this change adds one test).pnpm run types,pnpm run format,pnpm run audit:prod,pnpm run build,pnpm pack, andpnpm run check:packageall clean.The existing test that asserts the target-paths file is removed after a scan still passes, so
cleanup is still performed rather than skipped.
Notes
non-recursive
rmrejects on every platform rather than depending on POSIX permission bits.It asserts both that the real scan error survives and that exactly one cleanup warning is
emitted, so a platform where the cleanup unexpectedly succeeded fails the warning assertion
rather than passing vacuously.
releaseCredentialHomeis only assigned forstored_credentialswhenprepareRuntimeisundefined, and the test harness has to inject
prepareRuntime, so no unit test can reach thatbranch. Flagging this rather than implying coverage it does not have.
onWarningobserver andis never written to the workbench, and the CLI's handler already passes warnings through
cliErrorMessage(). This is the distinction relative to Scan failure messages are stored without credential redaction #43, where thefail-scan --messagepath persists an unredacted message.
recoverStaleCredentialHomeLockwill not reclaim while the owning pid is alive. That livenessdefect is in
acquireCodexSecurityCredentialHomeLock/recoverStaleCredentialHomeLockinsrc/runtime.ts, not in this block, and needs its own change. This PR makes the failurevisible rather than silent; it does not claim to repair the lock.
complete-scanrunning beforecollectResult, which lets a persistenceerror hide an incomplete scan. That is the other error-masking path in this file.
Reporter credit: @mariohercules, who identified the
finallyblock and the replacement behaviorin #38.