Files
orca/config/scripts/ci-background-steps.test.mjs
T
92dfb4fda6 test: trim redundant coverage and fix vacuous assertions (#32305)
* test: remove redundant cases and repair behavioral assertions

* docs: reconcile audit records with current main

* fix(deps): apply security updates from #32101

Apply the six-file DOMPurify 3.4.17 and Next.js 16.4.1 patch from PR #32101 to resolve the dependency security gate for this test audit. Original patch by Brennan Benson.

* test: satisfy changed-line quality gates

Add required braces and replace filter indexing with find. Rerun both CI quality gates and the 27 affected relay tests; refresh audit hashes and record the corrections.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-10-23 03:52:51 -07:00

96 lines
3.3 KiB
JavaScript

import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
import { join } from 'node:path'
import { parse } from 'yaml'
import { expect, it } from 'vitest'
// Why: the runner rewrites $GITHUB_EVENT_PATH in place (truncate, then write) at every step start,
// and background steps start on their own task. A JavaScript action (@actions/github parses the
// payload at import) or composite (its inner steps start later) in the background therefore races
// the foreground's step starts and reads an empty file: "Unexpected end of JSON input".
const workflowFiles = readdirSync('.github/workflows')
.filter((name) => /\.ya?ml$/.test(name))
.map((name) => join('.github/workflows', name))
const actionFiles = readdirSync('.github/actions')
.map((name) => join('.github/actions', name, 'action.yml'))
.filter((file) => existsSync(file))
// Single-step JavaScript actions whose bundle never reads the payload (checked by searching dist/index.js).
const payloadFreeActions = ['ruby/setup-ruby@v1']
// The runner runs 10 at once; a queued step starts (and rewrites the payload) mid-foreground-step.
const backgroundSlots = 10
function stepLists(file) {
const document = parse(readFileSync(file, 'utf8'))
return document.jobs
? Object.entries(document.jobs).map(([job, value]) => [job, value.steps ?? []])
: [['composite', document.runs?.steps ?? []]]
}
function backgroundSteps() {
const found = []
for (const file of [...workflowFiles, ...actionFiles]) {
for (const [job, steps] of stepLists(file)) {
for (const step of steps) {
if (step.background) {
found.push({ where: `${file} ${job} ${step.id ?? step.name}`, step })
}
}
}
}
return found
}
function readsEventPayload(run) {
const scripts = [...run.matchAll(/(?:config\/scripts|\.github\/actions)\/[\w./-]+/g)].map(
(match) => match[0]
)
return (
run.includes('GITHUB_EVENT_PATH') ||
scripts.some(
(file) =>
existsSync(file) &&
statSync(file).isFile() &&
readFileSync(file, 'utf8').includes('GITHUB_EVENT_PATH')
)
)
}
it('finds the background steps it guards', () => {
expect(backgroundSteps().length).toBeGreaterThan(0)
})
it('backgrounds only single run steps, never actions that start later steps or parse the payload', () => {
const actions = backgroundSteps()
.filter(({ step }) => typeof step.run !== 'string' && !payloadFreeActions.includes(step.uses))
.map(({ where }) => where)
expect(actions).toEqual([])
})
it('keeps the event payload out of background run steps', () => {
const readers = backgroundSteps()
.filter(({ step }) => typeof step.run === 'string' && readsEventPayload(step.run))
.map(({ where }) => where)
expect(readers).toEqual([])
})
it('never queues a background step behind a full slot pool', () => {
const crowded = []
for (const file of workflowFiles) {
for (const [job, steps] of stepLists(file)) {
const open = new Set()
for (const [index, step] of steps.entries()) {
if (step.background) {
open.add(step.id ?? index)
}
for (const id of [step.wait ?? []].flat()) {
open.delete(id)
}
if (open.size > backgroundSlots) {
crowded.push(`${file} ${job}`)
}
}
}
}
expect(crowded).toEqual([])
})