mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
The determinism guard reads the job that runs the browser, which is now the shard matrix rather than the gate that only carries the required check name. The report artifact is named per shard so the two uploads cannot clobber each other.
167 lines
5.6 KiB
TypeScript
167 lines
5.6 KiB
TypeScript
import { existsSync, readFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { parse } from "yaml";
|
|
|
|
import integrationConfig from "../../vitest.integration.config.mts";
|
|
import unitConfig from "../../vitest.config.mts";
|
|
|
|
function repositoryRoot(): string {
|
|
const testPath = expect.getState().testPath;
|
|
if (!testPath) throw new Error("Vitest did not expose the current test path");
|
|
return join(dirname(testPath), "../..");
|
|
}
|
|
|
|
function readRepoFile(path: string): string {
|
|
return readFileSync(join(repositoryRoot(), path), "utf8");
|
|
}
|
|
|
|
type WorkflowStep = {
|
|
if?: string;
|
|
uses?: string;
|
|
with?: Record<string, string | number>;
|
|
};
|
|
|
|
type WorkflowJob = {
|
|
env?: Record<string, string>;
|
|
steps?: WorkflowStep[];
|
|
};
|
|
|
|
type Workflow = {
|
|
jobs: Record<string, WorkflowJob>;
|
|
};
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
vi.resetModules();
|
|
});
|
|
|
|
describe("Playwright CI determinism", () => {
|
|
it("fails CI when a retry resolves a flaky test", async () => {
|
|
vi.stubEnv("CI", "true");
|
|
// Re-import after stubbing CI so config conditionals are evaluated as CI.
|
|
const { default: config } = await import("../../playwright.config");
|
|
|
|
expect(config.retries).toBe(2);
|
|
expect(config.failOnFlakyTests).toBe(true);
|
|
expect(config.reporter).toEqual([["github"], ["html", { open: "never" }]]);
|
|
// Two servers, and which is which matters. The suite's own server runs
|
|
// with the dashboard's RSC prefetch OFF so `page.route` fixtures govern
|
|
// what the dashboard paints; the second runs it ON, which is the default
|
|
// and therefore the only configuration self-hosters ever see. A React #418
|
|
// hydration bailout shipped on `/` precisely because nothing in CI ran the
|
|
// shipped one. Pin both so that gap cannot reopen by deletion.
|
|
const servers = config.webServer;
|
|
expect(Array.isArray(servers)).toBe(true);
|
|
const webServers = servers as Array<Record<string, unknown>>;
|
|
expect(webServers).toHaveLength(2);
|
|
for (const server of webServers) {
|
|
expect(server).toMatchObject({
|
|
command: expect.stringContaining(
|
|
`${JSON.stringify(process.execPath)} .next/standalone/server.js`,
|
|
),
|
|
env: expect.objectContaining({
|
|
NATIVE_CANVAS: "off",
|
|
}),
|
|
});
|
|
}
|
|
expect(webServers[0]!.env).toMatchObject({
|
|
DASHBOARD_SSR_PREFETCH: "false",
|
|
});
|
|
expect(webServers[1]!.env).toMatchObject({
|
|
DASHBOARD_SSR_PREFETCH: "true",
|
|
});
|
|
});
|
|
|
|
it("retains the Playwright report after every workflow outcome", () => {
|
|
const workflow = parse(
|
|
readRepoFile(".github/workflows/e2e.yml"),
|
|
) as Workflow;
|
|
// The suite runs as a shard matrix since the single job outgrew its
|
|
// timeout; the running job is `shard`, while `e2e` is the gate that
|
|
// carries the required check name and runs no browser. The report
|
|
// therefore lives on the shard job, named per shard so the two
|
|
// artifacts cannot clobber each other.
|
|
const upload = workflow.jobs.shard?.steps?.find((step) =>
|
|
step.uses?.startsWith("actions/upload-artifact@"),
|
|
);
|
|
|
|
expect(upload).toBeDefined();
|
|
expect(upload?.if).toBe("always()");
|
|
expect(upload?.with).toMatchObject({
|
|
name: "playwright-report-shard-${{ matrix.shard }}",
|
|
path: "playwright-report/",
|
|
"retention-days": 7,
|
|
"if-no-files-found": "ignore",
|
|
});
|
|
});
|
|
|
|
it("uses production-valid secrets and plain-HTTP cookies", () => {
|
|
const workflow = parse(
|
|
readRepoFile(".github/workflows/e2e.yml"),
|
|
) as Workflow;
|
|
const env = workflow.jobs.shard?.env;
|
|
|
|
expect(env?.API_TOKEN_HMAC_KEY).toMatch(/^[0-9a-f]{64}$/);
|
|
expect(env?.SESSION_COOKIE_SECURE).toBe("false");
|
|
});
|
|
});
|
|
|
|
describe("local worktree isolation", () => {
|
|
it("keeps nested worktree suites out of the root unit run", () => {
|
|
expect(unitConfig.test?.exclude ?? []).toContain(".worktrees/**");
|
|
});
|
|
});
|
|
|
|
describe("integration environment isolation", () => {
|
|
it("registers a worker setup bridge and authoritative test secrets", () => {
|
|
const test = integrationConfig.test;
|
|
|
|
expect(test?.setupFiles ?? []).toContain(
|
|
"./tests/integration/environment-setup.ts",
|
|
);
|
|
expect(test?.env).toMatchObject({
|
|
TZ: "UTC",
|
|
ENCRYPTION_KEY:
|
|
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
ENCRYPTION_KEYS: "",
|
|
ENCRYPTION_ACTIVE_KEY_ID: "",
|
|
API_TOKEN_HMAC_KEY:
|
|
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
|
SESSION_SECRET: "integration-test-session-secret-32-bytes",
|
|
});
|
|
expect(
|
|
existsSync(
|
|
join(repositoryRoot(), "tests/integration/environment-setup.ts"),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("container dependency installation", () => {
|
|
it("copies the prepare helper before the Git-free frozen install", () => {
|
|
const dockerfile = readRepoFile("Dockerfile");
|
|
|
|
expect(dockerfile).toContain(
|
|
"COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./\n" +
|
|
"COPY scripts/prepare.mjs scripts/prepare.mjs\n" +
|
|
"RUN pnpm install --frozen-lockfile --prod=false",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("production dependency advisory floors", () => {
|
|
it("pins vulnerable transitive ranges to compatible patched versions", () => {
|
|
const workspace = parse(readRepoFile("pnpm-workspace.yaml")) as {
|
|
overrides?: Record<string, string>;
|
|
};
|
|
|
|
expect(workspace.overrides).toMatchObject({
|
|
"dompurify@<=3.4.10": "^3.4.11",
|
|
"postcss@<8.5.10": "^8.5.10",
|
|
"@hono/node-server@<1.19.13": "^1.19.13",
|
|
"hono@<4.12.25": "^4.12.25",
|
|
});
|
|
});
|
|
});
|