Infinity Nikki's wiki prints "There are no Events in this category" between versions, and the refresh runner has honoured that since 2026-09-03 — it stores the empty parse as the source's real answer rather than letting a quiet lane reach the broken tier. The feed build never asked, so the same page arrived at CI as parsedCount 0, indistinguishable from a parser that has stopped reading a redesigned page, and brokenSources failed every build while every refresh stayed green. The runner's verdict cannot travel on its own: only a parser has seen the page, and by the time brokenSources runs there is nothing left but the feed. So the fact rides on SourceHealth, defaulted so an older feed the service worker cached keeps validating and reads as the strict answer. Both ends now ask it the same way — of an empty parse only, from the page's own words only — because a redesign yields zero rows too, and excusing that is the silently emptied calendar the gate exists for. The rule sits in a module rather than in build-feed.ts, which writes public/ and so runs a build if a test imports it. That is how the two ends drifted apart with nothing to catch it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
193 lines
7.2 KiB
TypeScript
193 lines
7.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import {
|
|
BUG_URL,
|
|
Colophon,
|
|
FEATURE_URL,
|
|
bugReportUrl,
|
|
} from "../src/client/components/Colophon.tsx";
|
|
|
|
/**
|
|
* The issue forms, and the app's links into them.
|
|
*
|
|
* These pin a seam nothing else can see: GitHub answers a malformed form with a
|
|
* rendered error only a visitor notices, and it drops a prefill parameter that
|
|
* names no field with no error at all. Both failures look exactly like a working
|
|
* link from here, so the only place they can be caught is a test that reads the
|
|
* YAML the links point at.
|
|
*
|
|
* Reads repo files, never build output — see AGENTS.md § Commands.
|
|
*/
|
|
|
|
const DIR = ".github/ISSUE_TEMPLATE";
|
|
|
|
/**
|
|
* Whether the README is in this context at all.
|
|
*
|
|
* The image build runs this suite as its gate, and `.dockerignore` excludes the
|
|
* README along with `docs/` and `AGENTS.md` — deliberately, so that editing a
|
|
* doc does not invalidate the layer that installs, tests and builds. `.github/`
|
|
* is copied in for the opposite reason, which is why every other check here runs
|
|
* there unchanged.
|
|
*
|
|
* So the README check stands down where the file was left out on purpose, rather
|
|
* than failing the image on the absence of something that context does not ship.
|
|
* It still runs in CI and locally, which is where a README link gets edited.
|
|
*/
|
|
const HAS_README = await Bun.file("README.md").exists();
|
|
|
|
type Field = {
|
|
type: string;
|
|
id?: string;
|
|
attributes?: { label?: string };
|
|
validations?: { required?: boolean };
|
|
};
|
|
type Form = { name?: string; description?: string; labels?: string[]; body?: Field[] };
|
|
|
|
async function form(file: string): Promise<Form> {
|
|
const text = await Bun.file(`${DIR}/${file}`).text();
|
|
return Bun.YAML.parse(text) as Form;
|
|
}
|
|
|
|
/** The `template=` filename a link points at, and its prefill parameters. */
|
|
function link(url: string): { template: string | null; params: URLSearchParams } {
|
|
const parsed = new URL(url);
|
|
const params = parsed.searchParams;
|
|
return { template: params.get("template"), params };
|
|
}
|
|
|
|
describe("issue forms", () => {
|
|
for (const file of ["bug_report.yml", "feature_request.yml"]) {
|
|
test(`${file} is a well-formed issue form`, async () => {
|
|
const doc = await form(file);
|
|
|
|
expect(doc.name).toBeTruthy();
|
|
expect(doc.description).toBeTruthy();
|
|
expect(doc.body?.length).toBeGreaterThan(0);
|
|
|
|
for (const field of doc.body ?? []) {
|
|
// GitHub rejects an unknown type outright.
|
|
expect([
|
|
"markdown",
|
|
"input",
|
|
"textarea",
|
|
"dropdown",
|
|
"checkboxes",
|
|
]).toContain(field.type);
|
|
|
|
// Every field that collects an answer needs a label to collect it under,
|
|
// and an id, or its answer cannot be prefilled or referenced.
|
|
if (field.type !== "markdown") {
|
|
expect(field.attributes?.label).toBeTruthy();
|
|
expect(field.id).toBeTruthy();
|
|
}
|
|
}
|
|
});
|
|
|
|
test(`${file} has unique field ids`, async () => {
|
|
const ids = (await form(file)).body?.flatMap((f) => f.id ?? []) ?? [];
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
});
|
|
|
|
test(`${file} asks for something, but not for everything`, async () => {
|
|
const answered = (await form(file)).body?.filter((f) => f.type !== "markdown") ?? [];
|
|
const required = answered.filter((f) => f.validations?.required === true);
|
|
|
|
// At least one required field, or the form accepts an empty report.
|
|
expect(required.length).toBeGreaterThan(0);
|
|
// A form that demands every field is one a reader abandons — and this app
|
|
// has no account to look anything up in, so an abandoned report is the
|
|
// whole loss.
|
|
expect(required.length).toBeLessThan(answered.length);
|
|
});
|
|
|
|
test(`${file} uses only labels a fresh repository already has`, async () => {
|
|
// A template naming a label that does not exist applies nothing, silently.
|
|
for (const label of (await form(file)).labels ?? []) {
|
|
expect(["bug", "enhancement"]).toContain(label);
|
|
}
|
|
});
|
|
}
|
|
|
|
test("config.yml keeps the blank-issue escape hatch open", async () => {
|
|
const text = await Bun.file(`${DIR}/config.yml`).text();
|
|
expect(Bun.YAML.parse(text)).toEqual({ blank_issues_enabled: true });
|
|
});
|
|
});
|
|
|
|
describe("the app's links into them", () => {
|
|
test("each points at a template file that exists", async () => {
|
|
for (const url of [BUG_URL, FEATURE_URL]) {
|
|
const { template } = link(url);
|
|
expect(template).toBeTruthy();
|
|
expect(await Bun.file(`${DIR}/${template}`).exists()).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("the prefilled freshness line names a real field on the bug form", async () => {
|
|
const ids = (await form("bug_report.yml")).body?.flatMap((f) => f.id ?? []) ?? [];
|
|
const { params } = link(bugReportUrl("15 Aug 2026, 04:12 — 2 days ago"));
|
|
|
|
for (const key of params.keys()) {
|
|
if (key === "template") continue;
|
|
// The failure this catches: renaming the field id leaves the link working
|
|
// and the prefill gone, with nothing anywhere to say so.
|
|
expect(ids).toContain(key);
|
|
}
|
|
expect(params.get("refreshed")).toBe("15 Aug 2026, 04:12 — 2 days ago");
|
|
});
|
|
|
|
test.skipIf(!HAS_README)("every template link in the README names a template that exists", async () => {
|
|
// A `?template=` that names nothing lands the reader on the chooser instead,
|
|
// which looks close enough to working that nobody reports it.
|
|
const readme = await Bun.file("README.md").text();
|
|
const named = [...readme.matchAll(/issues\/new\?template=([\w.-]+)/g)].map(
|
|
(m) => m[1],
|
|
);
|
|
|
|
expect(named.length).toBeGreaterThan(0);
|
|
for (const template of named) {
|
|
expect(await Bun.file(`${DIR}/${template}`).exists()).toBe(true);
|
|
}
|
|
// Both forms are reachable from the README, not just the one.
|
|
expect(new Set(named)).toEqual(
|
|
new Set(["bug_report.yml", "feature_request.yml"]),
|
|
);
|
|
});
|
|
|
|
test("the footer offers both, and carries its own freshness line into the bug form", () => {
|
|
const NOW = Date.parse("2026-08-17T12:00:00.000Z");
|
|
const html = renderToStaticMarkup(
|
|
<Colophon
|
|
sources={[
|
|
{
|
|
sourceId: "genshin-game8-events",
|
|
game: "genshin",
|
|
url: "https://game8.co/games/Genshin-Impact/archives/301601",
|
|
lastSuccessAt: new Date(NOW - 3 * 60 * 60 * 1000).toISOString(),
|
|
eventCount: 9,
|
|
|
|
parsedCount: 9,
|
|
statesNoEvents: false,
|
|
},
|
|
]}
|
|
now={NOW}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Report a problem");
|
|
expect(html).toContain("request a feature");
|
|
expect(html).toContain("template=feature_request.yml");
|
|
// The age the reader is being shown is the age the report will carry.
|
|
expect(html).toContain("3h 0m ago");
|
|
expect(html).toContain(encodeURIComponent("3h 0m ago"));
|
|
});
|
|
|
|
test("an unfetched feed prefills nothing rather than an empty value", () => {
|
|
// "" in the field reads as an answer the reader gave, and a report claiming
|
|
// the app never refreshed is worse than one that leaves the question open.
|
|
expect(bugReportUrl(null)).toBe(BUG_URL);
|
|
expect(link(bugReportUrl(null)).params.has("refreshed")).toBe(false);
|
|
});
|
|
});
|