| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import assert from 'node:assert/strict'; |
| import { readdirSync, readFileSync } from 'node:fs'; |
| import test from 'node:test'; |
| |
| import { formatGitHubOutputs, loadWorkspaceGraph, planTests } from './ci-test-plan.mjs'; |
| |
| const dirs = [ |
| 'packages/core', |
| 'packages/storage', |
| 'packages/runtime', |
| 'packages/runtime-host', |
| 'packages/cli', |
| 'packages/ui', |
| 'apps/desktop', |
| ]; |
| |
| const graph = { |
| dirs, |
| dependents: new Map([ |
| ['packages/core', new Set(['packages/storage', 'packages/runtime'])], |
| ['packages/storage', new Set(['packages/runtime', 'packages/runtime-host'])], |
| ['packages/runtime', new Set(['packages/runtime-host', 'packages/cli', 'apps/desktop'])], |
| ['packages/runtime-host', new Set(['packages/cli', 'apps/desktop'])], |
| ['packages/cli', new Set()], |
| ['packages/ui', new Set(['apps/desktop'])], |
| ['apps/desktop', new Set()], |
| ]), |
| testDirs: new Set(dirs), |
| }; |
| |
| test('documentation-only changes do not select code validation', () => { |
| const plan = planTests(['docs/ci.md'], { graph }); |
| |
| assert.equal(plan.code, false); |
| assert.equal(plan.asfSource, false); |
| assert.equal(plan.astryxSurface, false); |
| assert.deepEqual(plan.workspaces, []); |
| }); |
| |
| test('the Astryx inventory can run without selecting the code suite', () => { |
| const plan = planTests(['docs/astryx-surface-file-inventory.md'], { graph }); |
| |
| assert.equal(plan.code, false); |
| assert.equal(plan.astryxSurface, true); |
| }); |
| |
| test('desktop renderer changes retain Electron and Storybook coverage', () => { |
| const plan = planTests(['apps/desktop/src/renderer/app.tsx'], { graph }); |
| |
| assert.equal(plan.code, true); |
| assert.equal(plan.e2e, true); |
| assert.equal(plan.storybook, true); |
| assert.equal(plan.astryxSurface, true); |
| assert.deepEqual(plan.standardWorkspaces, ['apps/desktop']); |
| }); |
| |
| test('Storybook catalog changes avoid real-window E2E and workspace tests', () => { |
| const plan = planTests(['apps/desktop/stories/settings.stories.tsx'], { graph }); |
| |
| assert.equal(plan.code, true); |
| assert.equal(plan.e2e, false); |
| assert.equal(plan.storybook, true); |
| assert.deepEqual(plan.workspaces, []); |
| }); |
| |
| test('AX audit contract test edits avoid the Storybook browser pipeline', () => { |
| const plan = planTests(['scripts/ax-tree-audit.test.mjs'], { graph }); |
| assert.equal(plan.storybook, false); |
| assert.equal(plan.e2e, false); |
| }); |
| |
| test('runtime changes retain the dedicated Runtime Host lane', () => { |
| const plan = planTests(['packages/runtime/src/runtime.ts'], { graph }); |
| |
| assert.equal(plan.runtimeHost, true); |
| assert.equal(plan.runtimeSandbox, true); |
| assert.deepEqual(plan.standardWorkspaces, ['packages/runtime', 'packages/cli', 'apps/desktop']); |
| }); |
| |
| test('CLI release inputs select installed-package validation', () => { |
| const plan = planTests(['packages/runtime/src/runtime.ts'], { graph }); |
| |
| assert.equal(plan.cliPackage, true); |
| }); |
| |
| test('release metadata selects only the gate that consumes it', () => { |
| for (const path of ['LICENSE', 'NOTICE']) { |
| const plan = planTests([path], { graph }); |
| assert.equal(plan.cliPackage, true, path); |
| assert.equal(plan.releaseContract, true, path); |
| assert.equal(plan.asfSource, true, path); |
| } |
| }); |
| |
| test('source legal authority and generated provenance select the ASF source gate', () => { |
| for (const path of [ |
| 'DISCLAIMER-WIP', |
| 'biome.jsonc', |
| 'patches/node-pty+1.2.0-beta.15.patch', |
| 'apps/desktop/src/renderer/assets/provider-brands/example.svg', |
| 'apps/desktop/resources/licenses/renderer/SIMPLE_ICONS_LICENSE.md', |
| 'packages/eval/harbor/deepseek-harness-profile/cordis.patch.yml', |
| 'packages/core/src/model-metadata.generated.ts', |
| 'packages/runtime/src/telemetry/model-pricing.generated.ts', |
| 'scripts/model-metadata/models-dev-api.snapshot.json', |
| 'scripts/sync-model-metadata.mjs', |
| ]) { |
| assert.equal(planTests([path], { graph }).asfSource, true, path); |
| } |
| }); |
| |
| test('release authority changes select their dedicated contract gate', () => { |
| for (const path of [ |
| 'apps/desktop/src/main/app-update-test-context.ts', |
| 'apps/desktop/build/entitlements.mac.plist', |
| 'apps/desktop/electron-builder.config.mjs', |
| 'apps/desktop/package.json', |
| '.github/workflows/cli-package-validation.yml', |
| '.github/workflows/release-cli-finalize.yml', |
| '.github/workflows/release-cli-stage.yml', |
| '.github/workflows/release.yml', |
| 'scripts/package-macos-arm64.mjs', |
| 'scripts/package-macos-autoupdate-next.mjs', |
| 'scripts/package-macos-arm64-cli.mjs', |
| 'scripts/package-windows-x64.mjs', |
| 'scripts/prepare-windows-upgrade-baseline.mjs', |
| 'scripts/product-release-artifacts.mjs', |
| 'scripts/product-release-authority.mjs', |
| 'scripts/product-release-authority.test.mjs', |
| 'scripts/product-release-identity.mjs', |
| 'scripts/product-release-tag.mjs', |
| 'scripts/product-release.test.mjs', |
| 'scripts/release-eval-smoke-sitecustomize.py', |
| 'scripts/release-version.mjs', |
| 'scripts/release-cli-publication.test.mjs', |
| 'scripts/verify-macos-arm64-cli.mjs', |
| 'scripts/verify-macos-arm64-dmg.mjs', |
| 'scripts/verify-macos-autoupdate.mjs', |
| 'scripts/desktop-update-contract.mjs', |
| 'scripts/verify-packaged-app.mjs', |
| 'scripts/verify-windows-x64.mjs', |
| 'scripts/windows-upgrade-baseline.json', |
| 'scripts/windows-package-source-closure.mjs', |
| 'scripts/windows-package-source-closure.test.mjs', |
| ]) { |
| assert.equal(planTests([path], { graph }).releaseContract, true, path); |
| } |
| assert.equal(planTests(['.github/RELEASE_CHECKLIST.md'], { graph }).releaseContract, false); |
| }); |
| |
| // Both notices are committed generator output. A hand edit or a merge-conflict |
| // resolution can corrupt either one, and `check:release` is what regenerates |
| // and diffs them, so both must reach that gate — the desktop notice lives |
| // outside `packages/cli/**` and would otherwise reach no gate at all. |
| test('both committed third-party notices reach the release gate', () => { |
| for (const path of [ |
| 'apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt', |
| 'packages/cli/THIRD_PARTY_NOTICES.txt', |
| ]) { |
| assert.equal(planTests([path], { graph }).releaseContract, true, path); |
| } |
| }); |
| |
| // The test only reads the generator, so it belongs to the release contract and |
| // not to the CLI package gate, whose tarball build and install smoke prove |
| // nothing about a test-only edit. |
| test('the notice regression test selects the release gate alone', () => { |
| const plan = planTests(['scripts/generate-third-party-notices.test.mjs'], { graph }); |
| assert.equal(plan.releaseContract, true); |
| assert.equal(plan.cliPackage, false); |
| }); |
| |
| test('ASF source authority changes select their dedicated gate', () => { |
| for (const path of [ |
| '.gitattributes', |
| '.github/workflows/asf-source-candidate.yml', |
| 'docs/code-origin-audit.md', |
| 'scripts/asf-source-release.mjs', |
| 'scripts/asf-source-release.test.mjs', |
| ]) { |
| assert.equal(planTests([path], { graph }).asfSource, true, path); |
| } |
| assert.equal(planTests(['.github/ASF_SOURCE_RELEASE.md'], { graph }).asfSource, false); |
| assert.equal(planTests(['scripts/audit-alignment.mjs'], { graph }).asfSource, false); |
| }); |
| |
| test('shared CLI validation changes select installed-package validation', () => { |
| const plan = planTests(['.github/workflows/cli-package-validation.yml'], { graph }); |
| |
| assert.equal(plan.cliPackage, true); |
| }); |
| |
| test('desktop-only changes skip installed-package validation', () => { |
| assert.equal(planTests(['apps/desktop/src/main.ts'], { graph }).cliPackage, false); |
| }); |
| |
| test('full selection covers every live surface', () => { |
| const plan = planTests([], { graph, forceFull: true }); |
| |
| assert.equal(plan.full, true); |
| assert.equal(plan.asfSource, true); |
| assert.equal(plan.cliPackage, true); |
| assert.equal(plan.code, true); |
| assert.equal(plan.e2e, true); |
| assert.equal(plan.storybook, true); |
| assert.equal(plan.runtimeHost, true); |
| assert.equal(plan.releaseContract, true); |
| assert.deepEqual(plan.workspaces, dirs); |
| }); |
| |
| test('unknown top-level code fails safe to full selection', () => { |
| assert.equal(planTests(['unknown.config'], { graph }).full, true); |
| }); |
| |
| test('full-suite authority files select every surface', () => { |
| for (const path of ['package-lock.json', '.github/workflows/ci.yml']) { |
| assert.equal(planTests([path], { graph }).full, true, path); |
| } |
| }); |
| |
| test('GitHub output matches the selections consumed by CI', () => { |
| const output = formatGitHubOutputs(planTests([], { graph, forceFull: true })); |
| const outputKeys = new Set(output.split('\n').map((line) => line.split('=', 1)[0])); |
| const workflow = readWorkflow('ci.yml'); |
| const consumedKeys = new Set( |
| [...workflow.matchAll(/steps\.plan\.outputs\.([a-z0-9_]+)/gu)].map((match) => match[1]), |
| ); |
| |
| assert.deepEqual(outputKeys, consumedKeys); |
| }); |
| |
| test('core CI validates pull requests and the resulting main branch state', () => { |
| const workflow = readWorkflow('ci.yml'); |
| |
| assert.match(workflow, /pull_request:\n\s+branches: \[main\]/u); |
| assert.match(workflow, /push:\n\s+branches: \[main\]/u); |
| assert.match( |
| workflow, |
| /BASE_SHA: \$\{\{ github\.event_name == 'push' && github\.event\.before \|\| github\.event\.pull_request\.base\.sha \}\}/u, |
| ); |
| assert.match( |
| workflow, |
| /HEAD_SHA: \$\{\{ github\.event_name == 'push' && github\.sha \|\| github\.event\.pull_request\.head\.sha \}\}/u, |
| ); |
| assert.match(workflow, /\[\[ "\$BASE_SHA" =~ \^0\+\$ \]\]/u); |
| }); |
| |
| test('core CI uses the Windows inventory package-script authority', () => { |
| const workflow = readWorkflow('ci.yml'); |
| |
| assert.match(workflow, /run: npm run windows:inventory/u); |
| assert.doesNotMatch(workflow, /run: node scripts\/windows-test-inventory\.mjs --check/u); |
| }); |
| |
| test('contract checks run before dependency setup and can fail the job', () => { |
| const workflow = readWorkflow('ci.yml'); |
| const setupNodeStart = workflow.indexOf(' - uses: actions/setup-node@'); |
| |
| // These contracts need nothing but the checkout, so they run on every change |
| // rather than behind a surface flag — and a gate that cannot fail the job is |
| // not a gate. |
| for (const name of [ |
| 'Test CI planner', |
| 'Check Windows test inventory', |
| 'Verify ASF npm preflight policy', |
| ]) { |
| const start = workflow.indexOf(` - name: ${name}\n`); |
| assert.ok(start >= 0, name); |
| assert.ok(start < setupNodeStart, name); |
| |
| const step = workflow.slice(start, workflow.indexOf('\n - ', start + 1)); |
| assert.doesNotMatch(step, /\n\s+if:/u, name); |
| assert.doesNotMatch(step, /continue-on-error/u, name); |
| } |
| }); |
| |
| test('core CI checks the Astryx inventory for every code change before building', () => { |
| const workflow = readWorkflow('ci.yml'); |
| const inventoryStart = workflow.indexOf(' - name: Astryx surface inventory\n'); |
| const inventoryEnd = workflow.indexOf('\n - ', inventoryStart + 1); |
| const buildStart = workflow.indexOf(' - name: Build\n'); |
| |
| assert.ok(inventoryStart >= 0); |
| assert.ok(inventoryStart < buildStart); |
| |
| const inventoryStep = workflow.slice(inventoryStart, inventoryEnd); |
| assert.match( |
| inventoryStep, |
| /if: steps\.plan\.outputs\.code == 'true' \|\| steps\.plan\.outputs\.astryx_surface == 'true'/u, |
| ); |
| assert.doesNotMatch(inventoryStep, /continue-on-error/u); |
| }); |
| |
| test('core CI validates affected installed CLI packages on its existing runner', () => { |
| const workflow = readWorkflow('ci.yml'); |
| const toolchain = workflow.indexOf( |
| 'npm install --global --no-audit --no-fund "$(node -p \'require("./package.json").packageManager\')"', |
| ); |
| const pack = workflow.indexOf('run: npm run release:cli:pack'); |
| |
| assert.match(workflow, /if: steps\.plan\.outputs\.cli_package == 'true'/u); |
| assert.ok(toolchain >= 0); |
| assert.ok(toolchain < pack); |
| assert.match(workflow, /run: npm run release:cli:smoke/u); |
| }); |
| |
| test('release contracts run against built CLI outputs', () => { |
| const workflow = readWorkflow('ci.yml'); |
| const buildIndex = workflow.indexOf(' - name: Build\n'); |
| const buildEnd = workflow.indexOf('\n - ', buildIndex + 1); |
| const releaseIndex = workflow.indexOf(' - name: Release contracts\n'); |
| |
| assert.ok(buildIndex >= 0); |
| assert.match(workflow.slice(buildIndex, buildEnd), /release_contract == 'true'/u); |
| assert.ok(buildIndex < releaseIndex); |
| assert.match( |
| workflow.slice(releaseIndex), |
| /if: steps\.plan\.outputs\.release_contract == 'true'/u, |
| ); |
| }); |
| |
| test('pull request triggers stay on an explicit allowlist', () => { |
| // Naming the lanes that must not run on pull requests only covers the ones |
| // someone remembered to name; W0 kept an unbounded trigger that way. |
| const onPullRequests = readdirSync(WORKFLOW_DIR).filter(hasPullRequestTrigger).sort(); |
| |
| assert.deepEqual(onPullRequests, [ |
| 'ci.yml', |
| 'cli-package-validation.yml', |
| 'copilot-auto-review.yml', |
| 'dependency-audit.yml', |
| 'gitoxide-helper-admission.yml', |
| 'pr-effort-label.yml', |
| 'release-windows-check.yml', |
| 'runtime-host-owner-platform.yml', |
| 'runtime-host-peer-admission.yml', |
| 'windows-recovery.yml', |
| 'windows-sandbox-w0.yml', |
| ]); |
| }); |
| |
| test('every pull request lane holds a scarce runner for the same bounded time', () => { |
| // One tier, not per-lane values. The worst observed successful runs are 19 |
| // minutes (ci.yml) and 20 (release-windows-check), so 45 is about 2.3x the |
| // slowest lane: enough headroom for a cold cache and a flake retry, and far |
| // short of the 120 and 90 a hung job used to hold. A lane with no limit at |
| // all inherits GitHub's 360 and fails here. |
| // `pull_request` only: a `pull_request_target` lane reads the pull request |
| // rather than gating it, so it is not competing for a runner the author is |
| // waiting on and keeps its own tighter limit. |
| // Granularity is the file, not the job: a job inside a gating workflow that |
| // opts out of pull requests still carries the tier, because reading a job's |
| // `if:` would need the YAML parser this file cannot install. |
| const gates = readdirSync(WORKFLOW_DIR).filter((name) => |
| /\bpull_request\b/u.test(triggerBlock(name)), |
| ); |
| assert.ok(gates.length > 0, 'no pull request lane found'); |
| |
| for (const name of gates) { |
| // From `jobs:` on, with comment lines stripped, so prose above the triggers |
| // cannot be read as a job. |
| const workflow = readWorkflow(name).replaceAll(/^[ \t]*#.*$/gmu, ''); |
| const start = workflow.indexOf('\njobs:'); |
| assert.ok(start >= 0, `${name}: no jobs block`); |
| const jobs = workflow.slice(start); |
| |
| const limits = [...jobs.matchAll(/^ {4}timeout-minutes: (\d+)$/gmu)].map((match) => match[1]); |
| // Counted by `runs-on`, one per job that consumes a runner, rather than by |
| // job id: a quoted id escapes an id pattern, and a two-space line inside a |
| // `run: |` block satisfies one. |
| const runners = [...jobs.matchAll(/^ {4}runs-on:/gmu)].length; |
| assert.ok(runners > 0, `${name}: no job consumes a runner`); |
| assert.deepEqual( |
| limits, |
| Array.from({ length: runners }, () => '45'), |
| name, |
| ); |
| } |
| }); |
| |
| test('the recovery lane pairs its path filter with a nightly run and a main push', () => { |
| // Read from the `on:` block with comments stripped, so documenting a trigger |
| // cannot break its contract. |
| const triggers = triggerBlock('windows-recovery.yml'); |
| |
| // Same contract as the sandbox lane: the filter is a pre-filter, not the |
| // lane's import closure, so dropping the schedule would silently lose every |
| // transitive edit it cannot match, and dropping the filter would put every |
| // Windows recovery run back on every pull request. The main push carries no |
| // filter because `strict: false` lets a stale-base pull request go green, |
| // and because a paths filter only sees the first 300 files of a diff. |
| // Stripped comment lines survive as blank ones, so the gap between the |
| // trigger and its list is any mix of blank and four-space lines. |
| assert.match(triggers, /\n {2}pull_request:\n(?:(?: {4}[^\n]*)?\n)* {4}paths:/u); |
| assert.match(triggers, /\n {2}push:\n {4}branches: \[main\]\n/u); |
| assert.doesNotMatch( |
| triggers.match(/\n {2}push:\n(?:(?: {4}[^\n]*)?\n)*/u)?.[0] ?? '', |
| /\bpaths(-ignore)?:/u, |
| ); |
| assert.match(triggers, /\n {2}schedule:\n/u); |
| assert.match(triggers, /\n {2}workflow_dispatch:/u); |
| assert.match(readWorkflow('windows-recovery.yml'), /\n {4}name: windows_recovery/u); |
| }); |
| |
| test('the recovery lane keeps every run kind out of one shared concurrency group', () => { |
| const workflow = readWorkflow('windows-recovery.yml'); |
| |
| // github.head_ref is a bare branch name, so two forks pushing their own |
| // `main` would share a group and cancel each other; github.ref is |
| // refs/heads/main for the nightly, a dispatch and a main push alike, so a |
| // ref-keyed group made a dispatch queue behind the nightly and let the next |
| // dispatch discard it while pending. |
| assert.match( |
| workflow, |
| /group: windows-recovery-\$\{\{ github\.event\.pull_request\.number \|\| github\.run_id \}\}/u, |
| ); |
| assert.match(workflow, /\n {2}cancel-in-progress: true/u); |
| }); |
| |
| test('the recovery lane filters pull requests by the workspaces its steps execute', () => { |
| const workflow = readWorkflow('windows-recovery.yml'); |
| const filtered = new Set(pullRequestPathFilter('windows-recovery.yml')); |
| |
| // Derived from the dist paths the steps run, then widened along the workspace |
| // dependency graph the planner selects with. The separator class matches the |
| // backslash form too, because these steps run under pwsh where both are |
| // legal. A new workspace on this lane, or a new dependency under one of them, |
| // fails here until the filter admits its sources and project file. |
| const executed = [ |
| ...new Set( |
| [...workflow.matchAll(/packages[/\\]([^/\\]+)[/\\]dist[/\\]/gu)].map((match) => match[1]), |
| ), |
| ].sort(); |
| assert.deepEqual(executed, ['runtime', 'runtime-host', 'storage']); |
| |
| const closure = dependencyClosure(executed.map((workspace) => `packages/${workspace}`)); |
| assert.ok(closure.includes('packages/core'), 'dependency closure must reach core'); |
| for (const dir of closure) { |
| assert.ok(filtered.has(`${dir}/src/**`), `${dir}: sources`); |
| assert.ok(filtered.has(`${dir}/tsconfig.json`), `${dir}: project file`); |
| assert.ok(filtered.has(`${dir}/package.json`), `${dir}: manifest`); |
| } |
| }); |
| |
| test('the recovery lane filter follows the postinstall launcher chain', () => { |
| const filtered = new Set(pullRequestPathFilter('windows-recovery.yml')); |
| const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')); |
| // Derived from postinstall itself, then one hop into whatever those entry |
| // points launch, because a launcher the filter cannot see still decides what |
| // `npm ci` produces on Windows. A restated list missed exactly that hop. |
| const entrypoints = [...manifest.scripts.postinstall.matchAll(/node (scripts\/[\w.-]+)/gu)].map( |
| (match) => match[1], |
| ); |
| assert.ok(entrypoints.length > 0, 'postinstall runs no script'); |
| |
| for (const entrypoint of entrypoints) { |
| assert.ok(filtered.has(entrypoint), entrypoint); |
| const source = readFileSync(new URL(`../${entrypoint}`, import.meta.url), 'utf8'); |
| for (const launched of source.matchAll(/new URL\('\.\/([\w.-]+)'/gu)) { |
| assert.ok(filtered.has(`scripts/${launched[1]}`), `${entrypoint} launches ${launched[1]}`); |
| } |
| } |
| }); |
| |
| test('the recovery lane filters pull requests by what its install and clean steps consume', () => { |
| const filtered = new Set(pullRequestPathFilter('windows-recovery.yml')); |
| |
| // `npm.cmd ci` and `npm.cmd run build:test` run unconditionally, so these are |
| // first-class inputs of the lane rather than transitive edits the nightly can |
| // be left to cover. A grouped dependabot bump touches only the manifests, and |
| // the crash gates sit on a native file lock the Linux `test` lane never sees. |
| for (const path of [ |
| 'package.json', |
| 'package-lock.json', |
| 'patches/**', |
| 'scripts/apply-dependency-patches.mjs', |
| 'scripts/install-electron-with-retry.mjs', |
| 'scripts/clean-build.mjs', |
| 'scripts/clean-paths.mjs', |
| 'scripts/windows-runtime-host-local-ipc-trust.ps1', |
| 'tsconfig.base.json', |
| 'tsconfig.lib.json', |
| 'packages/runtime/scripts/**', |
| '.github/workflows/windows-recovery.yml', |
| ]) { |
| assert.ok(filtered.has(path), path); |
| } |
| }); |
| |
| test('the sandbox lane pairs its path filter with a nightly run', () => { |
| const workflow = readWorkflow('windows-sandbox-w0.yml'); |
| |
| // The filter is a pre-filter, not the lane's import closure, so dropping the |
| // schedule would silently lose every transitive edit it cannot match, and |
| // dropping the filter would put the whole runtime back on pull requests. |
| assert.match(workflow, /\n {2}pull_request:\n {4}paths:/u); |
| assert.match(workflow, /\n {2}schedule:/u); |
| }); |
| |
| test('the packaged Windows gate owns Runtime Host candidate election changes', () => { |
| const workflow = readWorkflow('release-windows-check.yml'); |
| |
| assert.match(workflow, /'packages\/runtime-host\/src\/client\/connect-or-spawn\.ts'/u); |
| assert.match(workflow, /'packages\/runtime-host\/src\/client\/launcher\.ts'/u); |
| }); |
| |
| test('the packaged Windows gate triggers on release orchestration changes', () => { |
| const workflow = readWorkflow('release-windows-check.yml'); |
| |
| assert.match(workflow, /'\.github\/workflows\/release\.yml'/u); |
| }); |
| |
| test('the packaged Windows gate workflow is itself a release-contract input', () => { |
| assert.equal( |
| planTests(['.github/workflows/release-windows-check.yml'], { graph }).releaseContract, |
| true, |
| ); |
| assert.match( |
| readWorkflow('release-windows-check.yml'), |
| /'\.github\/workflows\/release-windows-check\.yml'/u, |
| ); |
| }); |
| |
| test('the packaged Windows gate triggers on packaged sandbox inputs', () => { |
| const workflow = readWorkflow('release-windows-check.yml'); |
| |
| for (const path of [ |
| 'apps/desktop/scripts/copy-runtime-filesystem-worker.mjs', |
| 'packages/runtime/scripts/build-filesystem-worker.mjs', |
| 'packages/runtime/src/filesystem-worker/**', |
| 'packages/runtime/src/sandbox/**', |
| 'packages/runtime/src/path-containment.ts', |
| 'packages/runtime/src/sandbox-boundary-path.ts', |
| 'packages/core/src/permission-profile.ts', |
| 'packages/core/src/permission-profile-compiler.ts', |
| ]) { |
| assert.ok(workflow.includes(` - '${path}'`), path); |
| } |
| }); |
| |
| test('pull-request and release lanes share the packaged sandbox lifecycle verifier', () => { |
| for (const name of ['release-windows-check.yml', 'release.yml']) { |
| assert.match(readWorkflow(name), /npm run verify:windows-x64/u, name); |
| } |
| |
| const verifier = readFileSync(new URL('verify-windows-x64.mjs', import.meta.url), 'utf8'); |
| assert.match( |
| verifier, |
| /await verifyPackagedWindowsSandboxLifecycle\(sandboxExecutable, \{ run \}\)/u, |
| ); |
| }); |
| |
| test('the Gitoxide gate owns repository admission changes', () => { |
| const workflow = readWorkflow('gitoxide-helper-admission.yml'); |
| |
| assert.match( |
| workflow, |
| /'packages\/runtime-host\/src\/server\/gitoxide-repository-admission-authority-internal\.ts'/u, |
| ); |
| assert.match( |
| workflow, |
| /'packages\/runtime-host\/src\/__tests__\/gitoxide-repository-admission-authority-internal\.test\.ts'/u, |
| ); |
| }); |
| |
| test('specialized platform workflows stay reachable without pull requests', () => { |
| const cli = readWorkflow('cli-package-validation.yml'); |
| const baseline = readWorkflow('windows-baseline.yml'); |
| const recovery = readWorkflow('windows-recovery.yml'); |
| |
| for (const workflow of [cli, baseline, recovery]) { |
| assert.match(workflow, /\n workflow_dispatch:/u); |
| } |
| assert.match(cli, /\n workflow_call:/u); |
| assert.match(baseline, /\n schedule:/u); |
| }); |
| |
| test('Windows recovery executes the exact managed dependency ADS regressions', () => { |
| const recovery = readWorkflow('windows-recovery.yml'); |
| |
| assert.match(recovery, /name: Verify managed dependency alternate streams/u); |
| assert.match(recovery, /--test-name-pattern="NTFS alternate stream"/u); |
| assert.match( |
| recovery, |
| /packages\/storage\/dist\/__tests__\/managed-dependency-environment\.test\.js/u, |
| ); |
| assert.match(recovery, /# tests 3/u); |
| assert.match(recovery, /# pass 3/u); |
| assert.match(recovery, /# skipped 0/u); |
| }); |
| |
| test('Windows recovery executes the root initialization replacement race', () => { |
| const recovery = readWorkflow('windows-recovery.yml'); |
| |
| assert.match(recovery, /name: Verify root initialization replacement race/u); |
| assert.match( |
| recovery, |
| /--test-name-pattern="rejects replacement before opening the temporary marker"/u, |
| ); |
| assert.match(recovery, /packages\/storage\/dist\/__tests__\/root-authority\.test\.js/u); |
| assert.match(recovery, /# tests 1/u); |
| assert.match(recovery, /# pass 1/u); |
| assert.match(recovery, /# skipped 0/u); |
| }); |
| |
| test('Windows recovery executes the complete Skill catalog suite', () => { |
| const recovery = readWorkflow('windows-recovery.yml'); |
| |
| assert.match(recovery, /skill-catalog-coordinator\.test\.js/u); |
| assert.match(recovery, /skill-catalog-protocol\.test\.js/u); |
| assert.match(recovery, /skill-catalog-repository\.test\.js/u); |
| assert.match(recovery, /skill-catalog-transaction\.test\.js/u); |
| assert.match(recovery, /skill-catalog-two-client-uds\.test\.js/u); |
| assert.match(recovery, /# tests 90/u); |
| assert.match(recovery, /# pass 90/u); |
| assert.match(recovery, /# skipped 0/u); |
| }); |
| |
| test('workflows never persist the job credential into the checkout', () => { |
| for (const name of readdirSync(WORKFLOW_DIR)) { |
| for (const step of checkoutSteps(name)) { |
| assert.match(step, /persist-credentials: false/u, `${name}: ${step.trim()}`); |
| } |
| } |
| }); |
| |
| test('a pull_request_target checkout is pinned to the trusted base commit', () => { |
| // This event hands the job a writable token while the pull request is fork |
| // controlled, so what gets checked out is what decides whether that token can |
| // reach author-supplied code. `github.sha` is the base branch commit here; |
| // `head.sha` and a bare checkout under a merge-ref event are both the pull |
| // request's own tree. Nothing else in CI would notice that edit, which is why |
| // the rule lives here rather than in a comment. |
| for (const name of readdirSync(WORKFLOW_DIR)) { |
| if (!/\bpull_request_target\b/u.test(triggerBlock(name))) continue; |
| |
| for (const step of checkoutSteps(name)) { |
| assert.match(step, /\n\s+ref: \$\{\{ github\.sha \}\}\n/u, `${name}: ${step.trim()}`); |
| } |
| } |
| }); |
| |
| test('core CI runs the live Eval proxy lifecycle when Eval is selected', () => { |
| const workflow = readWorkflow('ci.yml'); |
| const evalPackage = JSON.parse( |
| readFileSync(new URL('../packages/eval/package.json', import.meta.url), 'utf8'), |
| ); |
| |
| assert.match( |
| workflow, |
| /if: contains\(steps\.plan\.outputs\.standard_workspaces, 'packages\/eval'\)/u, |
| ); |
| assert.match(workflow, /MAKA_EVAL_EGRESS_PROXY_TEST: '1'/u); |
| assert.match(workflow, /docker build[\s\S]*maka-eval-egress-proxy:12\.2\.3/u); |
| assert.match(workflow, /npm --workspace @maka\/eval run test:egress-proxy:live/u); |
| assert.equal( |
| evalPackage.scripts['test:egress-proxy:live'], |
| 'python3 harbor/test_egress_filter_live.py', |
| ); |
| assert.doesNotMatch(evalPackage.scripts['test:dist'], /test_egress_filter_live\.py/u); |
| }); |
| |
| const WORKFLOW_DIR = new URL('../.github/workflows/', import.meta.url); |
| |
| /** |
| * Reads the `paths` list belonging to a workflow's `pull_request` trigger. |
| * Anchoring to the trigger, instead of matching entry text anywhere in the |
| * file, is what makes the filter assertions fail when entries move under |
| * `paths-ignore`, under another trigger, or out of `on:` altogether. |
| */ |
| function pullRequestPathFilter(name) { |
| // Reads the `on:` block with comments already stripped, so a comment between |
| // the trigger and its list cannot end the scan, and accepts the quoting and |
| // spacing YAML allows, so a legal rewrite reports the entries it really has |
| // instead of an empty list that reads as a missing filter. |
| const lines = triggerBlock(name).split('\n'); |
| const start = lines.findIndex((line) => /^ {2}pull_request:\s*$/u.test(line)); |
| assert.ok(start >= 0, `${name}: no pull_request trigger`); |
| |
| const paths = []; |
| let inPaths = false; |
| for (const line of lines.slice(start + 1)) { |
| if (line.trim() === '') continue; |
| if (/^ {0,2}\S/u.test(line)) break; |
| if (/^ {4}\S/u.test(line)) { |
| inPaths = /^ {4}paths:\s*$/u.test(line); |
| continue; |
| } |
| const entry = inPaths ? /^\s+-\s+['"]?(.+?)['"]?\s*$/u.exec(line) : null; |
| if (entry) paths.push(entry[1]); |
| } |
| return paths; |
| } |
| |
| /** |
| * Workspace dirs `seeds` depend on, transitively, read off the same graph the |
| * planner selects with rather than a second definition of the same edges. The |
| * graph stores dependents, so a dependency is any dir listing one of ours. |
| */ |
| function dependencyClosure(seeds) { |
| const graph = loadWorkspaceGraph(); |
| const selected = new Set(seeds); |
| const pending = [...seeds]; |
| while (pending.length > 0) { |
| const dir = pending.shift(); |
| for (const [dependency, dependents] of graph.dependents) { |
| if (!dependents.has(dir) || selected.has(dependency)) continue; |
| selected.add(dependency); |
| pending.push(dependency); |
| } |
| } |
| return [...selected].sort(); |
| } |
| |
| function readWorkflow(name) { |
| return readFileSync(new URL(name, WORKFLOW_DIR), 'utf8'); |
| } |
| |
| /** |
| * Reads the `on:` block only, so a workflow cannot escape a trigger contract by |
| * writing `on: [pull_request]`, and prose elsewhere in the file cannot fake one. |
| */ |
| function triggerBlock(name) { |
| const withoutComments = readWorkflow(name).replaceAll(/^[ \t]*#.*$/gmu, ''); |
| |
| return withoutComments.match(/^on:(.*(?:\n(?![^\s#]).*)*)/mu)?.[1] ?? ''; |
| } |
| |
| function hasPullRequestTrigger(name) { |
| return /\bpull_request(_target)?\b/u.test(triggerBlock(name)); |
| } |
| |
| /** |
| * Slices each checkout step from its `uses:` line to the next step, so the |
| * assertion is per checkout: a bare one cannot be balanced out by a sibling |
| * step that opts out, or by the string appearing in a comment. |
| */ |
| function checkoutSteps(name) { |
| const withoutComments = readWorkflow(name).replaceAll(/^[ \t]*#.*$/gmu, ''); |
| |
| return ( |
| withoutComments.match(/^[ \t]*- uses: actions\/checkout@.*\n(?:(?![ \t]*- )[ \t]+.*\n)*/gmu) ?? |
| [] |
| ); |
| } |