Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d0a4005
fix(repo): resolve the e2e fixture bake against a pinned registry sna…
systemfsoftware-maker Oct 10, 2026
f027322
feat(e2e): move npm wire schemas into e2e-core and add the pure lock …
systemfsoftware-maker Oct 10, 2026
cc078a6
test(e2e): fail check on fixture lock drift with E2E_PINS_DRIFT
systemfsoftware-maker Oct 10, 2026
e33869a
feat(e2e): commit a package-lock.json per fixture with fixtures:lock
systemfsoftware-maker Oct 10, 2026
47c00ee
Merge remote-tracking branch 'origin/main' into fix/hermetic-e2e-bake
systemfsoftware-maker Oct 10, 2026
43cc5a4
fix(e2e): bake each fixture with one npm ci from its committed lock
systemfsoftware-maker Oct 10, 2026
4865367
fix(e2e): bake installs the lock check validated, closure versions re…
systemfsoftware-maker Oct 10, 2026
35f70ff
fix(e2e): pack the generator's closure, so the staged manifest matche…
systemfsoftware-maker Oct 10, 2026
0ef60f7
Merge remote-tracking branch 'origin/main' into fix/hermetic-e2e-bake
systemfsoftware-maker Oct 10, 2026
04ad77b
Merge remote-tracking branch 'origin/main' into fix/hermetic-e2e-bake
systemfsoftware-maker Oct 10, 2026
ddda281
test(e2e): plant a 2 s fixture install deadline (revert follows)
systemfsoftware-maker Oct 10, 2026
433325d
Revert "test(e2e): plant a 2 s fixture install deadline (revert follo…
systemfsoftware-maker Oct 10, 2026
542e353
fix(e2e-core): fail the pins check when npm cannot load a fixture lock
systemfsoftware-maker Oct 10, 2026
7b003ab
fix(e2e-core): attempt and report every fixture when a bake install f…
systemfsoftware-maker Oct 10, 2026
c357696
Merge remote-tracking branch 'origin/main' into fix/hermetic-e2e-bake
systemfsoftware-maker Oct 10, 2026
00d2eed
fix(e2e-core): pin every catalog package the workspace resolves once
systemfsoftware-maker Oct 10, 2026
5233488
Merge remote-tracking branch 'origin/main' into fix/hermetic-e2e-bake
systemfsoftware-maker Oct 10, 2026
0c437e4
fix(e2e): time the bake's image pull and boot apart from its installs
systemfsoftware-maker Oct 10, 2026
83fa46b
test(e2e-core): restore the workspace catalog parser laws
systemfsoftware-maker Oct 10, 2026
8d7c3b8
fix(e2e-core): key the fixture-lock drift check on pnpm-lock.yaml
systemfsoftware-maker Oct 11, 2026
6ee71a8
fix(e2e-core): decode npm ls into a listing union and fail undecided …
systemfsoftware-maker Oct 11, 2026
80e0e2e
fix(e2e): budget the bake by measured phase and name each overrun
systemfsoftware-maker Oct 11, 2026
957796d
fix(e2e-core): key npm locks and manifests by npm names and lock paths
systemfsoftware-maker Oct 11, 2026
e555e27
Merge branch 'fix/manifest-document-keys' into fix/hermetic-e2e-bake
systemfsoftware-maker Oct 11, 2026
d3080c4
Merge branch 'fix/manifest-document-keys' into fix/hermetic-e2e-bake
systemfsoftware-maker Oct 11, 2026
6304139
Merge branch 'fix/manifest-document-keys' into fix/hermetic-e2e-bake
systemfsoftware-maker Oct 11, 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
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,27 @@ jobs:
# the `if: always()` telemetry steps run inside the job's 30-minute budget. A cancelled job also
# runs `if: always()` steps, but only within GitHub's 5-minute cancellation window.
- name: E2E lane
id: lane
env:
STRYKER_E2E_BAKE_RECORD: ${{ runner.temp }}/e2e-bake.json
run: >-
timeout --signal=TERM --kill-after=10s 1200 pnpm test:e2e -- ${{ matrix.files }}
${{ startsWith(matrix.shard, 'rest') && '--exclude=tests/enterprise-mutation-lifecycle.e2e.test.ts --exclude=tests/enterprise-monorepo-sabotage.e2e.test.ts --exclude=tests/svelte-app.e2e.test.ts --exclude=tests/enterprise-composite-checker.e2e.test.ts' || '' }}
- name: Bake report
if: always()
env:
RECORD: ${{ runner.temp }}/e2e-bake.json
LANE: ${{ steps.lane.outcome }}
run: |
if [ -f "$RECORD" ]; then
jq -r .summary "$RECORD" >> "$GITHUB_STEP_SUMMARY"
jq -r '.annotations[]' "$RECORD"
elif [ "$LANE" = success ]; then
echo "::warning title=E2E_BAKE_NO_RECORD::The E2E lane passed but global setup wrote no bake record, so this leg reports no bake state. Next: check that STRYKER_E2E_BAKE_RECORD reaches vitest (turbo.json test:e2e passThroughEnv)."
else
printf "### Fixture bake\n\n\`E2E_BAKE_NO_RECORD\`: the lane ended (%s) before global setup recorded the bake.\n" "$LANE" >> "$GITHUB_STEP_SUMMARY"
echo "::error title=E2E_BAKE_NO_RECORD::The E2E lane ended ($LANE) before global setup recorded the fixture bake. Next: read the global setup error in the E2E lane log; the e2e-telemetry artifact holds the e2e.setup spans."
fi
- name: Export telemetry
if: always()
run: ./test/e2e/scripts/export-traces.ts
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 95 additions & 0 deletions test/e2e-core/bake/bake-fixtures.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/bin/sh
set -eu

argv_error() {
echo "E2E_BAKE_ARGV: bake-fixtures.sh $1. Next: call it as bake-fixtures.sh --root=<baked dir> --deadline=<seconds> --lanes=<count>, as the harness's bakeMissing does." >&2
exit 2
}

option() {
case "${2-}" in
"--$1="?*) printf '%s' "${2#--"$1"=}" ;;
*) argv_error "got '${2-}' where --$1=<value> belongs" ;;
esac
}

root="$(option root "${1-}")"
deadline="$(option deadline "${2-}")"
lanes="$(option lanes "${3-}")"
[ "$#" -eq 3 ] || argv_error "got $# arguments where exactly --root, --deadline and --lanes belong"
case "$deadline$lanes" in
*[!0-9]*) argv_error "got a non-numeric --deadline=$deadline or --lanes=$lanes" ;;
esac
[ "$deadline" -ge 1 ] || argv_error "got --deadline=$deadline where a deadline of at least 1 second belongs"
[ "$lanes" -ge 1 ] || argv_error "got --lanes=$lanes where a lane count of at least 1 belongs"

logs="$(mktemp -d)"
: >"$logs/reasons"

reason() {
echo "$1: $2 Next: $3" >>"$logs/reasons"
}

packs_member() {
case "$1" in
/packs/* | */packs/*) return 0 ;;
esac
grep -qF "/packs/$(basename "$1")" package-lock.json 2>/dev/null || return 1
}

relock="pnpm --filter @systemfsoftware/stryker-e2e-core fixtures:lock"

install() {
id="$1"
log="$logs/$id.log"
echo "[bake] $id: npm ci" >&2
started="$(date +%s)"
if timeout -s TERM -k 10 "$deadline" npm ci --no-audit --no-fund --loglevel=error >"$log" 2>&1; then
echo "[bake] $id: installed in $(($(date +%s) - started))s" >&2
return 0
else
code=$?
fi
elapsed=$(($(date +%s) - started))
grep -v '^npm notice' "$log" | tail -n 20 >&2
tarball="$(sed -n 's/^npm error path \(.*\.tgz\)$/\1/p' "$log" | head -n 1)"
first="$(sed -n 's/^npm error //p' "$log" | head -n 1 | cut -c1-200)"
mismatch="$(grep -E '^npm error (Missing|Invalid): ' "$log" | head -n 3 | sed 's/^npm error //' | tr '\n' ' ' | cut -c1-400)"
if [ "$code" -eq 124 ] || [ "$elapsed" -ge "$deadline" ]; then
reason E2E_BAKE_INSTALL_STALLED "$id: npm ci did not finish within ${deadline}s (exit $code after ${elapsed}s)." "if the registry was slow, re-run the job; if it stalls again, reproduce it against a staged fixture: the packs come from packing the workspace closure (pnpm -r --filter=<name> pack --out <staging>/packs/%s.tgz) and the harness stages fixtures at <staging>/baked/<id>, so run npm ci in that staged directory on the host to find the request that hangs."
elif [ -n "$tarball" ] && packs_member "$tarball"; then
reason E2E_BAKE_TARBALL_MISSING "$id: npm ci found no closure tarball $(basename "$tarball") at $tarball: ${first:-no npm error line}." "the lock names a closure member the harness did not pack; run $relock and commit test/e2e/testResources/$id/package-lock.json."
else
reason E2E_BAKE_FAILED "$id: npm ci exited $code: ${first:-no npm error line}${mismatch:+ ($mismatch)}." "run $relock, commit test/e2e/testResources/$id/package-lock.json, and if it still fails run npm ci in that fixture on the host."
fi
return "$code"
}

lane() {
own="$1"
index=0
lane_failed=0
for dir in "$root"/*/; do
if [ $((index % lanes)) -eq "$own" ]; then
cd "$dir"
install "$(basename "$dir")" || lane_failed=1
fi
index=$((index + 1))
done
return "$lane_failed"
}

