fix(auth): make the stuck-lock guidance conditional, not an instruction (#2082)
The one genuinely new finding from Copilot round 9; the other four were re-reports of rounds 7 and 8 against an earlier state of the files, verified already fixed ine956d498/edddc6c9. This message has now been wrong in both directions. Round 5 replaced "it expires on its own after 10s" — false whenever the same `rmdir` that blocked us also blocks stale takeover, which reclaims through that identical call — with "remove it by hand". That is false the other way: `proper-lockfile` forwards whatever the filesystem returned, so a transient failure can clear, another Inspector can legitimately acquire the path afterwards, and an operator following the advice then deletes a *live* holder's lock. Same class of hazard as the `ERELEASED` case in round 7: guidance that destroys the exclusion of a process that did nothing wrong. Nothing here can tell a permanent refusal from a passing one, so it no longer claims to. It reports what happened, says the lock may or may not clear and why, and states the two conditions the operator can check for themselves — saves still failing, and no other Inspector running — rather than asserting an outcome this code does not know. Behavioural only in what it tells a human; the locking path is unchanged. The test asserts both conditions are present and that the expiry promise is absent, so neither blanket version can come back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTHVxSu8AUgHRLvo1ntZ8H Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
edddc6c9cc
commit
31feed970c
2 changed files with 25 additions and 9 deletions
|
|
@ -475,8 +475,17 @@ describe("withSecretFileLock reports what it cannot clean up", () => {
|
|||
// not be turned into a failure by its own teardown.
|
||||
expect(result).toBe("saved");
|
||||
expect(warnings()).toContain("Could not release the lock");
|
||||
expect(warnings()).toContain("by hand");
|
||||
expect(warnings()).not.toContain("expires on its own");
|
||||
// Conditional guidance, not an instruction. `proper-lockfile` forwards
|
||||
// whatever the filesystem returned and nothing here can tell a permanent
|
||||
// refusal from a transient one — and if it was transient, this path may
|
||||
// by then hold a *different, live* Inspector's lock, so an unconditional
|
||||
// "remove it" would destroy the exclusion of a process that did nothing
|
||||
// wrong. Both conditions must be stated.
|
||||
expect(warnings()).toContain("If saves keep failing");
|
||||
expect(warnings()).toContain("no other Inspector is running");
|
||||
// …and it must not promise the lock clears by itself either, which is
|
||||
// false whenever the same `rmdir` also blocks stale takeover.
|
||||
expect(warnings()).not.toMatch(/It expires on its own/);
|
||||
});
|
||||
|
||||
it("warns rather than crashing the process when the library detects the takeover", async () => {
|
||||
|
|
|
|||
|
|
@ -494,13 +494,20 @@ export async function openSecretFileLock(
|
|||
// happened, so there is nothing to add.
|
||||
if ((err as NodeJS.ErrnoException).code === "ERELEASED") return;
|
||||
warnOnce(
|
||||
// No branch on the *removal* error code, deliberately. Our own guard
|
||||
// `stat`s before removing, so anything else reaching here is an
|
||||
// `rmdir` that was refused — and **stale takeover reclaims through
|
||||
// that same `rmdir`**, so whatever blocked ours blocks that too.
|
||||
// `ENOTEMPTY` is the reachable one; `EACCES`, `EPERM` and `EROFS`
|
||||
// behave identically. Promising expiry was wrong for all of them.
|
||||
`Could not release the lock on the secrets file at ${target} (${describeError(err)}). Stale takeover reclaims a lock through the same directory removal, so whatever blocked this blocks that too: saves will keep failing until you remove ${lockPathOf(target)} by hand.`,
|
||||
// **Conditional, not an instruction.** Two blanket versions of this
|
||||
// have now been wrong in opposite directions: "it expires on its own"
|
||||
// (false whenever the same `rmdir` that blocked us also blocks stale
|
||||
// takeover — `ENOTEMPTY`, `EACCES`, `EPERM`, `EROFS`), and "remove it
|
||||
// by hand" (false whenever the failure was transient, since the path
|
||||
// may by then hold a *different, live* Inspector's lock, and deleting
|
||||
// that destroys the exclusion of a process that did nothing wrong).
|
||||
//
|
||||
// `proper-lockfile` forwards whatever the filesystem returned, and
|
||||
// nothing here can tell a permanent refusal from a passing one. So
|
||||
// this reports what happened and states the two conditions the
|
||||
// operator can check for themselves, rather than asserting an outcome
|
||||
// this code does not know.
|
||||
`Could not release the lock on the secrets file at ${target} (${describeError(err)}). It may clear on its own — stale takeover reclaims a lock through the same directory removal, so it will not if whatever blocked this persists. If saves keep failing against this file and no other Inspector is running, remove ${lockPathOf(target)} by hand.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue