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
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,
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
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
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}`
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
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

View file

@ -477,23 +477,40 @@ const browser = await launchBrowser();
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. */
{
const { context, page } = await makePage(browser);
await seed(page, { decks: [] });
await page.goto(BASE, { waitUntil: "networkidle" });
await page.waitForTimeout(800);
const status = await page.evaluate(async () => {
const response = await fetch("/api/push", { headers: { "x-ollama-key": "test-key" } });
return { status: response.status, body: await response.json() };
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"),
});
check("push: status says disabled without VAPID", status.body.enabled === false, JSON.stringify(status.body));
await page.locator("nav.tabbar").getByRole("button").click();
await page.getByRole("dialog").waitFor({ timeout: 6000 });
await page.waitForTimeout(500);
check("push: daily goal setting visible", await page.getByText("Daily goal").isVisible());
check("push: reminder UI hidden", (await page.getByText("Daily reminder").count()) === 0);
await context.close();
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);
await seed(page, { decks: [] });
await page.goto("http://127.0.0.1:3215/", { waitUntil: "networkidle" });
await page.waitForTimeout(800);
const status = await page.evaluate(async () => {
const response = await fetch("/api/push", { headers: { "x-ollama-key": "test-key" } });
return { status: response.status, body: await response.json() };
});
check("push: status says disabled without VAPID", status.body.enabled === false, JSON.stringify(status.body));
await page.locator("nav.tabbar").getByRole("button").click();
await page.getByRole("dialog").waitFor({ timeout: 6000 });
await page.waitForTimeout(500);
check("push: daily goal setting visible", await page.getByText("Daily goal").isVisible());
check("push: reminder UI hidden", (await page.getByText("Daily reminder").count()) === 0);
await context.close();
}
} finally {
app.stop();
}
}
/* 5. reminders with VAPID: status, subscribe, delete, test send ------ */