Files
orca/config/scripts/pr-workflow-parallelism.test.mjs
T
NeilandBrennan Benson fbe94ceff6 fix: close readiness gaps found by merged-change audit (#17159)
* fix(ssh): fence stale kills and retired pane replay

* fix(ssh): support cancellable interactive authentication

* fix(ssh): await remote catalog before snapshot adoption

* fix(pty): contain Windows ConPTY input failures

* fix(power): avoid redundant macOS display blocking

* perf(editor): narrow markdown override subscriptions

* fix(quick-open): close directory handles after reads

* refactor(linux): remove unused proc socket scanner

* fix(usage): apply flat Sonnet 4.6 pricing

* ci: prime Node next native test cache

* docs(skills): resolve snapshot cleanup data path

* fix(ssh): recover install locks after host reboot

* test(ssh): recognize boot-aware install locks

* test(ssh): prove previous-boot lock recovery live

* test(wire): pin pre-metadata release coverage

* fix(terminal): preserve remote tab ownership through recovery races

* test(runtime): fence replaced terminal handles in agent guard

* fix(ssh): preserve remote snapshot authority across polls

* fix(pty): contain late ConPTY output EPIPE

* test(pty): register Windows exit watcher before kill

* fix: close SSH and tab readiness race gaps

* fix(tabs): retain headless order and placeholder titles

* fix(build): avoid parallel electron-vite config race

* test(windows): avoid MSYS temp path rewriting

* test(windows): avoid killing exited PTY

* fix(pty): avoid late ConPTY input teardown race

* fix(terminal): sync reconnect error ownership after commit

* fix(runtime): use canonical worktree identity comparison

* test(ssh): assert complete cold-hydration baseline

* test(windows): invoke quoted retention fixture via PowerShell

* test(windows): read ConPTY grid through mode con

* fix(terminal): publish PTY replacements atomically

* fix(terminal): infer stale identity on reattach

* fix(terminal): fence stale pane PTY callbacks

* fix(terminal): fence stale pane binds after rebind

* fix(terminal): reject stale pane transport callbacks

* fix(terminal): fence mirrored reattach spawn callbacks

* fix(terminal): replace stale pane PTYs on remount

* fix(ci): size the Windows launcher-compile test budget from measurement

`native-smoke (windows-latest)` fails ~4.5% of runs on
`preserves a multiline argument through the compiled remote launcher`
with "Test timed out in 15000ms" — on unrelated PRs, for reasons that
have nothing to do with them. Across 176 sampled attempts it is the only
red that job produced, and it hit seven different PRs in two days:
#16900, #16904, #16915, #16955 (twice), #16979, #17014, #17085.

The test is six process creations: powershell.exe forks csc.exe, then
the freshly compiled orca.exe forks node.exe, twice. Hosted Windows
runners periodically slow process creation down, and this test amplifies
that far harder than anything else in the job. Comparing the 80 attempts
where it ran under 3s against the 12 where it ran over 12s, its own
median goes 2198ms -> 15917ms (7.2x) while the same file's
powershell-only test moves 556 -> 686ms (1.2x), the cmd.exe and Git Bash
process tests in the neighbouring file move 1.4x, and the other 35 files
put together move 1.5x.

Measured across those 176 attempts: 1881ms to 35438ms, p50 4264ms,
correlation +0.881 with the job's total Vitest duration. 8 of 176 (4.5%)
exceeded the 15s cap; 2 of 176 (1.1%) also exceeded the shared 30s
testTimeout, so deleting the override and inheriting the config is not
enough on its own. 60s clears all 176 with 1.7x headroom on the worst.

This is slow, not hung. Every body here is synchronous spawnSync, so
Vitest cannot interrupt one — the timer fires only after the body
returns and the reported duration is real elapsed time. That is why a
failure reads `× ... 22464ms` under `Test timed out in 15000ms`. The
work finished; the stopwatch was short. Seven reruns at one identical
head measured 2053 / 4680 / 5551 / 8732 / 13506 / 14868 / 21937ms — the
last of those would have been red on code that had not changed.

The 15s came from #8897, which raised this test off Vitest's built-in 5s
default because the job then ran bare `pnpm vitest run`. #8909 landed
3h27m later and pointed the job at config/vitest.config.ts, which is the
real fix for that. The constant stayed behind and has been the binding
budget ever since.

* fix(terminal): fence stale remount reattach ownership

* fix(terminal): reconcile mounted pane identity after replacement

* fix(terminal): fence stale reattach fallback ownership

* fix(terminal): fence deferred SSH reattach ownership

* fix(terminal): fence stale split pane ownership callbacks

* fix(terminal): keep stale spawns from consuming startup

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-31 08:17:40 -07:00

480 lines
24 KiB
JavaScript

import { existsSync, globSync, readFileSync } from 'node:fs'
import { parse } from 'yaml'
import { describe, expect, it } from 'vitest'
const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
const unitTestWorkflow = parse(readFileSync('.github/workflows/unit-tests.yml', 'utf8'))
const nodeNextWorkflow = parse(readFileSync('.github/workflows/node-next-compat.yml', 'utf8'))
const dependencyAction = parse(
readFileSync('.github/actions/install-node-dependencies/action.yml', 'utf8')
)
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'))
const pnpmWorkspace = parse(readFileSync('pnpm-workspace.yaml', 'utf8'))
const shellContractFiles = [
'src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts',
'src/main/daemon/shell-ready.test.ts',
'src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts',
'src/main/providers/__tests__/shell-ready-framework-example.test.ts',
'src/main/pty/omp-shell-wrapper.node-pty.test.ts',
'src/main/shell-startup-feature-channel.test.ts',
'src/main/zsh-scoped-histfile.live-shell.test.ts',
'src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts',
'src/main/zsh-wrapper-version-mismatch.live-shell.test.ts',
'src/shared/posix-command-path-lookup.test.ts'
]
const patchedNodePtyContractFiles = [
'src/main/daemon/node-pty-fd-leak.test.ts',
'src/shared/fish-query-reply-child-stdin.node-pty.test.ts'
]
const nativeShellContractFiles = [...shellContractFiles, ...patchedNodePtyContractFiles]
const testFilePatterns = [
'config/**/*.{test,spec}.{js,cjs,mjs,ts,tsx}',
'src/**/*.{test,spec}.{js,cjs,mjs,ts,tsx}',
'tests/**/*.{test,spec}.{js,cjs,mjs,ts,tsx}',
'tests/tools/**/*.{test,spec}.{js,cjs,mjs,ts,tsx}'
]
// Why the harness import counts: the zsh startup hook runs from a `precmd`, so
// its tests drive a real zsh through a PTY in zsh-startup-hook-pty-harness
// rather than calling spawnSync('zsh') themselves. Without this branch the rule
// silently stops noticing the very tests that need the lane's zsh install.
const realZshUsage =
/(?:spawnSync|execFileSync|spawn)\(\s*['"](?:\/(?:usr\/)?bin\/)?zsh['"]|spawnSync\(\s*['"]which['"]\s*,\s*\[\s*['"]zsh['"]|name:\s*['"]zsh['"]\s*,\s*path:\s*executablePath|from '[^']*zsh-startup-hook-pty-harness'/
describe('PR workflow parallelism', () => {
it('cancels superseded runs for the same pull request', () => {
expect(workflow.concurrency.group).toBe('pr-checks-${{ github.event.pull_request.number }}')
expect(workflow.concurrency['cancel-in-progress']).toBe(true)
})
it('grants the PR workflow read-only repository access', () => {
expect(workflow.permissions).toEqual({ contents: 'read' })
})
it('runs Node 24 on PRs and the same eight-shard suite on Node 26 daily', () => {
const sharedTest = unitTestWorkflow.jobs.test
const testStep = sharedTest.steps.find((step) => step.name === 'Test shard')
const installStep = sharedTest.steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
const primerInstall = workflow.jobs.test_native_cache.steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
const nodeNextPrimerInstall = nodeNextWorkflow.jobs.test_native_cache.steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
expect(workflow.jobs.test.uses).toBe('./.github/workflows/unit-tests.yml')
expect(JSON.parse(workflow.jobs.test.with.node_versions)).toEqual(['24'])
expect(nodeNextWorkflow.jobs.test.uses).toBe('./.github/workflows/unit-tests.yml')
expect(JSON.parse(nodeNextWorkflow.jobs.test.with.node_versions)).toEqual(['26'])
expect(nodeNextWorkflow.on.schedule).toHaveLength(1)
expect(nodeNextWorkflow.on.workflow_dispatch).toBeNull()
expect(sharedTest.strategy.matrix.node).toBe('${{ fromJSON(inputs.node_versions) }}')
expect(sharedTest.strategy.matrix.shard).toEqual(
Array.from({ length: 8 }, (_, index) => index + 1)
)
expect(sharedTest.strategy.matrix.shard_total).toEqual([8])
expect(installStep.with['node-version']).toBe('${{ matrix.node }}')
expect(installStep.with['cache-electron-package']).toBe('true')
expect(testStep.run).toContain('--shard=${{ matrix.shard }}/${{ matrix.shard_total }}')
for (const testFile of nativeShellContractFiles) {
expect(testStep.run).toContain(`--exclude=${testFile}`)
}
expect(primerInstall.with['native-runtime']).toBe('node')
expect(primerInstall.with['node-version']).toBe('24')
expect(workflow.jobs.test.needs).toContain('test_native_cache')
expect(nodeNextPrimerInstall.with['native-runtime']).toBe('node')
expect(nodeNextPrimerInstall.with['node-version']).toBe('26')
expect(nodeNextWorkflow.jobs.test.needs).toEqual(['test_native_cache'])
})
it('runs real-shell coverage once outside the general shards', () => {
const shellStep = workflow.jobs.shell_contracts.steps.find(
(step) => step.name === 'Test real shell contracts'
)
const shellInstall = workflow.jobs.shell_contracts.steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
// Why parsed rather than substring-matched: the step name changes as shells are
// added, and `includes('fish')` would also match a comment or a longer package.
const aptPackages = (step) =>
(step.run?.match(/apt-get install[^\n]*/)?.[0] ?? '')
.split(/\s+/)
.filter((token) => !['apt-get', 'install', 'sudo', ''].includes(token))
.filter((token) => !token.startsWith('-'))
const jobsInstallingPackages = Object.entries(workflow.jobs)
.filter(([, job]) => (job.steps ?? []).some((step) => aptPackages(step).length > 0))
.map(([name]) => name)
expect(shellStep).toBeDefined()
expect(shellInstall).toBeDefined()
expect(shellStep.run.split(/\s+/)).toContain('--maxWorkers=1')
// Why the whole workflow, not just the general shards: any other lane installing
// these shells would silently start running the real-shell tests twice.
expect(jobsInstallingPackages).toEqual(['shell_contracts'])
// Why each shell is asserted: the live tests skip themselves when the binary is
// missing, so a dropped package silently empties this lane instead of failing it.
const shellPackages = workflow.jobs.shell_contracts.steps.flatMap(aptPackages)
for (const shell of ['zsh', 'fish']) {
expect(shellPackages).toContain(shell)
}
expect(shellInstall.with['native-runtime']).toBe('node')
for (const testFile of nativeShellContractFiles) {
expect(shellStep.run).toContain(testFile)
}
})
it('refreshes the apt index once while adding the fish PPA', () => {
const installStep = workflow.jobs.shell_contracts.steps.find(
(step) => step.name === 'Install zsh and fish'
)
// Comment lines mention both commands by name, so count the executed ones only.
const commands = installStep.run
.split('\n')
.filter((line) => !line.trim().startsWith('#'))
.join('\n')
const updates = commands.match(/apt-get update/g) ?? []
// Anchored to the start of a line so the retry message that names the command in
// prose is not mistaken for an invocation of it.
const addRepoCalls = commands
.split('\n')
.map((line) => line.trim())
.filter((line) => /^(sudo\s+)?add-apt-repository\b/.test(line))
// add-apt-repository refreshes every configured repo unless told not to, so an
// update on each side of it made this step pay for three full passes.
expect(updates).toHaveLength(1)
expect(addRepoCalls.length).toBeGreaterThan(0)
for (const call of addRepoCalls) {
expect(call.split(/\s+/)).toContain('-n')
}
// The one remaining update has to come after the PPA is on the list, or the fish
// index it exists to fetch would not be there yet.
expect(commands.lastIndexOf('add-apt-repository')).toBeLessThan(
commands.indexOf('apt-get update')
)
})
it('bounds the shell lane so a stalled apt mirror cannot hold the run open', () => {
const job = workflow.jobs.shell_contracts
// A passing run of this job takes ~4.5 minutes, almost all of it package download.
// Without a job bound a stalled mirror runs to GitHub's 6h default, and because this
// is a required check it holds the whole run open and blocks `gh run rerun --failed`.
expect(job['timeout-minutes']).toBeGreaterThan(0)
expect(job['timeout-minutes']).toBeLessThanOrEqual(30)
const installStep = job.steps.find((step) => step.name === 'Install zsh and fish')
// apt applies no wall-clock bound to a stalled mirror on its own. These turn an
// unbounded hang into a bounded, retried, legible failure.
expect(installStep.run).toMatch(/Acquire::http::Timeout/)
expect(installStep.run).toMatch(/Acquire::https::Timeout/)
expect(installStep.run).toMatch(/Acquire::Retries/)
// Retries multiply: a first attempt at 30s x 3 retries across every index file turned
// a dead mirror into a ~15 minute stall. One attempt, then move on.
expect(installStep.run).toMatch(/Acquire::Retries "1"/)
// Acquire timeouts are per-connection, so they cannot bound the command as a whole.
// Only a wall-clock bound can, and both apt invocations need one.
expect(installStep.run).toMatch(/timeout \d+ sudo apt-get update/)
expect(installStep.run).toMatch(/timeout \d+ sudo apt-get install/)
})
it('keeps every real-zsh test in the dedicated shell lane', () => {
const discoveredFiles = globSync(testFilePatterns)
// Why this file is excluded: it carries the detector pattern as a literal
// and would otherwise match itself.
.filter((testFile) => testFile !== 'config/scripts/pr-workflow-parallelism.test.mjs')
// TypeScript builds can leave an ignored JavaScript companion beside a source
// test. Inspect the source file once so generated output cannot duplicate it.
.filter(
(testFile) =>
!testFile.endsWith('.js') ||
!['.ts', '.tsx', '.mjs', '.cjs'].some((extension) =>
existsSync(testFile.replace(/\.js$/, extension))
)
)
.filter((testFile) => realZshUsage.test(readFileSync(testFile, 'utf8')))
.sort()
expect(discoveredFiles).toEqual([...shellContractFiles].sort())
})
it('overlaps bundles with independent output directories', () => {
const buildStep = workflow.jobs.package.steps.find(
(step) => step.name === 'Build package inputs'
)
expect(buildStep.run).toContain('scripts=(build:relay build:electron-vite:parallel)')
expect(buildStep.run).toContain('pnpm run "$script" &')
expect(
workflow.jobs.package.steps.find(
(step) => step.name === 'Project web client from renderer build'
).run
).toBe('pnpm run build:web-from-renderer')
expect(packageJson.scripts['build:desktop']).toContain('pnpm run build:web-from-renderer')
expect(packageJson.scripts['build:release']).toContain('pnpm run build:web-from-renderer')
})
it('smokes managed-hook companions under their supported Node 18 runtime', () => {
const steps = workflow.jobs.managed_hook_node18.steps
const installIndex = steps.findIndex(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
const buildIndex = steps.findIndex((step) => step.run === 'pnpm run build:relay')
const node18Index = steps.findIndex(
(step) => step.uses === 'actions/setup-node@v6' && step.with['node-version'] === '18'
)
const smokeIndex = steps.findIndex(
(step) => step.run === 'node config/scripts/smoke-managed-hook-runtime-node18.mjs'
)
expect(installIndex).toBeLessThan(buildIndex)
expect(buildIndex).toBeLessThan(node18Index)
expect(node18Index).toBeLessThan(smokeIndex)
})
it('restores the pnpm store before dependency installation', () => {
const steps = dependencyAction.runs.steps
const pnpmIndex = steps.findIndex((step) => step.name === 'Setup pnpm')
const nodeIndex = steps.findIndex((step) => step.name === 'Setup Node.js')
const requestedNodeIndex = steps.findIndex((step) => step.name === 'Setup requested Node.js')
expect(pnpmIndex).toBeLessThan(nodeIndex)
expect(pnpmIndex).toBeLessThan(requestedNodeIndex)
const packageManagerVersion = /^pnpm@([^+]+)/.exec(packageJson.packageManager)?.[1]
expect(packageManagerVersion).toBe('12.0.0')
expect(steps[pnpmIndex].uses).toBe('pnpm/setup@v2')
expect(steps[pnpmIndex].with.version).toBeUndefined()
expect(steps[pnpmIndex].with.install).toBe(false)
expect(steps[nodeIndex].with.cache).toBe('pnpm')
expect(steps[nodeIndex].if).toBe("inputs.node-version == ''")
expect(steps[requestedNodeIndex].if).toBe("inputs.node-version != ''")
expect(steps[requestedNodeIndex].with['node-version']).toBe('${{ inputs.node-version }}')
expect(steps[requestedNodeIndex].with.cache).toBe('pnpm')
})
it('uses the repository package-manager version for every direct pnpm setup', () => {
const directSetups = globSync('.github/workflows/*.yml').flatMap((workflowPath) => {
const parsed = parse(readFileSync(workflowPath, 'utf8'))
return Object.values(parsed.jobs ?? {}).flatMap((job) =>
(job.steps ?? [])
.filter((step) => step.uses === 'pnpm/setup@v2')
.map((step) => ({ workflowPath, step }))
)
})
expect(directSetups.length).toBeGreaterThan(0)
for (const { workflowPath, step } of directSetups) {
expect(step.with?.version, workflowPath).toBeUndefined()
expect(step.with?.install, workflowPath).toBe(false)
}
})
it('restores Electron downloads before preparing the package runtime', () => {
const steps = workflow.jobs.package.steps
const cacheIndex = steps.findIndex((step) => step.name === 'Cache electron-builder downloads')
const installIndex = steps.findIndex(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
expect(cacheIndex).toBeGreaterThanOrEqual(0)
expect(installIndex).toBeGreaterThanOrEqual(0)
expect(cacheIndex).toBeLessThan(installIndex)
})
it('prepares each native runtime before its consumers start', () => {
const installFor = (jobName) =>
workflow.jobs[jobName].steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
const sharedTestInstall = unitTestWorkflow.jobs.test.steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
for (const jobName of ['typecheck', 'git_compatibility', 'xterm_patch_sync']) {
expect(installFor(jobName).with, jobName).toBeUndefined()
}
expect(installFor('static_analysis').with['native-runtime']).toBe('node')
expect(installFor('shell_contracts').with['native-runtime']).toBe('node')
expect(sharedTestInstall.with['native-runtime']).toBe('node')
expect(installFor('package').with['native-runtime']).toBe('electron')
expect(installFor('package_windows').with['native-runtime']).toBe('node')
expect(installFor('package_windows').with['persist-native-cache']).toBe('false')
expect(
workflow.jobs.package_windows.steps.find(
(step) => step.name === 'Save compiled Node native modules'
).if
).toBe("steps.deps.outputs.native-cache-hit != 'true'")
expect(dependencyAction.inputs['persist-native-cache'].default).toBe('true')
expect(
dependencyAction.runs.steps.find((step) => step.name === 'Use external node-gyp').if
).toBe("runner.os == 'Linux' && inputs.native-runtime != 'none'")
const dependencyInstall = dependencyAction.runs.steps.find(
(step) => step.name === 'Install dependencies'
)
// Why frozen: re-resolving the graph costs a minute per job and the `git diff`
// guard below already fails the run when the lockfile is stale, so the slow
// resolution can never legitimately change anything.
expect(dependencyInstall.run).toContain('--frozen-lockfile')
expect(dependencyInstall.run).not.toContain('--no-frozen-lockfile')
expect(dependencyInstall.run).toContain(
'git -C "$GITHUB_WORKSPACE" diff --exit-code -- package.json pnpm-lock.yaml pnpm-workspace.yaml'
)
expect(dependencyInstall.run).toContain('--ignore-scripts')
expect(dependencyInstall.run).not.toContain('--os=')
expect(dependencyInstall.run).not.toContain('--cpu=')
expect(pnpmWorkspace.supportedArchitectures.os).toEqual(
expect.arrayContaining(['current', 'win32'])
)
expect(pnpmWorkspace.supportedArchitectures.cpu).toContain('current')
const prepareRuntime = dependencyAction.runs.steps.find(
(step) => step.name === 'Prepare native runtime'
)
expect(prepareRuntime.if).toBe("inputs.native-runtime != 'none'")
expect(prepareRuntime.run).toContain('ensure-native-runtime.mjs --runtime="$NATIVE_RUNTIME"')
})
it('reuses native preparation after the dependency action gate', () => {
const buildStep = workflow.jobs.package.steps.find(
(step) => step.name === 'Build package inputs'
)
const packageStep = workflow.jobs.package.steps.find(
(step) => step.name === 'Package unpacked app'
)
expect(buildStep.run).not.toContain('ensure:electron-runtime')
expect(packageStep.env.ORCA_REUSE_PREPARED_NATIVE_RUNTIME).toBe('1')
})
it('restores compiled native modules after the install that strips them', () => {
const steps = dependencyAction.runs.steps
const installIndex = steps.findIndex((step) => step.name === 'Install dependencies')
const cacheIndex = steps.findIndex((step) => step.name === 'Restore compiled native modules')
const prepareIndex = steps.findIndex((step) => step.name === 'Prepare native runtime')
// `--ignore-scripts` leaves no build/, so a restore before the install would be
// overwritten and one after the rebuild would never save a hit.
expect(installIndex).toBeLessThan(cacheIndex)
expect(cacheIndex).toBeLessThan(prepareIndex)
expect(steps[cacheIndex].if).toBe(
"inputs.native-runtime != 'none' && inputs.persist-native-cache != 'false'"
)
const restoreOnly = steps.find(
(step) => step.name === 'Restore compiled native modules without saving'
)
expect(restoreOnly.if).toBe(
"inputs.native-runtime != 'none' && inputs.persist-native-cache == 'false'"
)
expect(restoreOnly.uses).toBe('actions/cache/restore@v5')
// Native artifacts are ABI-bound: a key missing either dimension serves a build
// that cannot load, and ensure-native-runtime would recompile it anyway.
for (const cacheStep of [steps[cacheIndex], restoreOnly]) {
expect(cacheStep.with.key).toContain('${{ inputs.native-runtime }}')
expect(cacheStep.with.key).toContain('${{ runner.os }}')
expect(cacheStep.with.key).toContain('${{ runner.arch }}')
expect(cacheStep.with.key).toContain(
'steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version'
)
expect(cacheStep.with.key).toContain('steps.native-cache-scope.outputs.scope')
expect(cacheStep.with.key).toContain('config/patches/node-pty@1.1.0.patch')
expect(cacheStep.with.key).toContain(
'config/patches/@vscode__windows-process-tree@0.8.0.patch'
)
expect(cacheStep.with.key).toContain('.github/actions/install-node-dependencies/action.yml')
expect(cacheStep.with.key).toContain('config/scripts/ensure-native-runtime.mjs')
expect(cacheStep.with.key).toContain('config/scripts/rebuild-native-deps.mjs')
expect(cacheStep.with.path).toContain('node-pty@*/node_modules/node-pty/build')
expect(cacheStep.with.path).toContain('windows-native-registry@')
expect(cacheStep.with.path).toContain('@vscode+windows-process-tree@')
expect(cacheStep.with['restore-keys']).toBeUndefined()
}
expect(steps[cacheIndex].id).toBe('native-cache-restore')
expect(restoreOnly.id).toBe('native-cache-restore-only')
const cacheScope = steps.find((step) => step.name === 'Resolve native cache scope')
expect(cacheScope.if).toBe("inputs.native-runtime != 'none'")
expect(cacheScope.run).toContain('/etc/os-release')
expect(dependencyAction.outputs['native-cache-scope'].value).toBe(
'${{ steps.native-cache-scope.outputs.scope }}'
)
expect(dependencyAction.outputs['native-cache-hit'].value).toContain(
'steps.native-cache-restore.outputs.cache-hit'
)
expect(dependencyAction.outputs['native-cache-hit'].value).toContain(
'steps.native-cache-restore-only.outputs.cache-hit'
)
const electronCache = steps.find((step) => step.name === 'Cache Electron package archive')
const electronCacheResolution = steps.find(
(step) => step.name === 'Resolve Electron package cache'
)
expect(electronCacheResolution.if).toBe(
"inputs.native-runtime == 'electron' || inputs.cache-electron-package == 'true'"
)
expect(electronCache.if).toBe(electronCacheResolution.if)
expect(electronCache.uses).toBe('actions/cache@v5')
expect(electronCache.with.key).toContain('steps.electron-package-cache.outputs.version')
expect(dependencyAction.inputs['cache-electron-package'].default).toBe('false')
})
it('reuses TypeScript incremental state across typecheck runs', () => {
const steps = workflow.jobs.typecheck.steps
const cacheIndex = steps.findIndex((step) => step.name === 'Cache TypeScript incremental state')
const checkIndex = steps.findIndex((step) => step.run === 'pnpm run typecheck')
expect(cacheIndex).toBeGreaterThanOrEqual(0)
expect(cacheIndex).toBeLessThan(checkIndex)
expect(steps[cacheIndex].with.path).toBe('config/*.tsbuildinfo')
// Why restore-keys matter here: the base SHA key is shared by every commit in a PR,
// but actions/cache keeps the first successful graph until the base or config changes.
expect(steps[cacheIndex].with['restore-keys']).toBeTruthy()
// The buildinfo is only reusable while the compiler options that produced it hold.
expect(steps[cacheIndex].with.key).toContain(
"hashFiles('pnpm-lock.yaml', 'config/tsconfig*.json')"
)
expect(steps[cacheIndex].with.key).toContain('github.event.pull_request.base.sha')
expect(steps[cacheIndex].with['restore-keys']).not.toContain('tsbuildinfo-${{ runner.os }}-\n')
})
it('checks out full history without historical blobs', () => {
const fullHistoryCheckouts = Object.values(workflow.jobs)
.flatMap((job) => job.steps ?? [])
.filter(
(step) => step.uses?.startsWith('actions/checkout@') && step.with?.['fetch-depth'] === 0
)
expect(fullHistoryCheckouts.length).toBeGreaterThan(0)
for (const checkout of fullHistoryCheckouts) {
expect(checkout.with.filter).toBe('blob:none')
}
})
it('keeps verify as the aggregate required check', () => {
expect(workflow.jobs.verify.needs).toEqual([
'code_paths',
'static_analysis',
'root_directory_guard',
'typecheck',
'git_compatibility',
'codex_index_heal_contract',
'xterm_patch_sync',
'shell_contracts',
'test',
'orcad_browser',
'cross-version-wire',
'managed_hook_node18',
'package',
'package_windows'
])
const verifyStep = workflow.jobs.verify.steps.find(
(step) => step.name === 'Require successful checks'
)
expect(verifyStep.env.MANAGED_HOOK_NODE18).toBe('${{ needs.managed_hook_node18.result }}')
expect(verifyStep.run).toContain('"$MANAGED_HOOK_NODE18"')
// Why assert this one too: the browser provider test skips itself without
// ORCA_BROWSER_EXECUTABLE, so it only guards anything if verify actually reads it.
expect(verifyStep.env.ORCAD_BROWSER).toBe('${{ needs.orcad_browser.result }}')
expect(verifyStep.run).toContain('"$ORCAD_BROWSER"')
expect(verifyStep.env.CROSS_VERSION_WIRE).toBe('${{ needs.cross-version-wire.result }}')
expect(verifyStep.run).toContain('"$CROSS_VERSION_WIRE"')
})
})