Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d2f5242
ci: add a non-blocking Windows job for the oracle framework and gap CLI
KyleAMathews Nov 13, 2026
0fc897b
ci(windows): diagnostic split of the fast suites, fixture package.json
KyleAMathews Nov 13, 2026
72b9495
ci(windows): diagnostic replay, error-log dump, and discovered-config…
KyleAMathews Nov 13, 2026
dacfb88
fix(oracle-gaps): one path rule for Windows, and shell-aware replay l…
KyleAMathews Nov 13, 2026
8d96432
test: portable npm spawns, a segment rule for production files, nativ…
KyleAMathews Nov 13, 2026
ef9205d
ci(windows): sharded Windows jobs in place of the diagnostic ones
KyleAMathews Nov 13, 2026
8d2d037
fix(oracle-gaps): give TypeScript and the owner map one spelling of a…
KyleAMathews Nov 13, 2026
29c6959
ci(windows): P10 done-line, refusal, early-stop, and per-shell replay…
KyleAMathews Nov 13, 2026
00456e7
fix(oracle-gaps): oracles read the platform's replay lines, and scrat…
KyleAMathews Nov 13, 2026
f79aae7
fix(windows): forward-slash report paths, cmd quotes, six shards, and…
KyleAMathews Nov 13, 2026
8697227
fix(oracle-gaps): keys and report paths are forward-slash in the CLI …
KyleAMathews Nov 13, 2026
0c9f38d
ci(windows): the replay witness runs after a failed stop witness
KyleAMathews Nov 13, 2026
dc71773
ci(windows): print the replay output the witness reads (diagnostic)
KyleAMathews Nov 13, 2026
b8b48d7
ci(windows): the replay fixture declares its fixed seed
KyleAMathews Nov 13, 2026
00a2740
Merge remote-tracking branch 'origin/main' into ci/windows
KyleAMathews Nov 13, 2026
c1c34d2
fix(windows): the stop law on win32, the stop hook, the argv cap, and…
KyleAMathews Nov 13, 2026
ee1a5fd
fix(oracle-gaps): the temporary directory's real path for state, sock…
KyleAMathews Nov 13, 2026
70b118d
fix(oracle-gaps): the e2e fixture's temporary directory by its native…
KyleAMathews Nov 13, 2026
ebc39e6
ci(windows): split the blocking Windows jobs from the non-blocking on…
KyleAMathews Nov 13, 2026
50b4ab5
fix(windows): stop witness, replay quoting by the shells' grammar, an…
KyleAMathews Nov 14, 2026
b904d57
fix(oracle-gaps): path-spelling oracle generates win32 separators and…
KyleAMathews Nov 14, 2026
de8a373
fix(oracle-gaps): event-triggered stop, derived hostile names, and th…
KyleAMathews Nov 14, 2026
63bf49e
fix(oracle-gaps): constructed tests keep their names (classify reach)
KyleAMathews Nov 14, 2026
2b7b7f3
fix(windows): Rerun cmd lines, ! as no-form, separators by platform, …
KyleAMathews Nov 14, 2026
abc8b45
fix(windows): the witness feeds PowerShell its line on stdin, not as …
KyleAMathews Nov 14, 2026
69c82eb
diagnostic: the replay witness reports the shell's own error for a li…
KyleAMathews Nov 14, 2026
0772447
fix(windows): the -t argument never carries a shell-hostile character…
KyleAMathews Nov 14, 2026
ae44184
Revert "diagnostic: the replay witness reports the shell's own error …
KyleAMathews Nov 14, 2026
9698922
fix(windows): cmd env rule, exact witness matching, set-line readers,…
KyleAMathews Nov 14, 2026
257a5b0
fix(windows): the replay witness counts each printed line once, and r…
KyleAMathews Nov 14, 2026
0c42c33
fix(windows): the replay witness pairs each printed line with its cmd…
KyleAMathews Nov 14, 2026
08f028d
fix(windows): restore the replay witness's helpers that the pairing r…
KyleAMathews Nov 14, 2026
bd208ac
fix(windows): a Rerun line counts only for its own failing fixed unit…
KyleAMathews Nov 14, 2026
3b97795
fix(windows): the witness reads a fixed unit's FAIL line (`fixed seed…
KyleAMathews Nov 14, 2026
7d46701
fix(windows): the Rerun name check ignores vitest's colors
KyleAMathews Nov 14, 2026
1709814
fix(windows): exact FAIL-line matching, a percent-and-bang witness, a…
KyleAMathews Nov 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 296 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ jobs:
# and the classify, probe-form, and mutant-validity oracles in a third.
- run: |
if [ "$SHARD" = 3 ]; then
exec pnpm exec vitest --run --config oracle-gaps/vitest.config.mjs gap-report-classify gap-report-probe-form gap-report-mutant-validity
exec pnpm exec vitest --run --config oracle-gaps/vitest.config.mjs gap-report-classify gap-report-probe-form gap-report-mutant-validity gap-report-path-spelling
fi
pnpm exec vitest --run --config oracle-gaps/vitest.config.mjs gap-report-static -t "$(node scripts/pr-shards.mjs pattern oracle-gaps-static "$SHARD")"
working-directory: packages/oracles
Expand Down Expand Up @@ -168,6 +168,11 @@ jobs:
- test-oracle-gaps-fast
- test-oracle-gaps-end-to-end
- test-vitest-5
# The blocking Windows jobs (the non-blocking static and e2e jobs are not
# a dependency until they are green).
- windows-unit
- windows-gap-classify
- windows-gap-cli
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down Expand Up @@ -196,3 +201,293 @@ jobs:
with:
advanced-security: false
annotations: true
# Windows is a required part of CI: the framework, the eslint plugin, and
# the gap CLI run on windows-latest, split the way the Linux jobs split
# them. Each job stays under 10 minutes; each has a timeout backstop.
windows-unit:
name: Windows unit suites
runs-on: windows-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Tools
uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3
- run: pnpm --filter @tanstack/oracles run build
- run: pnpm --filter @tanstack/oracles run test
# Runs even when the framework tests fail, so each suite reports.
- if: always()
run: pnpm --filter @tanstack/eslint-plugin-oracles run test
windows-gap-oracles:
name: Windows gap report static oracle (${{ matrix.shard }}/6)
runs-on: windows-latest
# Not blocking yet: these suites fail on Windows, tracked by class in
# https://github.com/TanStack/oracles/issues/72. Remove this line when they
# are green.
continue-on-error: true
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Tools
uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3
- run: pnpm --filter @tanstack/oracles run build
# The same partition of the static oracle that Linux runs, by pattern.
- name: Gap report static oracle
shell: bash
working-directory: packages/oracles
env:
SHARD: ${{ matrix.shard }}
run: pnpm exec vitest --run --config oracle-gaps/vitest.config.mjs gap-report-static -t "$(node scripts/pr-shards.mjs pattern oracle-gaps-static-windows "$SHARD")"
windows-gap-classify:
name: Windows gap report classify, probe-form, and mutant-validity oracles
runs-on: windows-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Tools
uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3
- run: pnpm --filter @tanstack/oracles run build
- name: Gap report oracles
working-directory: packages/oracles
run: pnpm exec vitest --run --config oracle-gaps/vitest.config.mjs gap-report-classify gap-report-probe-form gap-report-mutant-validity gap-report-path-spelling
windows-gap-end-to-end:
name: Windows gap report end-to-end oracle (fixed seed, ${{ matrix.shard }}/6)
runs-on: windows-latest
# Not blocking yet: these suites fail on Windows, tracked by class in
# https://github.com/TanStack/oracles/issues/72. Remove this line when they
# are green.
continue-on-error: true
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Tools
uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3
- run: pnpm --filter @tanstack/oracles run build
- name: Gap report end-to-end oracle
shell: bash
working-directory: packages/oracles
env:
SHARD: ${{ matrix.shard }}
run: pnpm exec vitest --run --config oracle-gaps/vitest.config.mjs gap-report-oracle -t "$(node scripts/pr-shards.mjs pattern oracle-gaps-fixed-seed-windows "$SHARD")"
windows-gap-cli:
name: Windows gap CLI on a fixture
runs-on: windows-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Tools
uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3
- run: pnpm --filter @tanstack/oracles run build
- name: Write gap CLI fixture
shell: bash
run: |
set -euo pipefail
dir="$GITHUB_WORKSPACE/.ci-fixture"
mkdir -p "$dir/src" "$dir/tests"
cat > "$dir/package.json" <<'EOT'
{"name": "ci-fixture", "private": true, "type": "module"}
EOT
cat > "$dir/src/clamp.ts" <<'EOT'
export function clamp(x: number): number {
return x < 0 ? 0 : x > 10 ? 10 : x
}
EOT
cat > "$dir/tests/clamp.property.test.ts" <<'EOT'
import fc from 'fast-check'
import { differs, oracleProperty, pass } from '../../packages/oracles/dist/esm/index.js'
import { clamp } from '../src/clamp'

