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:
@@ -164,9 +164,12 @@ def build_ydl_opts(
|
||||
opts.update(extra)
|
||||
|
||||
# SEC-13: hard ceiling on a single download. AFTER the adapter merge, for the same reason
|
||||
# `cookiefile` is — a source adapter must not be able to raise a limit the quota layer set. A
|
||||
# non-positive budget is left unset: "0 bytes left" is a refusal for enqueue to make, not
|
||||
# something to express here as an impossible yt-dlp filter.
|
||||
# `cookiefile` is — a source adapter must not be able to raise a limit the quota layer set.
|
||||
#
|
||||
# A non-positive budget is left unset because 0 is not an expressible yt-dlp filter, NOT because
|
||||
# nothing needs to happen: the worker refuses that job outright before extraction (`_OwnerRefused`
|
||||
# → `quota_exceeded`). Enqueue cannot make that call — it ran while the user was still under the
|
||||
# cap. Delete the worker's refusal and this branch silently uncaps the one user already over it.
|
||||
if max_bytes and max_bytes > 0:
|
||||
opts["max_filesize"] = max_bytes
|
||||
|
||||
|
||||
@@ -567,9 +567,9 @@ def _download(
|
||||
_reset_asset_pending(asset_id)
|
||||
log.info("job %s aborted (pause/cancel)", job_id)
|
||||
except _OwnerRefused as exc:
|
||||
# Fail this job only. The asset is untouched and goes back to `pending`, so another user's
|
||||
# job for the same video is claimed and downloaded normally instead of inheriting a reason
|
||||
# that is about someone else's account.
|
||||
# Fail this job only. The asset is never marked `error`; it goes back to `pending` (below),
|
||||
# so another user's job for the same video is claimed and downloaded normally instead of
|
||||
# inheriting a reason that is about someone else's account.
|
||||
msg = str(exc)[:500]
|
||||
with SessionLocal() as db:
|
||||
service.finalize_if_running(
|
||||
|
||||
@@ -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.
|
||||
|
||||
+6
-1
@@ -46,7 +46,12 @@ if ($dumpCode -ne 0) {
|
||||
docker compose cp "db:$remote" $out
|
||||
$cpCode = $LASTEXITCODE
|
||||
Remove-RemoteDump $remote
|
||||
if ($cpCode -ne 0) { throw "BACKUP FAILED — could not copy the dump out of the container (exit $cpCode)." }
|
||||
# Remove the partial file: `cp` streams, so a mid-stream failure leaves a truncated dump under
|
||||
# the final name — non-zero length, so the size check below would not catch it either.
|
||||
if ($cpCode -ne 0) {
|
||||
Remove-Item $out -ErrorAction SilentlyContinue
|
||||
throw "BACKUP FAILED — could not copy the dump out of the container (exit $cpCode)."
|
||||
}
|
||||
if (-not (Test-Path $out) -or (Get-Item $out).Length -eq 0) {
|
||||
if (Test-Path $out) { Remove-Item $out }
|
||||
throw "BACKUP FAILED — the dump came out empty; no backup was written."
|
||||
|
||||
+7
-4
@@ -63,10 +63,13 @@ if ($restoreCode -ne 0) {
|
||||
throw "pg_restore reported errors while restoring $DumpFile (exit $restoreCode) — read the output above before treating this instance as restored."
|
||||
}
|
||||
Write-Output "Restored $DumpFile"
|
||||
$cf = if ($env:COMPOSE_FILE) { " -f $env:COMPOSE_FILE" } else { "" }
|
||||
Write-Output "api and worker are stopped. Start them again the way you normally start Siftlode:"
|
||||
Write-Output " docker compose$cf up -d"
|
||||
Write-Output "Use the SAME compose file you started with — 'up -d' recreates from whatever file it reads,"
|
||||
# Deliberately not a runnable line when we could not resolve the file: both compose files can be
|
||||
# present (a source checkout), and the two declare DIFFERENT volume names — so a pasted bare
|
||||
# 'docker compose up -d' would recreate the database on an empty volume and strand the restore.
|
||||
$cf = if ($env:COMPOSE_FILE) { $env:COMPOSE_FILE } else { "<the compose file you started with>" }
|
||||
Write-Output "api and worker are stopped. Start them again with:"
|
||||
Write-Output " docker compose -f $cf up -d"
|
||||
Write-Output "It must be the SAME file you started with: 'up -d' recreates from whatever file it reads,"
|
||||
Write-Output "and a different one points at different volumes. Rolling a release back? Pin the older"
|
||||
Write-Output "IMAGE_TAG in .env FIRST: every start re-runs the migrations, and 'start' would relaunch the"
|
||||
Write-Output "existing container on the image you are rolling back FROM."
|
||||
|
||||
+10
-3
@@ -42,9 +42,16 @@ fi
|
||||
# not swallowed and not dressed up as success.
|
||||
if docker compose exec -T db pg_restore -U "$USER_NAME" -d "$DB_NAME" --clean --if-exists < "$FILE"; then
|
||||
echo "Restored $FILE"
|
||||
echo "api and worker are stopped. Start them again the way you normally start Siftlode:"
|
||||
echo " docker compose ${COMPOSE_FILE:+-f $COMPOSE_FILE }up -d"
|
||||
echo "Use the SAME compose file you started with — 'up -d' recreates from whatever file it reads,"
|
||||
echo "api and worker are stopped. Start them again with:"
|
||||
if [ -n "${COMPOSE_FILE:-}" ]; then
|
||||
echo " docker compose -f $COMPOSE_FILE up -d"
|
||||
else
|
||||
# Deliberately NOT a runnable line. Both compose files can be present (a source checkout), and
|
||||
# the two declare DIFFERENT volume names — so a pasted bare 'docker compose up -d' would recreate
|
||||
# the database on an empty volume and strand what was just restored.
|
||||
echo " docker compose -f <the compose file you started with> up -d"
|
||||
fi
|
||||
echo "It must be the SAME file you started with: 'up -d' recreates from whatever file it reads,"
|
||||
echo "and a different one points at different volumes. Rolling a release back? Pin the older"
|
||||
echo "IMAGE_TAG in .env FIRST: every start re-runs the migrations, and 'start' would relaunch the"
|
||||
echo "existing container on the image you are rolling back FROM."
|
||||
|
||||
Reference in New Issue
Block a user