Files
orca/config/scripts/check-changed-code-quality.test.mjs
T
Brennan Benson 3039f50341 fix(ci): budget the changed-code batches from the whole spawn
The batch budget counted only the file list, but execve() charges argv, the
inherited environment and one pointer per entry against a single ceiling. A
256 KiB batch beside a large environment failed the spawn outright, so the
gate reported nothing instead of linting.

Compute the budget from everything the spawn carries: the Node binary, the
Oxlint entry point, the fixed flags and — on POSIX only, since Windows ships
it in a separate block — the environment. Budget against half of macOS'
kern.argmax rather than all of it: a Node child SIGSEGVs (24) or throws a
stack-overflow RangeError (26) near 970 KiB, well before E2BIG at 1,048 KiB.

Resolve the Oxlint launcher from its own manifest instead of assuming a flat
node_modules layout, and name the scan and batch when a spawn fails so it
reads as a launch failure rather than a crash in the gate.
2026-08-29 18:58:12 -07:00

287 lines
11 KiB
JavaScript

import { describe, expect, it } from 'vitest'
import {
OXLINT_SCANS,
batchFilesByArgumentBytes,
diagnosticTouchesAddedLines,
environmentEntries,
isMovedCode,
maxBatchArgumentBytes,
overlapsAddedLines,
parseAddedLineRanges,
runOxlintScan
} from './check-changed-code-quality.mjs'
describe('changed-code quality line matching', () => {
it('parses added and replaced hunk ranges while ignoring deletions', () => {
const ranges = parseAddedLineRanges(
['@@ -10,2 +10,3 @@', '@@ -20 +21 @@', '@@ -40,4 +42,0 @@', '@@ -50 +48,2 @@'].join('\n')
)
expect(ranges).toEqual([
{ start: 10, end: 12 },
{ start: 21, end: 21 },
{ start: 48, end: 49 }
])
})
it('matches diagnostics that overlap any added line', () => {
const ranges = [
{ start: 5, end: 7 },
{ start: 12, end: 12 }
]
expect(overlapsAddedLines(3, 5, ranges)).toBe(true)
expect(overlapsAddedLines(8, 11, ranges)).toBe(false)
expect(overlapsAddedLines(12, 14, ranges)).toBe(true)
})
it('normalizes absolute diagnostic paths before matching', () => {
const root = process.cwd()
const file = 'config/scripts/check-changed-code-quality.test.mjs'
const diagnostic = {
filename: `${root}/${file}`,
labels: [{ span: { line: 24 } }]
}
expect(
diagnosticTouchesAddedLines(diagnostic, new Map([[file, [{ start: 24, end: 24 }]]]), root)
).toBe(true)
})
// Why: pinning --config disables nested-config discovery, so root rules that
// mobile/.oxlintrc.json turns off would fail the gate on mobile files.
it('lets the untyped scan discover nested configs instead of pinning the root config', () => {
const scan = OXLINT_SCANS.find((candidate) => candidate.label === 'code quality')
expect(scan.args).not.toContain('--config')
expect(scan.args).not.toContain('--disable-nested-config')
})
})
describe('moved-code exemption', () => {
it('treats a verbatim contiguous block from the base as moved', () => {
const base = [['const a = 1', 'items.map((item, index) => (', 'key={index}', '))']]
expect(isMovedCode(['items.map((item, index) => (', 'key={index}', '))'], base)).toBe(true)
})
it('ignores indentation and whitespace changes from the move', () => {
const base = [[' items.map((item, index) => (', ' key={index}']]
expect(isMovedCode(['items.map((item, index) => (', 'key={index}'], base)).toBe(true)
})
it('does not exempt a genuinely new violation', () => {
const base = [['const a = 1', 'const b = 2']]
expect(isMovedCode(['rows.map((row, i) => <td key={i} />)'], base)).toBe(false)
})
it('does not exempt a block that is only partly present in the base', () => {
const base = [['doThing()', 'unrelated()']]
expect(isMovedCode(['doThing()', 'newlyAddedSideEffect()'], base)).toBe(false)
})
it('tolerates a few lines appended inside the moved block', () => {
// A split commonly grows a hook dependency array when closure variables
// become props; the moved body around it is still moved.
const body = Array.from({ length: 20 }, (_, i) => `line${i}()`)
const base = [body]
const moved = [...body.slice(0, 19), 'newDep,', body[19]]
expect(isMovedCode(moved, base)).toBe(true)
})
it('does not exempt when the anchor line is absent from the base', () => {
const base = [['doThing()', 'filler()', 'other()']]
expect(isMovedCode(['brandNewCall()', 'doThing()', 'other()'], base)).toBe(false)
})
it('does not exempt when most of the block is absent from the base', () => {
const base = [['keep0()', 'keep1()', 'unrelated()']]
const mostlyNew = ['keep0()', ...Array.from({ length: 18 }, (_, i) => `fresh${i}()`)]
expect(isMovedCode(mostlyNew, base)).toBe(false)
})
it('ignores blank lines when matching', () => {
const base = [['a()', 'b()']]
expect(isMovedCode(['a()', '', 'b()'], base)).toBe(true)
})
it('never exempts an empty highlight', () => {
expect(isMovedCode(['', ' '], [['a()']])).toBe(false)
})
})
describe('argument batching', () => {
// Mirrors the script's own accounting: every argv/envp entry costs a pointer beside its bytes.
const spawnBytes = (entries) =>
entries.reduce((total, entry) => total + Buffer.byteLength(entry, 'utf8') + 12, 0)
// Large enough to exceed the real byte budget, so the batching that ships is what runs.
const oversizedSet = Array.from(
{ length: 20000 },
(_, index) => `src/generated/module-${index}.ts`
)
const fixedArguments = [
'/usr/local/bin/node',
'/repo/node_modules/oxlint/bin/oxlint',
'--format',
'json'
]
// Big enough that the budget must shrink below the cap, small enough to still batch.
const largeEnvironment = { PATH: '/usr/bin', ORCA_LARGE_VARIABLE: 'x'.repeat(300 * 1024) }
const fatEnvironment = { PATH: '/usr/bin', ORCA_FAT_VARIABLE: 'x'.repeat(900 * 1024) }
// Half of macOS kern.argmax: spawning Node fails from stack pressure near 970 KiB, well
// before the kernel's own 1 MiB E2BIG.
const POSIX_CEILING = 512 * 1024
// CreateProcess' documented lpCommandLine maximum.
const WINDOWS_CEILING = 32767
it('runs a normal changed set as a single invocation', () => {
const files = Array.from({ length: 50 }, (_, index) => `src/renderer/src/module-${index}.tsx`)
expect(batchFilesByArgumentBytes(files)).toEqual([files])
})
it('uses a Windows-safe budget without shrinking Unix batches', () => {
expect(maxBatchArgumentBytes({ platform: 'win32', env: {} })).toBe(32767 - 8 * 1024)
expect(maxBatchArgumentBytes({ platform: 'linux', env: {} })).toBe(256 * 1024)
expect(maxBatchArgumentBytes({ platform: 'darwin', env: {} })).toBe(256 * 1024)
})
it('splits an oversized set into batches that each fit the limit', () => {
const batches = batchFilesByArgumentBytes(oversizedSet)
expect(batches.length).toBeGreaterThan(1)
expect(batches.flat()).toEqual(oversizedSet)
for (const batch of batches) {
expect(spawnBytes(batch)).toBeLessThanOrEqual(256 * 1024)
}
})
// Why: an empty argument list makes Oxlint lint the whole working directory.
it('never emits an empty batch, even when the first path is longer than the limit', () => {
const batches = batchFilesByArgumentBytes(['src/very-long-path.ts', 'src/a.ts'], 10)
expect(batches.every((batch) => batch.length > 0)).toBe(true)
expect(batches.flat()).toEqual(['src/very-long-path.ts', 'src/a.ts'])
})
it('counts multi-byte paths by their byte length, not their character count', () => {
expect(batchFilesByArgumentBytes(['src/\u00e9.ts', 'src/b.ts'], 30)).toEqual([
['src/\u00e9.ts'],
['src/b.ts']
])
})
// Why: execve() charges argv and the inherited environment against one ceiling, so a
// budget computed from the file list alone stacks 256 KiB of paths on top of whatever the
// shell already carries, and the spawn fails before Oxlint ever starts.
it('keeps the whole POSIX spawn — arguments and environment — under the ceiling', () => {
const limit = maxBatchArgumentBytes({
platform: 'darwin',
fixedArguments,
env: largeEnvironment
})
const batches = batchFilesByArgumentBytes(oversizedSet, limit)
const environmentCost = spawnBytes(environmentEntries(largeEnvironment))
expect(batches.length).toBeGreaterThan(1)
for (const batch of batches) {
expect(spawnBytes([...fixedArguments, ...batch]) + environmentCost).toBeLessThanOrEqual(
POSIX_CEILING
)
}
})
it('shrinks the POSIX budget as the environment grows', () => {
const small = maxBatchArgumentBytes({ platform: 'darwin', fixedArguments, env: { A: 'a' } })
const large = maxBatchArgumentBytes({
platform: 'darwin',
fixedArguments,
env: largeEnvironment
})
expect(large).toBeLessThan(small)
})
// Why: CreateProcess caps the command line only; the environment ships in its own block.
it('budgets Windows from the command line alone and stays inside CreateProcess', () => {
const limit = maxBatchArgumentBytes({
platform: 'win32',
fixedArguments,
env: fatEnvironment
})
expect(limit).toBe(maxBatchArgumentBytes({ platform: 'win32', fixedArguments, env: {} }))
for (const batch of batchFilesByArgumentBytes(oversizedSet, limit)) {
expect(spawnBytes([...fixedArguments, ...batch])).toBeLessThanOrEqual(WINDOWS_CEILING)
}
})
// Why: a zero budget would spawn Oxlint once per path instead of failing usefully.
it('floors the budget when the environment alone exceeds the ceiling', () => {
const limit = maxBatchArgumentBytes({
platform: 'darwin',
fixedArguments,
env: { ORCA_FAT_VARIABLE: 'x'.repeat(4 * 1024 * 1024) }
})
expect(limit).toBe(8 * 1024)
})
it('ignores environment entries that carry no value', () => {
expect(environmentEntries({ A: 'a', B: undefined })).toEqual(['A=a'])
})
})
describe('diagnostic collection across batches', () => {
const scan = { label: 'code quality', args: [] }
const diagnosticFor = (file) => ({ filename: file, message: `finding in ${file}` })
const files = Array.from({ length: 20000 }, (_, index) => `src/generated/module-${index}.ts`)
const observeBatches = () => {
const batches = []
runOxlintScan('/repo', scan, files, (_root, _scan, batch) => {
batches.push(batch)
return JSON.stringify({ diagnostics: [] })
})
return batches
}
const reportOnly = (failing) => (_root, _scan, batch) =>
JSON.stringify({ diagnostics: batch.includes(failing) ? [diagnosticFor(failing)] : [] })
it('keeps the diagnostics of every batch, not just the last one', () => {
const spawnBatch = (_root, _scan, batch) =>
JSON.stringify({ diagnostics: batch.map(diagnosticFor) })
expect(observeBatches().length).toBeGreaterThan(1)
expect(runOxlintScan('/repo', scan, files, spawnBatch)).toEqual(files.map(diagnosticFor))
})
it('reports a finding that only a middle batch produces', () => {
const batches = observeBatches()
expect(batches.length).toBeGreaterThan(2)
const failing = batches.at(Math.floor(batches.length / 2)).at(0)
expect(batches.at(0)).not.toContain(failing)
expect(batches.at(-1)).not.toContain(failing)
expect(runOxlintScan('/repo', scan, files, reportOnly(failing))).toEqual([
diagnosticFor(failing)
])
})
it('reports a finding that only the last batch produces', () => {
const batches = observeBatches()
const failing = batches.at(-1).at(-1)
expect(batches.at(0)).not.toContain(failing)
expect(runOxlintScan('/repo', scan, files, reportOnly(failing))).toEqual([
diagnosticFor(failing)
])
})
// Why: the whole point is that no single invocation carries the full argument list.
it('never hands the whole oversized set to one invocation', () => {
const batchSizes = observeBatches().map((batch) => batch.length)
expect(batchSizes.length).toBeGreaterThan(1)
expect(Math.max(...batchSizes)).toBeLessThan(files.length)
})
})