oracleProperty({
owns: ['src/clamp.ts#clamp'],
id: `fixture.clamp`,
law: {
statement: `clamp returns its argument within 0 and 10.`,
authority: `the CI fixture for the Windows gap-CLI run`,
limits: `integers from -20 to 20`,
},
histories: {
arbitrary: fc.integer({ min: -20, max: 20 }),
runs: 50,
fixedSeeds: [1],
states: [],
measured: [],
},
timeout: 5_000,
model: (x: number) => (x < 0 ? 0 : x > 10 ? 10 : x),
driver: (x: number, observe: (checkpoint: string, value: unknown) => void) => {
observe(`result`, clamp(x))
},
check: (_x: number, trace: ReadonlyArray<{ checkpoint: string; value: unknown }>, expected: number) => {
const seen = trace.find(({ checkpoint }) => checkpoint === `result`)?.value
return differs(`result`, `clamp`, seen, expected) ?? pass
},
})
EOT
cat > "$dir/vitest.config.mjs" <<'EOT'
export default {
test: { include: ['tests/**/*.test.ts'], environment: 'node', coverage: { enabled: false } },
}
EOT
# A failing property for the replay witness: every history fails, so a
# random unit prints its replay lines. Written in bash, as the fixture is.
- name: Write replay fixture
shell: bash
run: |
set -euo pipefail
dir="$GITHUB_WORKSPACE/.ci-replay"
mkdir -p "$dir/tests"
cat > "$dir/tests/fail.property.test.ts" <<'EOT'
import fc from 'fast-check'
import { differs, oracleProperty, pass } from '../../packages/oracles/dist/esm/index.js'

