Files
orca/config/scripts/pr-preflight-gates.test.mjs
T
Neil 24069f0a97 ci: fail when a checked-in test file has no runner (#33080)
* ci: fail when a checked-in test file has no runner

Two node:test files under .github/actions lost their runner when #31792
trimmed cloud pretest, and the two monaco .bench.ts budgets were never run
by any CI job. Wire both back in and add a static check that every
test-like file is collected by a declared runner or imported by a
collected entry (the src .spec.* suite-module convention).

* ci: ignore type-only imports and quote the lease glob for cmd.exe

* ci: parse type-only imports, cover UNIT_EXCLUDE, route cloud runner edits

Type-only imports are now dropped with the TypeScript parser, so a type
alias before a runtime re-export no longer hides it. UNIT_EXCLUDE files
must be named by a pr.yml or unit-tests.yml step. A cloud-only diff that
adds a test or edits a runner config now runs the static job. The monaco
benchmarks stay manual (pnpm test:perf:benchmarks) until a CI runner
proves their wall-clock budgets.

* ci: require UNIT_INCLUDE names in dedicated jobs and erase typeof import()

* ci: resolve each runtime import to the one file Vite loads

The unit dependency graph keeps every plausible candidate so it can
invalidate conservatively, which cannot prove a suite runs: an import of
./case.spec counted both case.spec.ts and case.spec.tsx. Resolve with
Vite's extension order instead.

* ci: take runtime imports from transpiled output

Imports used only as types are erased without a type modifier, so
parsing for type-only syntax still followed them. Derive edges from
ts.transpileModule output, which erases what Vitest's transform erases,
and only transpile files that can reach an uncollected candidate.

* ci: fix the agent-status benchmark config and route all cloud code

The agent-status benchmark config inherited the unit projects, whose
includes override its own, so it ran the whole unit suite and never the
benchmark. Clear projects as the performance config does. Route every
cloud code file to the static job, since content-detected tests can have
any name.
2026-10-24 00:57:17 -07:00

439 lines
19 KiB
JavaScript

import { readFileSync, readdirSync } from 'node:fs'
import { runInNewContext } from 'node:vm'
import { expect, it } from 'vitest'
import { runProcess } from '@orca/process-host'
import { parse } from 'yaml'
import { classifyPrJobs } from './pr-code-change-scope.mjs'
const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
const preflight = workflow.jobs.preflight
const steps = preflight.steps
const compiler = steps.find((step) => step.id === 'typecheck')
const compilerJoin = steps.findIndex((step) => step.wait === 'typecheck')
const plan = steps.find((step) => step.id === 'unit-plan')
it('shares one setup and overlaps the compiler only after type-aware checks finish', () => {
expect(workflow.jobs.static_analysis).toBeUndefined()
expect(workflow.jobs.typecheck).toBeUndefined()
expect(preflight.needs).toEqual(['code_paths'])
expect(preflight['runs-on']).toBe('blacksmith-8vcpu-ubuntu-2404')
const installs = steps.filter(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
expect(installs).toHaveLength(2)
for (const install of installs) {
expect(install.with['native-runtime']).toBe('node')
expect(install.with['node-version']).toBe('24')
expect(install.with['persist-native-cache']).not.toBe('false')
}
expect(steps[0].with['fetch-depth']).toBeGreaterThanOrEqual(2)
expect(compiler.background).toBe(true)
expect(compiler.run).toContain('pnpm run typecheck:ci')
const bunSetup = steps.find((step) => step.name === 'Set up Bun for preflight checks')
expect(bunSetup.with['bun-version-file']).toBe('config/.bun-version')
expect(bunSetup.if).toBeUndefined()
expect(plan.background).toBe(true)
expect(plan.run.trim().split('\n')).toEqual([
'if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi',
'node config/scripts/ci-unit-plan.mjs'
])
expect(plan.env.ORCA_UNIT_SELECTION_MODE).toContain('vars.ORCA_UNIT_SELECTION_MODE')
expect(plan.env.ORCA_UNIT_FULL_SHARD_COUNT).toBe("${{ vars.ORCA_UNIT_FULL_SHARD_COUNT || '5' }}")
expect(steps.indexOf(plan)).toBeLessThan(compilerJoin)
expect(steps.indexOf(compiler)).toBeGreaterThan(
steps.findIndex((step) => step.wait === 'changed-code-quality')
)
expect(steps.indexOf(compiler)).toBeLessThan(
steps.findIndex((step) => step.id === 'localization-extraction')
)
expect(compilerJoin).toBeGreaterThan(
steps.findIndex((step) => step.wait?.includes('localization-extraction'))
)
})
it('uses the shared Bun pin for every current-toolchain workflow', () => {
expect(readFileSync('config/.bun-version', 'utf8').trim()).toBe('1.4.4')
for (const file of readdirSync('.github/workflows').filter((name) => /\.ya?ml$/.test(name))) {
const current = parse(readFileSync(`.github/workflows/${file}`, 'utf8'))
for (const job of Object.values(current.jobs)) {
for (const step of job.steps ?? []) {
if (!String(step.uses).startsWith('oven-sh/setup-bun@')) {
continue
}
expect(step.with['bun-version-file'], file).toBe('config/.bun-version')
expect(step.with['bun-version'], file).toBeUndefined()
}
}
}
})
it('checks formatting over the same tree the local format script writes', () => {
const check = steps.find((step) => step.name === 'Check formatting')
const manifest = JSON.parse(readFileSync('package.json', 'utf8'))
expect(check.run.replace('node node_modules/oxfmt/bin/oxfmt', 'oxfmt')).toBe(
manifest.scripts.format.replace('--write', '--check')
)
})
it('keeps the Bun anti-slop scan aligned with the local audit', () => {
const audit = steps.find((step) => step.name === 'Reject low-evidence patterns')
const manifest = JSON.parse(readFileSync('package.json', 'utf8'))
const localCommand = manifest.scripts['audit:anti-slop'].split('&&').at(-1).trim()
expect(audit.run.trim().split('\n')[0]).toBe('node config/scripts/sync-anti-slop-plugin.mjs')
expect(
audit.run
.trim()
.split('\n')
.at(-1)
.replace('bun --smol node_modules/oxlint/bin/oxlint', 'oxlint')
).toBe(localCommand)
})
it('requires physical preflight success before publishing shards and admitting consumers', () => {
const join = steps.findIndex((step) => step.wait === 'unit-plan')
const upload = steps.findIndex((step) => step.uses === 'actions/upload-artifact@v7')
expect(join).toBeGreaterThan(compilerJoin)
expect(upload).toBeGreaterThan(join)
expect(steps[upload]['continue-on-error']).toBeUndefined()
expect(steps[upload].with.name).toBe('unit-selection-attempt-${{ github.run_attempt }}')
expect(preflight.outputs.shards).toBe('${{ steps.unit-plan.outputs.shards }}')
expect(workflow.jobs.test.with.shards).toBe('${{ needs.preflight.outputs.shards }}')
for (const job of ['test', 'package', 'package_windows']) {
expect(workflow.jobs[job].needs).toEqual(['code_paths', 'preflight'])
}
for (const result of ['success', 'failure', 'cancelled', 'skipped']) {
const admitted = runInNewContext(workflow.jobs.test.if, {
cancelled: () => false,
needs: { code_paths: { result: 'success', outputs: { test: 'true' } }, preflight: { result } }
})
expect(admitted, result).toBe(result === 'success')
}
const verify = workflow.jobs.verify.steps.find(
(step) => step.name === 'Require successful checks'
)
expect(verify.env.PREFLIGHT).toBe('${{ needs.preflight.result }}')
expect(verify.env.PREFLIGHT_SHOULD_RUN).toBe(
"${{ needs.code_paths.outputs.static_analysis == 'true' || needs.code_paths.outputs.typecheck == 'true' }}"
)
expect(verify.run).toContain('check_job preflight "$PREFLIGHT" "$PREFLIGHT_SHOULD_RUN"')
expect(workflow.jobs.verify.needs).toContain('preflight')
expect(workflow.jobs.verify.needs).not.toContain('typecheck')
})
it.each([
{ name: 'both phases succeed', phases: ['true', 'true'], result: 'success', admitted: true },
{ name: 'static-only succeeds', phases: ['true', 'false'], result: 'success', admitted: true },
{ name: 'type-only succeeds', phases: ['false', 'true'], result: 'success', admitted: true },
{ name: 'static-only fails', phases: ['true', 'false'], result: 'failure', admitted: false },
{ name: 'type-only fails', phases: ['false', 'true'], result: 'failure', admitted: false },
{ name: 'both phases fail', phases: ['true', 'true'], result: 'failure', admitted: false },
{ name: 'preflight is cancelled', result: 'cancelled', admitted: false },
{ name: 'preflight is unfinished', result: undefined, admitted: false },
{ name: 'both phases unselected', phases: ['false', 'false'], result: 'skipped', admitted: true },
{ name: 'required static skips', phases: ['true', 'false'], result: 'skipped', admitted: false },
{
name: 'required compiler skips',
phases: ['false', 'true'],
result: 'skipped',
admitted: false
},
{ name: 'both required phases skip', result: 'skipped', admitted: false },
{
name: 'unselected preflight fails',
phases: ['false', 'false'],
result: 'failure',
admitted: false
},
{
name: 'static selection missing',
phases: [undefined, 'false'],
result: 'skipped',
admitted: false
},
{
name: 'compiler selection missing',
phases: ['false', undefined],
result: 'skipped',
admitted: false
},
{ name: 'selection unknown', phases: ['', 'false'], result: 'skipped', admitted: false },
{
name: 'docs have no route',
phases: ['false', 'false'],
result: 'skipped',
route: 'false',
admitted: false
},
{ name: 'source has no route', result: 'success', route: 'false', admitted: false },
{ name: 'route missing', result: 'success', route: '', admitted: false },
{ name: 'path detection fails', result: 'success', pathsResult: 'failure', admitted: false },
{ name: 'path detection skips', result: 'skipped', pathsResult: 'skipped', admitted: false },
{ name: 'workflow cancelled after success', result: 'success', cancelled: true, admitted: false },
{
name: 'workflow cancelled with unselected phases',
phases: ['false', 'false'],
result: 'skipped',
cancelled: true,
admitted: false
}
])('admits advisory E2E only after eligible preflight: $name', (scenario) => {
const [static_analysis, typecheck] = scenario.phases ?? ['true', 'true']
expect(workflow.jobs.e2e.needs).toEqual(['code_paths', 'preflight'])
const admitted = runInNewContext(workflow.jobs.e2e.if, {
cancelled: () => scenario.cancelled ?? false,
needs: {
code_paths: {
result: scenario.pathsResult ?? 'success',
outputs: { static_analysis, typecheck, e2e_should_run: scenario.route ?? 'true' }
},
preflight: { result: scenario.result }
}
})
expect(admitted).toBe(scenario.admitted)
})
it.each(['true', 'false'])(
'preserves advisory E2E on ready-for-review only with proven required-check reuse: %s',
(reused) => {
const scope = classifyPrJobs(['src/main/runtime/orca-runtime.ts'])
const context = {
steps: {
readiness: { outputs: { reused } },
filter: {
outputs: Object.fromEntries(
Object.entries(scope).map(([key, value]) => [key, String(value)])
)
},
e2e_filter: { outputs: { should_run: 'true' } }
}
}
const outputs = Object.fromEntries(
['static_analysis', 'typecheck', 'e2e_should_run'].map((name) => [
name,
String(runInNewContext(workflow.jobs.code_paths.outputs[name].slice(3, -2), context))
])
)
expect(outputs).toEqual({
static_analysis: reused === 'true' ? 'false' : 'true',
typecheck: reused === 'true' ? 'false' : 'true',
e2e_should_run: 'true'
})
const needs = { code_paths: { result: 'success', outputs }, preflight: { result: 'skipped' } }
expect(runInNewContext(preflight.if, { needs })).toBe(reused !== 'true')
expect(runInNewContext(workflow.jobs.e2e.if, { needs, cancelled: () => false })).toBe(
reused === 'true'
)
expect(workflow.jobs.verify.needs).not.toContain('e2e')
}
)
it('pins every foreground and background step to its selected phase', () => {
const staticPhase = "needs.code_paths.outputs.static_analysis == 'true'"
const typePhase = "needs.code_paths.outputs.typecheck == 'true'"
const foreground = steps.filter(
(step) => !step.background && /outputs\.(static_analysis|typecheck)/.test(step.if ?? '')
)
expect(foreground.map((step) => [step.name ?? step.run ?? step.uses, step.if])).toEqual([
['Check formatting', staticPhase],
['Reject low-evidence patterns', staticPhase],
['Enforce type-aware code-quality baseline', staticPhase],
[
'./.github/actions/install-mobile-dependencies',
`${staticPhase} && needs.code_paths.outputs.mobile_dependencies == 'true'`
],
['Verify locked audit tools', staticPhase],
['Enforce React Doctor on changed lines', staticPhase],
['Check Zustand selector fan-out budget', staticPhase],
['Check reliability gate manifest', staticPhase],
['Enforce dead design-system classes', staticPhase],
['Check VM runtime rollback compatibility', staticPhase],
['Enforce ratchets', staticPhase],
['Check Node runtime pin', staticPhase],
['Check every test file has a runner', staticPhase],
['Check root and mobile lock the same shared dependencies', staticPhase],
['Decide whether the orcad terminal smoke runs', staticPhase],
['Verify the generated RPC params catalog', staticPhase],
['Verify the generated ACP protocol schema', staticPhase],
['Verify bundled skill guides', staticPhase],
['Verify skill freshness manifest', staticPhase],
['Verify localization coverage', staticPhase],
['Guard against project-owned .d.ts in preload/shared', staticPhase],
['Check feature wall asset budget', staticPhase],
['Verify macOS entitlements', staticPhase],
['actions/upload-artifact@v7', typePhase]
])
expect(
steps
.filter((step) => step.background)
.map((step) => [step.id, step.env.PREFLIGHT_PHASE_SELECTED])
).toEqual([
['process-host-imports', `\${{ ${staticPhase} }}`],
['main-sync-spawn-ratchet', `\${{ ${staticPhase} }}`],
['root-lint', `\${{ ${staticPhase} }}`],
['native-code-quality', `\${{ ${staticPhase} }}`],
['changed-code-quality', `\${{ ${staticPhase} }}`],
['typecheck', `\${{ ${typePhase} }}`],
['localization-extraction', `\${{ ${staticPhase} }}`],
['localization-catalogs', `\${{ ${staticPhase} }}`],
['unit-plan', `\${{ ${typePhase} }}`]
])
})
it.each([
{ changed: ['LICENSE'], static_analysis: false, typecheck: false, mobile_dependencies: false },
{ changed: ['README.md'], static_analysis: true, typecheck: false, mobile_dependencies: false },
{
changed: ['mobile/src/App.tsx'],
static_analysis: true,
typecheck: false,
mobile_dependencies: true
},
{
changed: ['cloud/apps/relay/Dockerfile'],
static_analysis: false,
typecheck: false,
mobile_dependencies: false
},
{
changed: ['src/main/index.ts'],
static_analysis: true,
typecheck: true,
mobile_dependencies: false
},
{
changed: ['mobile/src/App.tsx', 'src/main/index.ts'],
static_analysis: true,
typecheck: true,
mobile_dependencies: true
},
{ changed: [], static_analysis: true, typecheck: true, mobile_dependencies: true }
])('preserves exact phase selection for unrelated paths: $changed', ({ changed, ...expected }) => {
const scope = classifyPrJobs(changed)
expect({
static_analysis: scope.static_analysis,
typecheck: scope.typecheck,
mobile_dependencies: scope.mobile_dependencies
}).toEqual(expected)
const needs = {
code_paths: {
outputs: Object.fromEntries(Object.entries(scope).map(([key, value]) => [key, String(value)]))
}
}
expect(runInNewContext(preflight.if, { needs })).toBe(
expected.static_analysis || expected.typecheck
)
expect(runInNewContext(compiler.env.PREFLIGHT_PHASE_SELECTED.slice(3, -2), { needs })).toBe(
expected.typecheck
)
expect(scope.test).toBe(expected.typecheck)
})
it('registers successful no-op background work when a phase is unselected or already failed', () => {
for (const step of steps.filter((step) => step.background)) {
expect(step.if).toBe('!cancelled()')
expect(step.env.PREFLIGHT_PHASE_SELECTED).toContain('needs.code_paths.outputs.')
expect(step.env.PREFLIGHT_PRIOR_SUCCESS).toBe("${{ job.status == 'success' }}")
expect(step.run.split('\n')[0]).toBe(
'if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi'
)
}
})
it('shares import analysis without dropping either boundary gate', () => {
const imports = steps.find((step) => step.id === 'process-host-imports')
expect(imports.run).toContain('bun config/scripts/check-import-boundaries.mjs')
expect(steps.some((step) => step.name === 'Check compatibility-folder imports')).toBe(false)
expect(steps.find((step) => step.name === 'Verify localization coverage').run).toBe(
'bun config/scripts/audit-localization-coverage.mjs --check'
)
})
it('prepares an isolated Node 18 executable before the existing Node 24 dependency installs', () => {
const setup = steps.find((step) => step.id === 'node18_runtime')
const capture = steps.find((step) => step.id === 'node18_executable')
const selected = "needs.code_paths.outputs.managed_hook_node18 == 'true'"
expect(setup.uses).toBe('actions/setup-node@v6')
expect(setup.if).toBe(selected)
expect(setup.with).toEqual({ 'node-version': '18', 'package-manager-cache': false })
expect(setup['continue-on-error']).toBe(true)
expect(capture['continue-on-error']).toBe(true)
expect(capture.run).toBe(`node -p '"executable=" + process.execPath' >> "$GITHUB_OUTPUT"`)
expect(steps.indexOf(capture)).toBeGreaterThan(steps.indexOf(setup))
for (const install of steps.filter(
(step) => step.uses === './.github/actions/install-node-dependencies'
)) {
expect(steps.indexOf(install)).toBeGreaterThan(steps.indexOf(capture))
expect(install.with['node-version']).toBe('24')
expect(install['continue-on-error']).toBeUndefined()
}
for (const outcome of ['success', 'failure', 'cancelled', 'skipped', '', undefined]) {
expect(
runInNewContext(capture.if, {
needs: { code_paths: { outputs: { managed_hook_node18: 'true' } } },
steps: { node18_runtime: { outcome } }
})
).toBe(outcome === 'success')
}
const bun = steps.filter((step) => step.uses?.startsWith('oven-sh/setup-bun@'))
expect(bun).toHaveLength(1)
expect(bun[0].if).toBeUndefined()
})
it('runs the unchanged Node 18 smoke after every Node 24 step and preserves its required result', () => {
const smoke = steps.at(-1)
const receipt = workflow.jobs.managed_hook_node18
expect(smoke.id).toBe('managed_hook_node18')
expect(smoke.if).toBe("success() && needs.code_paths.outputs.managed_hook_node18 == 'true'")
expect(smoke.env.NODE18_EXECUTABLE).toBe('${{ steps.node18_executable.outputs.executable }}')
expect(smoke.env.ORCA_BACKGROUND_LAUNCH).toBe('1')
expect(smoke.run.trim().split('\n')).toEqual([
'test -n "$NODE18_EXECUTABLE"',
'"$NODE18_EXECUTABLE" config/scripts/smoke-process-host-node18.mjs'
])
expect(smoke['continue-on-error']).toBe(true)
expect(preflight['continue-on-error']).toBeUndefined()
expect(preflight.outputs.managed_hook_node18).toBe('${{ steps.managed_hook_node18.outcome }}')
for (const background of steps.filter((step) => step.background)) {
const joined = steps.findIndex((step) =>
Array.isArray(step.wait) ? step.wait.includes(background.id) : step.wait === background.id
)
expect(joined, background.id).toBeGreaterThan(steps.indexOf(background))
expect(joined, background.id).toBeLessThan(steps.indexOf(smoke))
}
expect(receipt.name).toBe('managed hooks on Node 18')
expect(receipt['runs-on']).toBe('blacksmith-2vcpu-ubuntu-2404')
expect(receipt.steps).toHaveLength(1)
expect(receipt.steps[0].uses).toBeUndefined()
expect(receipt.steps[0].env.MANAGED_HOOK_NODE18).toBe(
'${{ needs.preflight.outputs.managed_hook_node18 }}'
)
expect(receipt['continue-on-error']).toBeUndefined()
expect(receipt.steps[0]['continue-on-error']).toBeUndefined()
})
it.skipIf(process.platform === 'win32')(
'fails the smoke for a missing captured executable and requires actual success in the receipt',
async () => {
const smoke = steps.at(-1)
const receipt = workflow.jobs.managed_hook_node18.steps[0]
const cases = [
[smoke.run, 'NODE18_EXECUTABLE', '', 1],
...['success', 'failure', 'cancelled', 'skipped', '', 'unknown'].map((outcome) => [
receipt.run,
'MANAGED_HOOK_NODE18',
outcome,
outcome === 'success' ? 0 : 1
])
]
for (const [script, variable, value, expected] of cases) {
const result = await runProcess({
program: 'bash',
args: ['-e', '-c', script],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1', [variable]: value },
timeoutMs: 10_000
})
expect(result.code, `${variable}=${value}: ${result.stderr}`).toBe(expected)
}
}
)