import fs from "node:fs"; import path from "node:path"; import assert from "node:assert/strict"; import { describe, it } from "vitest"; import { missingReadinessEnv } from "./run-playwright-a11y.mjs"; const repoRoot = process.cwd(); const readinessWorkflowEnvKeys = missingReadinessEnv({ env: {} }); function readWorkflow(relativePath) { return fs.readFileSync(path.join(repoRoot, relativePath), "utf8"); } describe("a11y workflow install cache", () => { for (const workflow of [".github/workflows/a11y.yml", ".gitea/workflows/a11y.yml"]) { it(`${workflow} uses the pnpm store cache before installing`, () => { const source = readWorkflow(workflow); const configureStoreIndex = source.indexOf("pnpm config set store-dir .pnpm-store"); const cacheIndex = source.indexOf("uses: actions/cache@v4"); const installIndex = source.indexOf("pnpm install --frozen-lockfile --prefer-offline"); assert.ok(configureStoreIndex > 0, `${workflow} must configure .pnpm-store`); assert.ok(cacheIndex > configureStoreIndex, `${workflow} must cache .pnpm-store after configuring it`); assert.ok(source.includes("path: .pnpm-store"), `${workflow} must cache the pnpm store path`); assert.ok(source.includes("key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}"), `${workflow} must key cache by pnpm lockfile`); assert.ok(installIndex > cacheIndex, `${workflow} must install with --prefer-offline after cache restore`); }); } it(".github/workflows/a11y.yml cancels superseded PR runs without path-filtering required checks", () => { const source = readWorkflow(".github/workflows/a11y.yml"); assert.match(source, /^concurrency:\s*$/m, "GitHub a11y workflow must cancel superseded runs"); assert.ok( source.includes("group: a11y-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"), "GitHub a11y workflow must scope concurrency by workflow and PR/ref", ); assert.match(source, /^\s+cancel-in-progress:\s*true\s*$/m, "GitHub a11y workflow must cancel in-progress duplicates"); assert.doesNotMatch(source, /^\s+paths:\s*$/m, "GitHub required a11y checks must not be skipped by path filters"); assert.doesNotMatch(source, /^\s+paths-ignore:\s*$/m, "GitHub required a11y checks must not be skipped by path-ignore filters"); }); }); describe("e2e readiness workflows", () => { for (const workflow of [".github/workflows/e2e-readiness.yml", ".gitea/workflows/e2e-readiness.yml"]) { it(`${workflow} runs the browser readiness suite in fail-closed mode`, () => { const source = readWorkflow(workflow); assert.match(source, /^ workflow_dispatch:\s*$/m, `${workflow} must support manual operator dispatch`); assert.match(source, /^ schedule:\s*$/m, `${workflow} must run on a schedule`); assert.ok(source.includes("pnpm config set store-dir .pnpm-store"), `${workflow} must configure the pnpm store`); assert.ok(source.includes("path: .cache/ms-playwright"), `${workflow} must cache Playwright browsers`); assert.ok(source.includes("pnpm install --frozen-lockfile --prefer-offline"), `${workflow} must install deterministically`); assert.ok(source.includes("pnpm exec playwright install --with-deps chromium webkit"), `${workflow} must install every browser used by E2E_BROWSER_MATRIX=1`); assert.ok(source.includes("pnpm run verify:e2e-a11y"), `${workflow} must run the strict Playwright wrapper`); assert.ok(source.includes("plugin_lifecycle_mutation:"), `${workflow} must expose the plugin lifecycle mutation manual dispatch opt-in`); assert.ok(source.includes("plugin_settings_write:"), `${workflow} must expose the plugin settings write manual dispatch opt-in`); assert.ok(source.includes("workflow_outbox_drain:"), `${workflow} must expose the workflow outbox-drain manual dispatch opt-in`); assert.ok(source.includes("workflow_webhook_run:"), `${workflow} must expose the real workflow webhook manual dispatch opt-in`); assert.ok(source.includes("workflow_webhook_target_url:"), `${workflow} must expose the real workflow webhook target URL input`); assert.ok(source.includes("subscription_success:"), `${workflow} must expose the Stripe checkout success manual dispatch opt-in`); assert.ok(source.includes("subscription_success_path:"), `${workflow} must expose the Stripe checkout success return path input`); assert.ok(source.includes("Apply manual operator E2E opt-ins"), `${workflow} must apply manual operator opt-ins before running Playwright`); for (const requiredEnv of [ 'REQUIRE_E2E_A11Y: "1"', 'REQUIRE_AUTH_E2E: "1"', 'REQUIRE_E2E_READINESS: "1"', 'E2E_A11Y_STRICT: "1"', 'E2E_BROWSER_MATRIX: "1"', 'E2E_ALLOW_WORKFLOW_RUN: "1"', "E2E_TENANT_EMAIL: ${{ secrets.E2E_TENANT_EMAIL }}", "E2E_TENANT_PASSWORD: ${{ secrets.E2E_TENANT_PASSWORD }}", "E2E_PLUGIN_READONLY_EMAIL: ${{ secrets.E2E_PLUGIN_READONLY_EMAIL }}", "E2E_PLUGIN_READONLY_PASSWORD: ${{ secrets.E2E_PLUGIN_READONLY_PASSWORD }}", "E2E_TENANT_API_DOCS_PATH: /e2e-readiness/settings/api/docs", "E2E_TENANT_API_LOGS_PATH: /e2e-readiness/settings/api/logs", "E2E_PLUGIN_SETTINGS_PATH: /e2e-readiness/settings/plugins", "E2E_WORKFLOW_ENGINE_PATH: /e2e-readiness/workflow-engine/workflows", "E2E_WORKFLOW_RUN_API_PATH: /e2e-readiness/workflow-engine/workflows/wf_123/run/api", "E2E_WORKFLOW_WEBHOOK_BUILDER_PATH: /e2e-readiness/workflow-engine/workflows/wf_webhook_123", "E2E_WORKFLOW_WEBHOOK_RUN_MANUAL_PATH: /e2e-readiness/workflow-engine/workflows/wf_webhook_123/run/manual", "PLUGIN_BINARY_BASE_PATH: /tmp/e2e-plugins", ]) { assert.ok(source.includes(requiredEnv), `${workflow} missing ${requiredEnv}`); } for (const optInExport of [ "E2E_ALLOW_PLUGIN_LIFECYCLE_MUTATION=1", "E2E_ALLOW_PLUGIN_SETTINGS_WRITE=1", "E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN=1", "E2E_ALLOW_WORKFLOW_WEBHOOK_RUN=1", "E2E_WORKFLOW_WEBHOOK_TARGET_URL=%s", "E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1", "E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH=%s", ]) { assert.ok(source.includes(optInExport), `${workflow} must export ${optInExport} for manual dispatch`); } for (const requiredKey of readinessWorkflowEnvKeys) { assert.match(source, new RegExp(`^\\s+${requiredKey}:\\s+`, "m"), `${workflow} missing strict readiness env key ${requiredKey}`); } assert.doesNotMatch(source, /SKIP_E2E_A11Y:\s*"?1"?/, `${workflow} must not silently skip the browser suite`); assert.ok(source.includes("path: test-results/"), `${workflow} must upload Playwright traces`); }); } it("the Gitea readiness workflow uses the self-hosted runner and host resolver", () => { const source = readWorkflow(".gitea/workflows/e2e-readiness.yml"); assert.ok(source.includes("runs-on: toolctrl-ci")); assert.ok(source.includes("TOOLCTRL_GITEA_HOST_IP")); assert.ok(source.includes("git.tool-ctrl.de")); }); });