oracleProperty({
id: `fixture ’ & | < > ( ) ^ replay`,
law: {
statement: `a fixture that always fails, so its replay lines are printed.`,
authority: `the CI witness for the per-shell replay lines`,
limits: `integers from -20 to 20`,
},
histories: {
arbitrary: fc.integer({ min: -20, max: 20 }),
runs: 20,
fixedSeeds: [1],
states: [],
measured: [],
},
timeout: 5_000,
model: (x: number) => x,
driver: (x: number, observe: (checkpoint: string, value: unknown) => void) => {
observe(`result`, x + 1)
},
check: (_x: number, trace: ReadonlyArray<{ checkpoint: string; value: unknown }>, expected: number) =>
differs(`result`, `plus one`, trace.find(({ checkpoint }) => checkpoint === `result`)?.value, expected) ?? pass,
})
EOT
cat > "$dir/tests/hostile.property.test.ts" <<'EOT'
import fc from 'fast-check'
import { differs, oracleProperty, pass } from '../../packages/oracles/dist/esm/index.js'

oracleProperty({
id: `fixture Q"1 replay`,
law: {
statement: `a fixture that always fails, so its replay lines are printed.`,
authority: `the CI witness for the per-shell replay lines`,
limits: `integers from -20 to 20`,
},
histories: {
arbitrary: fc.integer({ min: -20, max: 20 }),
runs: 20,
fixedSeeds: [1],
states: [],
measured: [],
},
timeout: 5_000,
model: (x: number) => x,
driver: (x: number, observe: (checkpoint: string, value: unknown) => void) => {
observe(`result`, x + 1)
},
check: (_x: number, trace: ReadonlyArray<{ checkpoint: string; value: unknown }>, expected: number) =>
differs(`result`, `plus one`, trace.find(({ checkpoint }) => checkpoint === `result`)?.value, expected) ?? pass,
})
EOT
cat > "$dir/tests/percent.property.test.ts" <<'EOT'
import fc from 'fast-check'
import { differs, oracleProperty, pass } from '../../packages/oracles/dist/esm/index.js'

