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", () => { it("the Gitea a11y workflow uses the pnpm store cache before installing", () => { const workflow = ".gitea/workflows/a11y.yml"; const source = readWorkflow(workflow); const configureStoreIndex = source.indexOf("pnpm config set store-dir .pnpm-store"); const cacheIndex = source.search(/uses: actions\/cache@[0-9a-f]{40}/); 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`); }); }); describe("e2e readiness workflows", () => { it("the Gitea readiness workflow runs the browser readiness suite in fail-closed mode", () => { const workflow = ".gitea/workflows/e2e-readiness.yml"; 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("customer_portal_invitation:"), `${workflow} must expose the disposable VA-P26a invitation opt-in`); 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 checkout success manual dispatch opt-in`); assert.ok(source.includes("subscription_success_path:"), `${workflow} must expose the 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_LOGS_PATH: /e2e-readiness/settings/integrations/api/logs", "E2E_PLUGIN_SETTINGS_PATH: /e2e-readiness/settings/integrations/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`); const invitationStep = source.slice( source.indexOf("- name: Run operator-gated VA-P26a invitation lifecycle"), source.indexOf("- name: Upload Playwright traces") ); assert.match(invitationStep, /github\.event_name == 'workflow_dispatch'/u, `${workflow} must never run the invitation proof on schedule`); assert.match(invitationStep, /inputs\.customer_portal_invitation/u, `${workflow} must bind the explicit invitation dispatch opt-in`); for (const contract of [ 'E2E_ALLOW_CUSTOMER_PORTAL_P26A_INVITATION_MUTATION: "1"', 'REQUIRE_E2E_MUTATING_READINESS: "1"', "E2E_PORTAL_INVITATION_ADMIN_EMAIL", "E2E_PORTAL_INVITATION_ADMIN_PASSWORD", "E2E_PORTAL_INVITATION_SETUP_PASSWORD", "E2E_PORTAL_SETUP_MAIL_FIXTURE_URL", "E2E_PORTAL_SETUP_MAIL_FIXTURE_TOKEN", "customer-portal-invitation+${correlation_key}@saas-ctrl.com", "pnpm run verify:e2e-customer-portal-invitation", ]) { assert.ok(invitationStep.includes(contract), `${workflow} invitation step missing ${contract}`); } assert.doesNotMatch(invitationStep, /GITHUB_ENV/u, `${workflow} must keep invitation secrets and recipient scoped to one shell step`); }); 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("HIVEGRID_GITEA_HOST_IP")); assert.ok(source.includes("git.tool-ctrl.de")); }); });