pids=''
n=0
while [ "$n" -lt "$lanes" ]; do
lane "$n" &
pids="$pids $!"
n=$((n + 1))
done

failed=0
for pid in $pids; do
wait "$pid" || failed=1
done
cat "$logs/reasons" >&2
exit "$failed"
5 changes: 4 additions & 1 deletion test/e2e-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"clean": "rimraf dist",
"build": "tsdown",
"typecheck": "tsc -b",
"fixtures:lock": "tsx --conditions=@systemfsoftware/source scripts/lock-fixtures.ts",
"test": "vitest run",
"mutation": "stryker run",
"lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}",
Expand All @@ -24,7 +25,8 @@
"@systemfsoftware/effect-cell-types": "catalog:",
"@systemfsoftware/stryker-js-cli-contract": "workspace:^",
"@systemfsoftware/stryker-js-plugin-interface": "workspace:^",
"effect": "catalog:"
"effect": "catalog:",
"yaml": "catalog:"
},
"devDependencies": {
"@effect/platform-node": "catalog:",
Expand All @@ -51,6 +53,7 @@
"rimraf": "catalog:",
"ts-morph": "catalog:",
"tsdown": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
},
Expand Down
27 changes: 27 additions & 0 deletions test/e2e-core/scripts/lock-fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NodeFileSystem, NodePath, NodeRuntime } from '@effect/platform-node'
import * as NodeChildProcessSpawner from '@effect/platform-node/NodeChildProcessSpawner'
import * as Effect from 'effect/Effect'
import * as FileSystem from 'effect/FileSystem'
import * as Layer from 'effect/Layer'
import * as Path from 'effect/Path'

import { fixturesOf, lockFixture, repoLockContextOf } from '../tests/__fixtures__/fixture-locks.js'

const FILE_AND_PATH = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)

const PORTS = Layer.mergeAll(FILE_AND_PATH, NodeChildProcessSpawner.layer.pipe(Layer.provide(FILE_AND_PATH)))

const lockEveryFixture = Effect.scoped(Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
const staging = yield* fs.makeTempDirectoryScoped({ prefix: 'fixture-locks-' })
const repoRoot = yield* path.fromFileUrl(new URL('../../..', import.meta.url))
const context = yield* repoLockContextOf({ repoRoot, stagingRoot: staging })
const fixtures = yield* fixturesOf(path.join(repoRoot, 'test', 'e2e', 'testResources'))
yield* Effect.forEach(fixtures, (fixture) =>
lockFixture({ context, fixture }).pipe(
Effect.tap(() => Effect.log(`locked ${fixture.fixtureId}`)),
))
}))

NodeRuntime.runMain(lockEveryFixture.pipe(Effect.provide(PORTS)))
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, it } from '@systemfsoftware/vitest'
import * as Result from 'effect/Result'
import * as S from 'effect/Schema'

import {
admitFixtureLock,
AdmitFixtureLockCommand,
LockAdmitted,
LockProblemsListed,
LockUnreadable,
} from '../admit-fixture-lock.workflow.js'

const failing = (command: AdmitFixtureLockCommand): AdmitFixtureLockCommand =>
AdmitFixtureLockCommand.make({
fixtureId: command.fixtureId,
exitCode: command.exitCode === 0 ? 1 : command.exitCode,
stdout: command.stdout,
stderr: command.stderr,
})