oracleProperty({
id: `fixture 100% done!`,
law: {
statement: `a fixture that always fails, so its replay lines are printed.`,
authority: `the CI witness for the per-shell replay lines`,
limits: `integers from -20 to 20`,
},
histories: {
arbitrary: fc.integer({ min: -20, max: 20 }),
runs: 20,
fixedSeeds: [1],
states: [],
measured: [],
},
timeout: 5_000,
model: (x: number) => x,
driver: (x: number, observe: (checkpoint: string, value: unknown) => void) => {
observe(`result`, x + 1)
},
check: (_x: number, trace: ReadonlyArray<{ checkpoint: string; value: unknown }>, expected: number) =>
differs(`result`, `plus one`, trace.find(({ checkpoint }) => checkpoint === `result`)?.value, expected) ?? pass,
})
EOT
printf '%s\n' "export default { test: { include: ['tests/**/*.test.ts'], environment: 'node', coverage: { enabled: false } } }" > "$dir/vitest.config.mjs"
# The user-facing path: PowerShell, the default shell, as a user runs it.
# Each run's stdout and stderr are kept apart, and the done line is
# checked on both streams against the exit code (P10).
- name: Gap CLI on the fixture
run: |
node packages/oracles/oracle-gaps/index.mjs --package .ci-fixture --source src/clamp.ts --config vitest.config.mjs > cli-out.txt 2> cli-err.txt
$code = $LASTEXITCODE
node packages/oracles/scripts/windows-witness.mjs done cli-out.txt cli-err.txt $code
if ($LASTEXITCODE -ne 0) { throw "the done line failed" }
- name: Gap CLI on the fixture, discovered config
run: |
node packages/oracles/oracle-gaps/index.mjs --package .ci-fixture --source src/clamp.ts > cli-out2.txt 2> cli-err2.txt
$code = $LASTEXITCODE
node packages/oracles/scripts/windows-witness.mjs done cli-out2.txt cli-err2.txt $code
if ($LASTEXITCODE -ne 0) { throw "the done line failed" }
# The daemon path: start it, check through it, then stop it.
- name: Gap CLI through the daemon
run: |
node packages/oracles/oracle-gaps/index.mjs --package .ci-fixture --source src/clamp.ts --config vitest.config.mjs --daemon > cli-out3.txt 2> cli-err3.txt
$code = $LASTEXITCODE
node packages/oracles/scripts/windows-witness.mjs done cli-out3.txt cli-err3.txt $code
if ($LASTEXITCODE -ne 0) { throw "the done line failed" }
- name: Stop the gap CLI daemon
if: always()
run: node packages/oracles/oracle-gaps/index.mjs --stop-daemon --package .ci-fixture
# A package with no package.json is refused, with exit 2 and a done line
# that says so.
- name: Gap CLI refuses a package with no package.json
run: |
New-Item -ItemType Directory -Force .ci-no-manifest | Out-Null
node packages/oracles/oracle-gaps/index.mjs --package .ci-no-manifest --source src/x.ts > nm-out.txt 2> nm-err.txt
$code = $LASTEXITCODE
if ($code -ne 2) { throw "want a refusal with exit 2, got $code" }
if (-not (Select-String -Path nm-out.txt, nm-err.txt -Pattern 'has no package.json' -Quiet)) { throw "the refusal does not name package.json" }
node packages/oracles/scripts/windows-witness.mjs done nm-out.txt nm-err.txt $code
if ($LASTEXITCODE -ne 0) { throw "the done line failed" }
# One early stop: SIGINT once the CLI has printed its first line. The
# run must end with the stop's done line on both streams, or with no
# output at all (the all-or-nothing law, #57).
- name: Early stop of the gap CLI
run: node packages/oracles/scripts/windows-witness.mjs stop .ci-fixture --package . --source src/clamp.ts --config vitest.config.mjs
# The per-shell replay lines: a failing property's PowerShell and cmd
# lines each reproduce its failure in their own shell (RED: the POSIX
# line does not, in PowerShell).
- name: Replay lines reproduce the failure in PowerShell and cmd
if: always()
run: |
node "$env:GITHUB_WORKSPACE\packages\oracles\node_modules\vitest\vitest.mjs" run --root .ci-replay > replay-out.txt 2>&1
Get-Content replay-out.txt
node packages/oracles/scripts/windows-witness.mjs replay replay-out.txt "$env:GITHUB_WORKSPACE\.ci-replay"
if ($LASTEXITCODE -ne 0) { throw "the replay witness failed" }
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ const TEST_FILE = join(
// test file has it off. Production source is `src` under a package or at the
// root, as the production config's globs say.
function predictSeverity(filePath) {
return filePath.includes(`/src/`) ? `error` : `off`
// A file is production when one of its directories is `src`. The path's
// separators are either kind on Windows, so the directories are split on both.
const directories = filePath.split(/[\\/]+/).slice(0, -1)
return directories.includes(`src`) ? `error` : `off`
}

// The rule's behavior is linted through the plugin's production config alone:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ function shippedFiles(dir) {
const packed = spawnSync(
`npm`,
[`pack`, `--dry-run`, `--json`, `--ignore-scripts`],
{ cwd: dir, encoding: `utf8` },
// On Windows npm is an npm.cmd shim, which only a shell runs.
{ cwd: dir, encoding: `utf8`, shell: process.platform === `win32` },
)
return JSON.parse(packed.stdout)[0].files.map((file) => file.path)
}
Expand Down Expand Up @@ -86,7 +87,8 @@ function strays(dir, file, text) {
.filter((cited) => {
const ships = [dirname(file), `.`].some((base) =>
[cited, cited.replace(/\.js$/, `.ts`)].some((name) =>
shipped.includes(join(base, name)),
// npm names a shipped file with forward slashes, on every platform.
shipped.includes(join(base, name).replaceAll(`\\`, `/`)),
),
)
const names =
Expand All @@ -101,7 +103,8 @@ function strays(dir, file, text) {
.map((match) => match[1])
.filter((name) => repoOnlyNames.has(name))
return [...paths, ...names].map(
(cited) => `${relative(repo, join(dir, file))}: ${cited}`,
(cited) =>
`${relative(repo, join(dir, file)).replaceAll(`\\`, `/`)}: ${cited}`,
)
}

Expand Down
5 changes: 3 additions & 2 deletions packages/oracles/oracle-gaps/analysis.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { existsSync, readFileSync, realpathSync } from 'node:fs'
import { basename, join } from 'node:path'
import { packagePath } from './args.mjs'
import { posix, relativeWithin } from './paths.mjs'
import { Refusal } from './ending.mjs'
import { ownership, readOwns, splitEntry } from './owners.mjs'
import { instrument } from './schemata.mjs'
Expand Down Expand Up @@ -59,7 +60,7 @@ function hostFor(ts) {
* has an owner through the call trees, throws too.
*/
export function analyze(ts, pkg, sources, testFiles) {
const relative = (file) => file.slice(pkg.length + 1)
const relative = (file) => relativeWithin(pkg, file) ?? posix(file)
// An oracle file is a test file that calls `oracleProperty`, or passes a
// literal `owns` list to any call, such as a wrapper's (owners.mjs).
const declarations = new Map()
Expand Down Expand Up @@ -124,7 +125,7 @@ export function analyze(ts, pkg, sources, testFiles) {
.flat()
.map((entry) => splitEntry(entry)[0]),
]),
].map((source) => join(pkg, source)),
].map((source) => posix(join(pkg, source))),
compilerOptions(ts),
hostFor(ts),
)
Expand Down
Loading
Loading