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:
@@ -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"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user