describe('admitFixtureLock', () => {
it.prop(
'∀c_ExitZero_≡LockAdmitted',
{ of: [AdmitFixtureLockCommand], subject: admitFixtureLock },
(subject, [command]) => subject(command).pipe(Result.getOrThrow, S.is(LockAdmitted)) === (command.exitCode === 0),
)

it.prop(
'∀c_NonZeroExit_≡NamedProblemsOrUnreadable',
{ of: [AdmitFixtureLockCommand], subject: admitFixtureLock },
(subject, [command]) => {
const admission = Result.getOrThrow(subject(failing(command)))
return admission.fixtureId === command.fixtureId &&
((S.is(LockProblemsListed)(admission) && admission.problems.length > 0) ||
(S.is(LockUnreadable)(admission) && admission.detail.length > 0))
},
)

it.prop(
'∀c_ReportedProblemList_≡ListedExactlyWhenNonEmpty',
{ of: [AdmitFixtureLockCommand, S.Array(S.String)], subject: admitFixtureLock },
(subject, [command, problems]) => {
const reported = AdmitFixtureLockCommand.make({
fixtureId: command.fixtureId,
exitCode: command.exitCode === 0 ? 1 : command.exitCode,
stdout: JSON.stringify({ problems }),
stderr: command.stderr,
})
const admission = Result.getOrThrow(subject(reported))
return admission.fixtureId === command.fixtureId &&
(problems.length > 0
? S.is(LockProblemsListed)(admission) &&
JSON.stringify(admission.problems) === JSON.stringify(problems)
: S.is(LockUnreadable)(admission) && admission.detail.length > 0)
},
)
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it } from '@systemfsoftware/vitest'
import * as Arbitrary from 'effect/Arbitrary'
import * as Equal from 'effect/Equal'
import * as Rec from 'effect/Record'
import * as Result from 'effect/Result'
import * as S from 'effect/Schema'

Expand All @@ -10,7 +11,12 @@ import {
resolveCatalogSpecs,
type WorkspaceCatalogs,
} from '../catalog-resolution.js'
import { MalformedFixtureManifest, UnresolvedCatalogSpec } from '../catalog-resolution.schema.js'
import {
DependencySpecs,
MalformedFixtureManifest,
UnresolvedCatalogSpec,
WorkspaceCatalogsYaml,
} from '../catalog-resolution.schema.js'

const MANIFEST = 'fixture/package.json'
const DEFAULT_SENTINEL = 'default-catalog-sentinel'
Expand Down Expand Up @@ -146,20 +152,22 @@ describe('parseFixtureManifest', () => {

describe('parseWorkspaceCatalogs', () => {
it.prop(
'∀c_WorkspaceDefaultCatalogSection_≡DefaultCatalog',
{ of: [nameArb, rangeArb], subject: parseWorkspaceCatalogs },
(subject, [packageName, range]) => {
const catalogs = subject(`catalog:\n ${packageName}: ${range}\n`)
return Equal.equals(catalogs, { default: { [packageName]: range }, named: {} })
},
'∀s_WorkspaceCatalogSection_≡DefaultCatalog',
{ of: [DependencySpecs], subject: parseWorkspaceCatalogs },
(subject, [specs]) => Equal.equals(subject(`catalog: ${JSON.stringify(specs)}\n`), { default: specs, named: {} }),
)

it.prop(
'∀c_WorkspaceNamedCatalogSection_≡NamedCatalog',
{ of: [catalogNameArb, nameArb, rangeArb], subject: parseWorkspaceCatalogs },
(subject, [catalogName, packageName, range]) => {
const catalogs = subject(`catalogs:\n ${catalogName}:\n ${packageName}: ${range}\n`)
return Equal.equals(catalogs, { default: {}, named: { [catalogName]: { [packageName]: range } } })
},
'∀s_WorkspaceCatalogsDefaultSection_≡DefaultCatalog',
{ of: [DependencySpecs], subject: parseWorkspaceCatalogs },
(subject, [specs]) =>
Equal.equals(subject(`catalogs: ${JSON.stringify({ default: specs })}\n`), { default: specs, named: {} }),
)

it.prop(
'∀w_WorkspaceCatalogsSection_≡NamedCatalogsWithoutDefault',
{ of: [WorkspaceCatalogsYaml], subject: parseWorkspaceCatalogs },
(subject, [workspace]) =>
Equal.equals(subject(JSON.stringify(workspace)).named, Rec.remove(workspace.catalogs ?? {}, 'default')),
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, it } from '@systemfsoftware/vitest'
import * as Result from 'effect/Result'
import * as S from 'effect/Schema'

import {
ClosureMemberExtra,
ClosureMemberMissing,
findLockDrift,
FindLockDriftCommand,
LockMissing,
PinMoved,
} from '../find-lock-drift.workflow.js'

describe('findLockDrift', () => {
it.prop(
'∀c_LockMissingReported_≡NoLockCommitted',
{ of: [FindLockDriftCommand], subject: findLockDrift },
(subject, [command]) => {
const drift = Result.getOrThrow(subject(command))
const reportsMissing = drift.length === 1 && S.is(LockMissing)(drift[0])
return reportsMissing === (command.lock === null) &&
drift.every((finding) => !S.is(LockMissing)(finding) || reportsMissing)
},
)

it.prop(
'∀c_AnyLock_≡FindingsNameTheFixtureAndSideWithTheCommand',
{ of: [FindLockDriftCommand], subject: findLockDrift },
(subject, [command]) =>
Result.getOrThrow(subject(command)).every((finding) =>
finding.fixtureId === command.fixtureId &&
(!S.is(ClosureMemberMissing)(finding) || command.closure.includes(finding.packageName)) &&
(!S.is(ClosureMemberExtra)(finding) || !command.closure.includes(finding.packageName)) &&
(!S.is(PinMoved)(finding) ||
(command.pins[finding.packageName] === finding.pinned && finding.pinned !== finding.locked))
),
)
})
Loading
Loading