fix(e12): unbreak the gate, and stop printing a command that can strand the restore

Round five. 47 -> 34 -> 15 -> 7 -> 6 raw findings, and this round's sharpest was the
loop's own blind spot: **the branch did not pass the gate.** `prettier --check` is one of
its eight lanes and I had been running the other seven, so an unformatted `onError` from
round one sat on the branch for four rounds — `siftlode publish` would have refused it,
with no --skip flag to get past.

**The restart hint could still strand the restored data.** Round four said the scripts no
longer print a command they cannot resolve; they did. The COMPOSE_FILE fallback only fires
when `docker-compose.yml` is ABSENT, so in a source checkout it never fires and the hint
expanded to a bare, pasteable `docker compose up -d` — which reads the other file, whose
volumes are `pgdata`/`downloads` rather than `siftlode_pgdata`/`siftlode_downloads`, and
recreates the database on an empty one. When the file is unresolved the scripts now print
`-f <the compose file you started with>`: not runnable, which is the point.

**A comment naming the wrong owner.** `formats.py` still said a zero budget is "a refusal
for enqueue to make" — the premise round one disproved, three files away from the two
docstrings that now say the opposite. Anyone acting on it would delete the worker's refusal
and re-open the original inversion, so it now says where the refusal actually lives.

Also: the reset-flag rule was rewritten in three consecutive rounds and pinned by nothing —
two tests now cover a fragment losing its token, both by a pasted verify link and by
navigating away entirely; `backup.ps1` deletes a partially-copied dump before throwing, the
guard round four gave the docs but not the script it mirrors; and one comment that read as
if the asset were untouched by a line four below it.

Gate green, all eight lanes: ruff, ruff format, 3x tsc, eslint, prettier, knip,
vitest 687, pytest 950.
This commit is contained in:
2026-09-05 04:41:21 +02:00
parent 14f1c97c0a
commit 9f28f46839
8 changed files with 70 additions and 24 deletions
+3 -4
View File
@@ -350,10 +350,9 @@ function PendingAccess() {
onError: (err) =>
notify({
level: "error",
message:
isEmailOff(err)
? t("users.pendingAccess.resendEmailOff")
: t("users.pendingAccess.resendFailed"),
message: isEmailOff(err)
? t("users.pendingAccess.resendEmailOff")
: t("users.pendingAccess.resendFailed"),
}),
});
@@ -111,9 +111,7 @@ describe("the account panel reports a refused recovery action", () => {
await clickAction("users.account.resend");
await waitFor(() =>
expect(errorMessages()).toContain("users.pendingAccess.resendEmailOff"),
);
await waitFor(() => expect(errorMessages()).toContain("users.pendingAccess.resendEmailOff"));
});
it("reports a genuine send failure as a failure, not as a sent mail", async () => {
@@ -137,8 +135,6 @@ describe("the account panel reports a refused recovery action", () => {
await clickAction("users.account.activate");
await waitFor(() =>
expect(errorMessages()).toContain("users.pendingAccess.activateFailed"),
);
await waitFor(() => expect(errorMessages()).toContain("users.pendingAccess.activateFailed"));
});
});
+33
View File
@@ -158,6 +158,39 @@ describe("confirming an emailed verification link", () => {
expect(screen.getByTestId("reset").textContent).toBe("reset-pending");
});
// The flag must not outlive the token it describes. Navigating away from a `#reset=` fragment —
// a pasted verify link, the back button — leaves the address bar with no reset token, so a
// still-true flag promises a recovery that signing out can no longer complete AND (being checked
// first by VerifyBanner) hides whatever arrived in its place.
it("drops the reset notice once the fragment carrying it is gone", async () => {
const verifyEmail = vi.fn().mockResolvedValue({ ok: true, email: "someone@example.com" });
const outcome = await mountAt("/#reset=going-away", verifyEmail);
expect(screen.getByTestId("reset").textContent).toBe("reset-pending");
await act(async () => {
window.history.replaceState(window.history.state, "", "/#verify=tok-next");
window.dispatchEvent(new HashChangeEvent("hashchange"));
});
expect(screen.getByTestId("reset").textContent).toBe("-");
expect(outcome()).toBe("ok:someone@example.com");
});
// …and a fragment change that carries neither token drops it too (the back button out of a
// reset link), which a one-way "set it when you see one" listener would not.
it("drops the reset notice when the fragment changes to no token at all", async () => {
await mountAt("/#reset=going-away", vi.fn());
expect(screen.getByTestId("reset").textContent).toBe("reset-pending");
await act(async () => {
window.history.replaceState(window.history.state, "", "/#somewhere-else");
window.dispatchEvent(new HashChangeEvent("hashchange"));
});
expect(screen.getByTestId("reset").textContent).toBe("-");
});
// A reset link can arrive over an unread confirmation (and a slow /auth/verify can resolve after
// one did), so both states can be set at once. Neither may eat the other: `dismiss` used to clear
// both, which is how the reset guidance could be destroyed before it was ever rendered.