Reminder tests own their VAPID state (the gate runs with keys set)

The deploy gate runs the suites against a release that reads the production
env — which now carries VAPID keys — so phase 2's "reminders are hidden
without VAPID" check failed there and the gate (correctly) refused to switch.

That check now runs on its own instance with VAPID_PUBLIC_KEY/_PRIVATE_KEY
forced empty, so it pins the unconfigured state whatever the operator's env
holds. Verified twice: with no VAPID in the environment and with keys exported
exactly like the deploy shell does (83 checks both ways).
This commit is contained in:
Martin Dvořák 2026-10-18 14:53:30 +00:00
commit d72b335965
2 changed files with 34 additions and 17 deletions

View file

@ -1529,7 +1529,7 @@ Things I chose while building, with the reasoning:
ever sent while three really are. The settings copy says what the server now holds, the ever sent while three really are. The settings copy says what the server now holds, the
privacy page and `.env.example` say it too, and the phase-2 suite pins the whole path with a privacy page and `.env.example` say it too, and the phase-2 suite pins the whole path with a
faked browser subscription: personal text in both languages, the generic fallback in both, faked browser subscription: personal text in both languages, the generic fallback in both,
and the sender's preference order (81 checks). and the sender's preference order (83 checks).
109. **Teach it back: the AI stops being the tutor and becomes the student.** The mode where the user 109. **Teach it back: the AI stops being the tutor and becomes the student.** The mode where the user
explains a topic and an AI classmate probes the gaps — never a grader, never lecturing. explains a topic and an AI classmate probes the gaps — never a grader, never lecturing.
@ -2287,7 +2287,7 @@ Things I chose while building, with the reasoning:
collapsed the session mid-run). Push reminders are all-or-nothing: no VAPID keys → no UI, no collapsed the session mid-run). Push reminders are all-or-nothing: no VAPID keys → no UI, no
endpoints, nothing to misconfigure; `web-push` is the one added server dependency and the daily endpoints, nothing to misconfigure; `web-push` is the one added server dependency and the daily
send is an explicit cron/timer script — the shipped `deploy/freezmo-reminders.{service,timer}` send is an explicit cron/timer script — the shipped `deploy/freezmo-reminders.{service,timer}`
pair. Verified by the phase-2 suite (81 checks: history and SRS shapes, the review/mistakes pair. Verified by the phase-2 suite (83 checks: history and SRS shapes, the review/mistakes
sessions, stats math, exam with timer and breakdowns, the VAPID-less hidden state, the sessions, stats math, exam with timer and breakdowns, the VAPID-less hidden state, the
configured subscribe/validate/test/delete path, and the reminder each device composes from its configured subscribe/validate/test/delete path, and the reminder each device composes from its
own notes in both languages; the suite runs its own server instance on a separate port and own notes in both languages; the suite runs its own server instance on a separate port and

View file

@ -477,11 +477,24 @@ const browser = await launchBrowser();
await context.close(); await context.close();
} }
/* 4. reminders hidden without VAPID ---------------------------------- */ /* 4. reminders hidden without VAPID ----------------------------------
Its own instance, with the VAPID variables forced empty: an operator's env
(production's, or the deploy gate's release) may well have keys, and this
check is about the state where reminders do not exist at all. */
{ {
check("push: hidden-state port is free", !(await portInUse(3215)));
const app = spawnApp(3215, {
VAPID_PUBLIC_KEY: "",
VAPID_PRIVATE_KEY: "",
PUSH_DIR: path.join(REPO, "tests", ".tmp", "push-off"),
});
try {
const up = await waitForHttp("http://127.0.0.1:3215/api/health");
check("push: instance without VAPID is up", up);
if (up) {
const { context, page } = await makePage(browser); const { context, page } = await makePage(browser);
await seed(page, { decks: [] }); await seed(page, { decks: [] });
await page.goto(BASE, { waitUntil: "networkidle" }); await page.goto("http://127.0.0.1:3215/", { waitUntil: "networkidle" });
await page.waitForTimeout(800); await page.waitForTimeout(800);
const status = await page.evaluate(async () => { const status = await page.evaluate(async () => {
const response = await fetch("/api/push", { headers: { "x-ollama-key": "test-key" } }); const response = await fetch("/api/push", { headers: { "x-ollama-key": "test-key" } });
@ -494,6 +507,10 @@ const browser = await launchBrowser();
check("push: daily goal setting visible", await page.getByText("Daily goal").isVisible()); check("push: daily goal setting visible", await page.getByText("Daily goal").isVisible());
check("push: reminder UI hidden", (await page.getByText("Daily reminder").count()) === 0); check("push: reminder UI hidden", (await page.getByText("Daily reminder").count()) === 0);
await context.close(); await context.close();
}
} finally {
app.stop();
}
} }
/* 5. reminders with VAPID: status, subscribe, delete, test send ------ */ /* 5. reminders with VAPID: status, subscribe, delete, test send ------ */