mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
* refactor(shell): collapse the zsh wrapper to one .zshenv and a precmd hook (STA-4786)
Orca needs to run code after the user's own zsh startup files. It bought that by
keeping ZDOTDIR pointed at its own wrapper dir for the whole of startup and
sourcing each user file by hand -- four generated files per transport, with a
fake ZDOTDIR live while /etc/zshrc ran. That single decision is the root of a
whole bug family:
- /etc/zshrc assigns HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history unconditionally, so
history landed inside Orca's own dir (#11044), and an epilogue had to repair it.
- zsh's sourcehome() ignores ZDOTDIR once the shell is in sh/ksh emulation, so a
user .zshenv or .zprofile ending in `emulate sh` hid every later wrapper file.
The emulation degrade blocks and their forked $(emulate) probes exist for that.
- One wrapper dir shared by two installed builds could mix files from both, so
every generated file had to redefine the helpers it called.
- The baked generation-time ZDOTDIR literal is unusable when a Windows-generated
wrapper is sourced inside WSL via /mnt/c (#8003), so the runtime path had to be
re-derived from %x.
The wrapper now hands ZDOTDIR back on its first lines and defers Orca's work to a
precmd hook that runs at the first prompt -- after .zprofile, /etc/zshrc, .zshrc
and .zlogin, every one of which zsh reads from the user's own directory exactly
as in an unwrapped shell. Each bug above stops being reachable rather than being
repaired, and their machinery goes with them: eight of thirteen exported blocks in
shell-templates.ts, both drifted discovery bodies (unifying them closes the
"reconciling the two is a follow-up" note the file carried), and the relay's
separate ORCA_USER_ZDOTDIR shape. Generated zsh drops from 819 lines across
twelve files to 143 in three.
Two things the design has to get right, both found by running it rather than
reasoning about it:
- Every function is defined ABOVE the source of the user's .zshenv. A user file
ending in `emulate sh` puts the rest of the wrapper under sh parsing rules, and
the first prototype died there with `parse error near '\n'` -- silently, leaving
the pane unwrapped. Function bodies are parsed at definition time.
- ORCA_ORIG_ZDOTDIR is vetted, not trusted. The launch config only sets it when it
resolved a usable dir, but a pane inherits its parent's environment too, so a
stale value from an older build can arrive on its own and would point ZDOTDIR
back at a wrapper dir. The ownership check Node applies now also runs in the
shell, where that route is visible.
Orca also stops inventing a ZDOTDIR: where the user has none, ORCA_ORIG_ZDOTDIR is
absent and the pane ends with ZDOTDIR unset, as an unwrapped login zsh does.
Verified on real zsh over a real PTY -- necessary, because a precmd hook never runs
in a shell started with -c, so the existing `zsh -i -c` probes could not have
exercised this design at all. src/main/zsh-startup-hook-pty-harness.ts drives the
shell to a prompt and reports through a file rather than stdout, which a PTY echoes.
* test(shell): cover the relay variant of the zsh hook in a real shell
The relay writes its own variant -- no OSC 133, remote CLI bin dir instead of the
agent-teams shim -- and it had no live coverage. It used to carry a second ZDOTDIR
shape as well, which is how it drifted from the desktop template in the first
place; now the spec flags are the only difference, and this pins that.
* fix(shell): rebase the single-file hook onto content-addressed wrapper trees
#15285 landed content-addressed wrapper roots and a per-transport fileset module
while this branch was in flight. The fileset modules are now the single place the
tree is described, so 'only .zshenv' is stated once per transport and the
required-paths check follows from it rather than repeating the list.
* test(shell): point the mixed-build proof at the relay, the one fixed wrapper path
#15285 content-addressed the desktop and daemon trees, so two builds can no
longer write the same directory there and the scenario this file covers became
unreachable on those paths. The relay still writes a fixed ~/.orca-relay/
shell-ready, so that is where the hazard survives and where the proof belongs.
* fix(test): make the zsh PTY harness survive a startup that stops to ask
Two CI-only failures, both from driving a real PTY where the old probes drove a
pipe:
- A host whose global zshrc runs `compinit` over directories it considers
insecure stops startup and ASKS. A pipe-backed `zsh -i -c` never saw the
question; a PTY sits at it until the timeout. The harness now answers it.
ZSH_DISABLE_COMPFIX does not help -- that is an oh-my-zsh convention and plain
compinit ignores it, which I confirmed by reproducing the prompt locally.
- The PS1 line was typed at t=0, so on such a host the question consumed it as
its answer. The harness now waits for the shell to fall quiet first, which
also stops a slow prompt framework racing the same write.
Also merges a duplicate vitest import the native code-quality audit flagged.
* fix(test): stop the live-shell assertions assuming macOS host behaviour
Two of them hardcoded what my machine does rather than what Orca owes:
- LINEINIT was pinned to 'none'. A host whose global zsh config installs its own
zle-line-init widget has one either way; the contract is that it looks the same
wrapped as unwrapped, which the assertion beside it already states.
- The dropped-precmd_functions case asserted HISTFILE was no longer the scoped
path. Whether the scoped value survives at all is the host's call: macOS
/etc/zshrc overwrites HISTFILE so it does not, and a host with no such
assignment keeps whatever the spawn env set. Now compared against an unwrapped
pane given the same env, which is the real contract on both.
Also notes, where the emulation cases live, that they only discriminate on a host
whose system zshrc clobbers HISTFILE -- on CI's Ubuntu the load-bearing assertion
is ORCA_HISTFILE having been consumed.
* test(shell): re-pin the fixes the four-file wrapper was built for
Archaeology over the removed blocks: each existed for a bug, so each needs the
bug shown to be unreachable rather than just the code gone. Six restored or added,
each naming the change that introduced the behaviour.
- #8003, twice: the wrapper sourced from a relocated root, and from a non-ASCII
(token-range) one. The old file baked its generation-time path in and had to
re-derive the runtime one from %x to avoid using it; this one bakes nothing.
Both runs assert ORCA_SHELL_FEATURES came back consumed, so 'the user's .zshrc
loaded' cannot pass on a pane that never read the wrapper at all.
- #4667: user startup files must see their OWN ZDOTDIR while they run, or plugin
and theme lookups resolve into Orca's dir. The old wrapper swapped ZDOTDIR
around each source; this one never takes it away, and the values now have to
match an unwrapped pane's.
- #1947: a user .zshenv that returns early.
- #15258: an inherited ZDOTDIR that is an Orca wrapper dir must be refused. CI
proved this route is live -- the launch config only sets ORCA_ORIG_ZDOTDIR when
it resolved a usable dir, but a pane inherits its parent's environment too.
- #11044/#11146: a nested Orca inherits neither cross-process channel and no
ZDOTDIR of Orca's, which is what makes #11044's plain shape unreachable rather
than repaired. Verified the child-env probe detects a real leak before trusting
it to report the absence of one.
355 lines
17 KiB
JavaScript
355 lines
17 KiB
JavaScript
import { 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 dependencyAction = parse(
|
|
readFileSync('.github/actions/install-node-dependencies/action.yml', 'utf8')
|
|
)
|
|
const packageJson = JSON.parse(readFileSync('package.json', '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/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/main/pty/omp-shell-wrapper.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('shards the general test suite across Node 24 and Node 26', () => {
|
|
expect(workflow.jobs.test.strategy.matrix.node).toEqual(['24', '26'])
|
|
expect(workflow.jobs.test.strategy.matrix.shard).toEqual(
|
|
Array.from({ length: 16 }, (_, index) => index + 1)
|
|
)
|
|
expect(workflow.jobs.test.strategy.matrix.shard_total).toEqual([16])
|
|
const testStep = workflow.jobs.test.steps.find((step) => step.name === 'Test shard')
|
|
const installStep = workflow.jobs.test.steps.find(
|
|
(step) => step.uses === './.github/actions/install-node-dependencies'
|
|
)
|
|
|
|
expect(installStep.with['node-version']).toBe('${{ matrix.node }}')
|
|
expect(testStep.run).toContain('--shard=${{ matrix.shard }}/${{ matrix.shard_total }}')
|
|
for (const testFile of nativeShellContractFiles) {
|
|
expect(testStep.run).toContain(`--exclude=${testFile}`)
|
|
}
|
|
})
|
|
|
|
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')
|
|
.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)
|
|
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('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'
|
|
)
|
|
|
|
for (const jobName of [
|
|
'static_analysis',
|
|
'typecheck',
|
|
'git_compatibility',
|
|
'xterm_patch_sync'
|
|
]) {
|
|
expect(installFor(jobName).with, jobName).toBeUndefined()
|
|
}
|
|
expect(installFor('shell_contracts').with['native-runtime']).toBe('node')
|
|
expect(installFor('test').with['native-runtime']).toBe('node')
|
|
expect(installFor('package').with['native-runtime']).toBe('electron')
|
|
|
|
expect(
|
|
dependencyAction.runs.steps.find((step) => step.name === 'Use external node-gyp').if
|
|
).toBe("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 diff --exit-code package.json pnpm-lock.yaml')
|
|
expect(dependencyInstall.run).toContain('--ignore-scripts')
|
|
expect(dependencyInstall.run).not.toContain('--os=')
|
|
expect(dependencyInstall.run).not.toContain('--cpu=')
|
|
expect(packageJson.pnpm.supportedArchitectures.os).toEqual(
|
|
expect.arrayContaining(['current', 'win32'])
|
|
)
|
|
expect(packageJson.pnpm.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'")
|
|
// Native artifacts are ABI-bound: a key missing either dimension serves a build
|
|
// that cannot load, and ensure-native-runtime would recompile it anyway.
|
|
expect(steps[cacheIndex].with.key).toContain('${{ inputs.native-runtime }}')
|
|
expect(steps[cacheIndex].with.key).toContain('steps.requested-node.outputs.node-version')
|
|
expect(steps[cacheIndex].with.key).toContain('config/patches/node-pty@1.1.0.patch')
|
|
// No restore-keys: a partial-match key is exactly the ABI-mismatched build above.
|
|
expect(steps[cacheIndex].with['restore-keys']).toBeUndefined()
|
|
})
|
|
|
|
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: an exact-key miss is the normal case (the key is
|
|
// per-SHA), so without them the cache would never once be read.
|
|
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')"
|
|
)
|
|
})
|
|
|
|
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([
|
|
'static_analysis',
|
|
'root_directory_guard',
|
|
'typecheck',
|
|
'git_compatibility',
|
|
'xterm_patch_sync',
|
|
'shell_contracts',
|
|
'test',
|
|
'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"')
|
|
})
|
|
})
|