test: the gate's output carries no noise to hide a real warning in

- Home tests mocked the api without downloadIndex, so every card's download button logged a
  missing-queryFn error (64 stderr blocks per run).
- QueryState rendered without an i18n instance; Modal.fullscreen awaited a synchronous act,
  which React reports as an act environment it does not support.
- pytest: Starlette 1.x's TestClient is built on httpx2 and warned on every run without it
  (pinned in requirements-dev); alembic.ini declares its path separator.
This commit is contained in:
2026-09-17 03:03:37 +02:00
parent c3d88d9b20
commit 9c9b1cf6d5
6 changed files with 14 additions and 5 deletions
+2
View File
@@ -1,6 +1,8 @@
[alembic] [alembic]
script_location = alembic script_location = alembic
prepend_sys_path = . prepend_sys_path = .
# Split the path list the way the OS does (alembic's legacy space/comma/colon split is deprecated).
path_separator = os
[loggers] [loggers]
keys = root,sqlalchemy,alembic keys = root,sqlalchemy,alembic
+4 -2
View File
@@ -1,7 +1,9 @@
# Test + lint tooling — NEVER in the prod image. Installed only by backend/Dockerfile.test (the # Test + lint tooling — NEVER in the prod image. Installed only by backend/Dockerfile.test (the
# throwaway test image), which the prod build (root Dockerfile) never references. Pinned so the # throwaway test image), which the prod build (root Dockerfile) never references. Pinned so the
# suite is reproducible: pytest for the pure-logic tests, and ruff at the SAME version the gate's # suite is reproducible: pytest for the pure-logic tests, and ruff at the SAME version the gate's
# backend lint lane currently runs on the host (see the run_lint note in siftlode.sh). httpx is # backend lint lane runs on the host (the gate fails when the host's ruff is not this one). httpx2:
# already an app dependency (requirements.txt), so its TestClient needs nothing extra here. # Starlette 1.x's TestClient is built on it (`import httpx2 as httpx`) and only falls back to the
# app's own httpx with a deprecation warning on every run.
pytest==8.4.2 pytest==8.4.2
ruff==0.15.21 ruff==0.15.21
httpx2==2.13.0
@@ -49,6 +49,7 @@ vi.mock("../lib/api", () => ({
setState: () => Promise.resolve(), setState: () => Promise.resolve(),
watchLaterAdd: () => Promise.resolve(), watchLaterAdd: () => Promise.resolve(),
watchLaterRemove: () => Promise.resolve(), watchLaterRemove: () => Promise.resolve(),
downloadIndex: () => Promise.resolve({}), // the cards' download buttons read it
}, },
DISCOVER_BUDGETS: ["short", "medium", "long", "any"], DISCOVER_BUDGETS: ["short", "medium", "long", "any"],
discoverCardKey: (c: { source: string; id: string }) => `${c.source}:${c.id}`, discoverCardKey: (c: { source: string; id: string }) => `${c.source}:${c.id}`,
@@ -53,6 +53,7 @@ vi.mock("../lib/api", () => ({
setState: () => Promise.resolve(), setState: () => Promise.resolve(),
watchLaterAdd: () => Promise.resolve(), watchLaterAdd: () => Promise.resolve(),
watchLaterRemove: () => Promise.resolve(), watchLaterRemove: () => Promise.resolve(),
downloadIndex: () => Promise.resolve({}), // the cards' download buttons read it
comfortPins: (...a: unknown[]) => comfortPins(...a), comfortPins: (...a: unknown[]) => comfortPins(...a),
addComfortPin: (...a: unknown[]) => addComfortPin(...a), addComfortPin: (...a: unknown[]) => addComfortPin(...a),
removeComfortPin: (...a: unknown[]) => removeComfortPin(...a), removeComfortPin: (...a: unknown[]) => removeComfortPin(...a),
@@ -31,7 +31,7 @@ describe("a dialog open while fullscreen is left", () => {
it("keeps what the viewer had typed in it, and the focus inside it", async () => { it("keeps what the viewer had typed in it, and the focus inside it", async () => {
const stage = document.createElement("div"); const stage = document.createElement("div");
document.body.appendChild(stage); document.body.appendChild(stage);
await setFullscreen(stage); setFullscreen(stage);
render( render(
<Modal title="Download" onClose={() => {}}> <Modal title="Download" onClose={() => {}}>
<input aria-label="name" defaultValue="" /> <input aria-label="name" defaultValue="" />
@@ -42,7 +42,7 @@ describe("a dialog open while fullscreen is left", () => {
field.focus(); field.focus();
expect(stage.contains(field)).toBe(true); expect(stage.contains(field)).toBe(true);
await setFullscreen(null); // the browser's own Esc leaves fullscreen; the dialog stays open setFullscreen(null); // the browser's own Esc leaves fullscreen; the dialog stays open
const after = screen.getByLabelText("name") as HTMLInputElement; const after = screen.getByLabelText("name") as HTMLInputElement;
expect(after.value).toBe("my cut"); expect(after.value).toBe("my cut");
@@ -78,7 +78,7 @@ describe("a dialog with an autoFocus child", () => {
const field = screen.getByLabelText("query"); const field = screen.getByLabelText("query");
expect(document.activeElement).toBe(field); expect(document.activeElement).toBe(field);
await setFullscreen(stage); // the player goes fullscreen under the open dialog setFullscreen(stage); // the player goes fullscreen under the open dialog
expect(stage.contains(field)).toBe(true); expect(stage.contains(field)).toBe(true);
expect(document.activeElement).toBe(field); expect(document.activeElement).toBe(field);
@@ -8,6 +8,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { render, cleanup } from "@testing-library/react"; import { render, cleanup } from "@testing-library/react";
import { LoadingState } from "./QueryState"; import { LoadingState } from "./QueryState";
// The label is not under test; without an i18n instance react-i18next warns on every render.
vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key }) }));
let now = 0; let now = 0;
beforeEach(() => { beforeEach(() => {
vi.spyOn(performance, "now").mockImplementation(() => now); vi.spyOn(performance, "now").mockImplementation(() => now);