mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
* test(ci): ratchet Windows-gated tests into both registration lists
PR CI has one windows-2022 job running a curated explicit file list. Every
other job runs on ubuntu, where a Windows-gated suite self-skips and reports
success -- so an unregistered Windows-gated file executes on no machine and
passes green with nothing to tell the author.
Scans every test file for the win32 suite-level gate spellings in use plus the
.win32.test.* filename, and asserts each one appears in BOTH the
"Test Windows-specific boundaries" vitest argv and WINDOWS_PACKAGE_TESTS: the
classifier decides whether the job runs, the argv decides whether the file
runs. The eight already-unregistered files on main are held in a shrink-only
debt list.
* fix(ci): detect compound win32 gates in the lane-registration ratchet
The gate matcher anchored its argument on the closing paren, so
`runIf(platform === 'win32' && hasAddon)` was not matched at all -- the
guard excluded real Windows-gated files by accident of a regex rather
than by design, and would have missed a compound gate on a file that
genuinely needed registering.
Match the condition followed by `)` or `&&`, and resolve named flags from
their assignment in the same file, so `RUN_REAL = platform === 'win32' &&
env…` used as `runIf(RUN_REAL)` is detected whatever the flag is called
and whichever polarity it was written in. That replaces the hardcoded
`isWindows`/`IS_WINDOWS`/`isWin32` names, which guessed polarity from a
name; an imported flag stays undetected and is now documented with the
live example. `||` compounds are rejected on purpose: they can run off
Windows.
Ten env-opt-in suites surface as a result. They are win32-gated but also
require an `ORCA_REAL_*` env var, so registering them would not make CI
run them; they go in MANUAL_OPT_IN, whose entries are asserted to be
genuinely compound and env-gated so the list cannot become a quiet
parking spot.
Also: reuse `scanSourceTree` instead of a fifth divergent walk in the
repo (its docblock records the incident where a hand-rolled walk scanned
`tests/e2e/.cross-version-checkouts/`), adding an `extensions` option so
it can see `.mjs`; strip comments so prose about a gate is not a gate;
skip `mobile/`, which `classifyPrJobs` can never report as registered;
assert exactly one `windows-2022` job, the premise the guard rests on;
cap growth of both grandfathered lists; and test that the self-exemption
covers nothing but this file.
Corrects two docblock claims that were false: that nothing in the repo
computes a gate indirectly (three files did), and that a compound gate's
registration was asserted while only its execution was not (neither was).
* fix(ci): make the manual-opt-in exemption prove the env read reaches the gate
`requiresEnvOptIn` proved the file MENTIONED an env var, not that the gate
DEPENDED on one, so `runIf(platform === 'win32' && hasAddon)` in a file
that happens to read `process.env.RUNNER_TEMP` parked as manual. That is
the native-addon-bytes shape -- a test CI could run -- and only the cap
number stood in the way. Now the win32 check must be compound and one of
its other conjuncts must read `process.env` itself or name a const that
does, which still accepts all ten listed suites.
The compound clause guarding that hole was itself unasserted: deleting it
left every test green. Two fixtures close it, including an env read on the
same line as a bare gate, which is the case that makes the `&&` do work
rather than decorate.
Split FLAG_ASSIGNMENT by polarity. One shared `&&` lookahead was right for
`===` (a second conjunct narrows) and wrong for `!==` (it widens), so
`p = platform !== 'win32' && x` used as `skipIf(p)` read as Windows-only
though it runs on Windows and on POSIX when `x` is false. The literal form
was already rejected; routing it through a flag flipped the answer.
Widen the one-lane assertion from a `windows-2022` equality test to any
`runs-on` that could land on Windows -- `windows-latest`, a label array, a
`{ group, labels }` object -- treating an unresolvable `${{ }}` expression
as Windows so it fails closed.
Docblock: the case-level count is now deliberately approximate. The
reviewer measures 26 against this guard's 31; the figure moves with which
gate spellings are counted, and the policy does not rest on it.
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
292 lines
9.9 KiB
TypeScript
292 lines
9.9 KiB
TypeScript
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
import { join, relative } from 'node:path'
|
|
|
|
/**
|
|
* Shared file walk for the ratchet guards.
|
|
*
|
|
* Why one copy: four guards had grown their own `collectSourceFiles` /
|
|
* `isTestFile` / allowlist reader, and they had already drifted -- one skipped
|
|
* dot-directories and three did not, which is how the WSL separator guard came
|
|
* to scan `tests/e2e/.cross-version-checkouts/` and report 21 offenders that
|
|
* were copies of shipped releases. A guard that can be wrong about what it
|
|
* scanned is worse than no guard, because its count is the goalpost.
|
|
*/
|
|
|
|
const IGNORED_DIRECTORIES = new Set(['node_modules', 'dist', 'out', 'build', '.git'])
|
|
|
|
/** Tests may do the thing the guard forbids; that is often why they exist. */
|
|
export function isTestFile(relativePath: string): boolean {
|
|
return (
|
|
/\.(?:test|spec)\.tsx?$/.test(relativePath) ||
|
|
// `repro` must be a whole token: a bare substring exempted the shipped
|
|
// windows-terminal-capability-reprobe.ts from every guard using this walk.
|
|
/(?:test-harness|test-utils|test-setup|test-fixture|\brepro\b|reproduction)/.test(
|
|
relativePath
|
|
) ||
|
|
relativePath.includes('/__tests__/')
|
|
)
|
|
}
|
|
|
|
export type ScannedFile = { path: string; relativePath: string; source: string }
|
|
|
|
/**
|
|
* Every `.ts`/`.tsx` file under `root`, with its text.
|
|
*
|
|
* Dot-directories are skipped: they hold generated and vendored trees (the
|
|
* cross-version e2e checkouts among them), which are not ours to fix.
|
|
*
|
|
* `extensions` widens or narrows which filenames are read -- a guard over CI
|
|
* config also has to see `.mjs`, and one that only wants test files pays for
|
|
* reading nothing else.
|
|
*/
|
|
export function scanSourceTree(
|
|
root: string,
|
|
options: { includeTests?: boolean; extensions?: RegExp } = {}
|
|
): ScannedFile[] {
|
|
const extensions = options.extensions ?? /\.tsx?$/
|
|
const found: ScannedFile[] = []
|
|
const visit = (directory: string): void => {
|
|
for (const entry of readdirSync(directory)) {
|
|
if (IGNORED_DIRECTORIES.has(entry) || entry.startsWith('.') || entry === '__fixtures__') {
|
|
continue
|
|
}
|
|
const path = join(directory, entry)
|
|
if (statSync(path).isDirectory()) {
|
|
visit(path)
|
|
continue
|
|
}
|
|
if (!extensions.test(entry)) {
|
|
continue
|
|
}
|
|
const relativePath = relative(root, path).replace(/\\/g, '/')
|
|
if (!options.includeTests && isTestFile(relativePath)) {
|
|
continue
|
|
}
|
|
found.push({ path, relativePath, source: readFileSync(path, 'utf8') })
|
|
}
|
|
}
|
|
visit(root)
|
|
return found
|
|
}
|
|
|
|
/** Read a ratchet allowlist, dropping comments and blanks. */
|
|
export function readAllowlist(fixturePath: string): string[] {
|
|
return readFileSync(fixturePath, 'utf8')
|
|
.split('\n')
|
|
.map((line) => line.trim())
|
|
.filter((line) => line.length > 0 && !line.startsWith('#'))
|
|
}
|
|
|
|
/**
|
|
* Comments blanked out, so a construct documented in prose is not counted as code.
|
|
*
|
|
* Why a scanner and not two regexes: a POSIX glob inside a shell script written
|
|
* as a template literal contains a slash-star sequence, and the naive version
|
|
* read that as a comment opener, blanking everything to the next star-slash --
|
|
* 24,000 characters of live code in one file. A guard then read straight past a
|
|
* real unguarded spawn and reported the file clean, which is worse than no
|
|
* guard. Quote state is the difference, so it has to be tracked.
|
|
*/
|
|
export function stripComments(source: string): string {
|
|
let out = ''
|
|
let index = 0
|
|
let quote: string | null = null
|
|
while (index < source.length) {
|
|
const char = source[index]!
|
|
const next = source[index + 1]
|
|
if (quote) {
|
|
// Only a template literal may span lines. Resetting at a newline stops an
|
|
// apostrophe in prose, or a quote inside a regex literal, from swallowing
|
|
// the rest of the file and disabling comment stripping from there on.
|
|
if (char === '\n' && quote !== '`') {
|
|
quote = null
|
|
out += char
|
|
index += 1
|
|
continue
|
|
}
|
|
if (char === '\\') {
|
|
out += ' '
|
|
index += 2
|
|
continue
|
|
}
|
|
if (char === quote) {
|
|
quote = null
|
|
}
|
|
out += char
|
|
index += 1
|
|
continue
|
|
}
|
|
if (char === "'" || char === '"' || char === '`') {
|
|
quote = char
|
|
out += char
|
|
index += 1
|
|
continue
|
|
}
|
|
if (char === '/' && next === '*') {
|
|
const end = source.indexOf('*/', index + 2)
|
|
const stop = end === -1 ? source.length : end + 2
|
|
// Keep newlines so reported line numbers stay honest.
|
|
out += source.slice(index, stop).replace(/[^\n]/g, ' ')
|
|
index = stop
|
|
continue
|
|
}
|
|
if (char === '/' && next === '/') {
|
|
const end = source.indexOf('\n', index)
|
|
const stop = end === -1 ? source.length : end
|
|
out += ' '.repeat(stop - index)
|
|
index = stop
|
|
continue
|
|
}
|
|
out += char
|
|
index += 1
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* String contents replaced by spaces, quotes kept.
|
|
*
|
|
* Why: a brace matcher that counts parentheses inside a shell script embedded
|
|
* as a string closes the call early, so the options object -- and any flag in
|
|
* it -- falls outside the matched range and reads as absent.
|
|
*/
|
|
/**
|
|
* True when the lexer could not keep its bearings through the file.
|
|
*
|
|
* Why callers must check this: three separate attempts to make the blanker
|
|
* exact all shipped with a desync that silently hid real calls, and each time
|
|
* the offender count went DOWN, which read as progress. A scanner that cannot
|
|
* say "I lost track here" will keep under-reporting. Treat a desync as an
|
|
* offender -- over-reporting is a nuisance, under-reporting is a false clean.
|
|
*/
|
|
export function blankStringContentsDesynced(source: string): boolean {
|
|
return blankStringContents(source, true) !== ''
|
|
}
|
|
|
|
/**
|
|
* After a value `/` is division; after an opener or a binary operator it opens
|
|
* a regex.
|
|
*
|
|
* The set is deliberately narrow, because the two errors are not symmetric. A
|
|
* false negative leaves a pattern unblanked, which at worst desyncs the lexer
|
|
* -- and every caller treats desync as an offender, so it fails closed. A
|
|
* false positive blanks live code, and a scan that cannot see a call reports
|
|
* it clean. A wider set cost 13 real JSX spans (`<Icon size={14} /> : <Icon`)
|
|
* and swallowed a whole `execFile(...)` after `n-- / 2`, with no desync to
|
|
* show for it.
|
|
*
|
|
* So the postfix and value-terminating characters are excluded even though
|
|
* each also has a prefix reading: `!` (non-null assertion vs `!/re/.test(x)`),
|
|
* `+` `-` `*` `%` `^` `~` (postfix `--`/`++`), and `>` `}` (JSX close).
|
|
*/
|
|
function startsRegexLiteral(emitted: string): boolean {
|
|
const prev = emitted.replace(/\s+$/, '').at(-1)
|
|
return prev === undefined || '(,=:[&|?;'.includes(prev)
|
|
}
|
|
|
|
/** End index (exclusive) of the regex literal opening at `start`, or -1. */
|
|
function findRegexLiteralEnd(source: string, start: number): number {
|
|
let inClass = false
|
|
for (let index = start + 1; index < source.length; index += 1) {
|
|
const char = source[index]
|
|
if (char === '\\') {
|
|
index += 1
|
|
continue
|
|
}
|
|
// A `/` inside `[...]` is literal, so it must not close the pattern.
|
|
if (char === '[') {
|
|
inClass = true
|
|
} else if (char === ']') {
|
|
inClass = false
|
|
} else if (char === '\n') {
|
|
return -1
|
|
} else if (char === '/' && !inClass) {
|
|
return index + 1
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
export function blankStringContents(source: string, reportDesync = false): string {
|
|
let out = ''
|
|
let index = 0
|
|
let quote: string | null = null
|
|
// Brace depth per interpolation, so a `}` inside `${ { a: 1 } }` does not
|
|
// close it. A plain counter mistook the first `}` for the closer.
|
|
const templates: number[] = []
|
|
while (index < source.length) {
|
|
const char = source[index]!
|
|
if (quote === '`' && char === '$' && source[index + 1] === '{') {
|
|
templates.push(0)
|
|
quote = null
|
|
out += '${'
|
|
index += 2
|
|
continue
|
|
}
|
|
if (quote === null && templates.length > 0) {
|
|
const depth = templates.at(-1) ?? 0
|
|
if (char === '{') {
|
|
templates[templates.length - 1] = depth + 1
|
|
} else if (char === '}') {
|
|
if (depth === 0) {
|
|
templates.pop()
|
|
quote = '`'
|
|
out += char
|
|
index += 1
|
|
continue
|
|
}
|
|
templates[templates.length - 1] = depth - 1
|
|
}
|
|
}
|
|
if (quote) {
|
|
// Same rule stripComments uses: only a template may span lines, so an
|
|
// apostrophe in a regex literal cannot invert the rest of the file. That
|
|
// desync dropped a real unguarded spawn out of the ratchet.
|
|
if (char === '\n' && quote !== '`') {
|
|
quote = null
|
|
out += char
|
|
index += 1
|
|
continue
|
|
}
|
|
if (char === '\\') {
|
|
out += ' '
|
|
index += 2
|
|
continue
|
|
}
|
|
if (char === quote) {
|
|
quote = null
|
|
out += char
|
|
} else {
|
|
out += char === '\n' ? char : ' '
|
|
}
|
|
index += 1
|
|
continue
|
|
}
|
|
// A regex literal can carry a lone apostrophe (`/'/g` in a shell quoter),
|
|
// which reads as a string opener and desyncs the rest of the file. The
|
|
// classic prev-token test disambiguates it from division: after a value a
|
|
// `/` divides, after an operator or opener it starts a pattern.
|
|
// `/*` and `//` open comments, never patterns. Callers normally strip
|
|
// comments first, but this runs standalone too, and at index 0 a file
|
|
// starting with a banner comment read as one giant regex.
|
|
const next = source[index + 1]
|
|
if (char === '/' && next !== '/' && next !== '*' && startsRegexLiteral(out)) {
|
|
const end = findRegexLiteralEnd(source, index)
|
|
if (end !== -1) {
|
|
out += `/${' '.repeat(end - index - 1)}`
|
|
index = end
|
|
continue
|
|
}
|
|
}
|
|
if (char === "'" || char === '"' || char === '`') {
|
|
quote = char
|
|
}
|
|
out += char
|
|
index += 1
|
|
}
|
|
if (reportDesync) {
|
|
return quote !== null || templates.length > 0 ? 'desynced' : ''
|
|
}
|
|
return out
|
|
}
|