Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/busy-mammals-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js-vitest-runner": minor
---

Every failed test reports a reproduce argv: vitest run <file> -t <full test name>, or vitest run <file> for a file-level failure.
5 changes: 5 additions & 0 deletions .changeset/fine-pandas-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js": minor
---

Each failure record carries a one-step reproduction: the failing test's own runner command under the mutation-worker budget, stryker run --dryRunOnly when the runner gives none, stryker run --mutant <ids> for new survivors, or DoesNotReplay naming why and what stands in (worker kind, pid and exit code for crashed workers).
5 changes: 5 additions & 0 deletions .changeset/green-kings-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js": major
---

Every failed run ends in one FailureRecord: printed on stderr, emitted as the terminal stream event, and written to reports/mutation/failure.json. StageError and PrepareError are replaced by RunFailure, exit codes come from the failure catalog (a failing baseline test exits 5), failed baseline tests are named by project-relative file and line, and the cli.run span carries stryker.failure.code.
5 changes: 5 additions & 0 deletions .changeset/humble-coats-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js": minor
---

A failed run's record reaches every surface: the Mutation Server Protocol answers engine failures with the record in error.data and keeps serving, MCP rerun_mutant returns a RerunEngineUnusable refusal carrying it and the new get_failure tool reads reports/mutation/failure.json, a configured sarif reporter writes a SARIF log whose invocation is executionSuccessful false with one notification per record, and the run log no longer prints an orphan 'Possible causes' block or the bundled parser's WASI ExperimentalWarning.
5 changes: 5 additions & 0 deletions .changeset/moody-snails-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js-cli-contract": minor
---

RunFailed now carries a typed FailureRecord (catalog code, stage, structured evidence, cause chain, next action, replay capsule, trace id) instead of error/remediation/reason prose; the stream schema is 3.0. The package publishes contract/failure-catalog.json and FailureRecord renderers for terminal, markdown, GitHub annotations and SARIF.
5 changes: 5 additions & 0 deletions .changeset/petite-insects-feel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js": minor
---

A dry-run-only run with incremental mode on publishes its dry-run coverage to the incremental file, keeping any verdicts already there, so a later run that reads the file through incrementalSources skips its own dry run. The run-inputs digest no longer covers dryRunOnly, so a preflight and the runs that reuse its coverage share one digest; existing caches are refused once with runInputsChanged.
5 changes: 5 additions & 0 deletions .changeset/proud-bananas-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js": minor
---

stryker annotate also renders the GitHub annotations of reports/mutation/failure.json when a run failed.
5 changes: 5 additions & 0 deletions .changeset/quiet-clowns-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js-plugin-interface": minor
---

FailedTestResult gains an optional reproduce argv: the command that re-runs exactly that failing test.
5 changes: 5 additions & 0 deletions .changeset/sixty-wombats-write.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js-cli-contract": minor
---

FailedTestEvidence carries the runner's reproduce argv, and recordOf takes the decided capsule from its RecordContext instead of deriving it.
5 changes: 5 additions & 0 deletions .changeset/small-results-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js-plugin-interface": major
---

FailedTestResult carries an optional project-relative source location and the raw stack; the unused startPosition field is removed.
5 changes: 5 additions & 0 deletions .changeset/upset-chicken-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js-vitest-runner": minor
---

Failed tests report the first project source frame as their location and keep the raw stack; file-level failures are named and identified by their project-relative path.
5 changes: 5 additions & 0 deletions .changeset/witty-spies-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js-plugin-interface": major
---

TestRunner.HitLimitReasonText now accepts only the canonical `Hit limit reached (count/limit)` shape; text that merely starts with the prefix no longer validates.
5 changes: 5 additions & 0 deletions .changeset/young-memes-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js": patch
---

Timeout reasons that only start with `Hit limit reached` no longer classify as hit-limit timeouts; the timeout-fields property now draws every deciding input class under the mutation-worker budget.
177 changes: 169 additions & 8 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name: Mutation
on:
push:
branches: [main]
pull_request:
workflow_dispatch:

concurrency:
Expand Down Expand Up @@ -51,12 +52,96 @@ jobs:
if-no-files-found: ignore
retention-days: 1

mutation:
name: mutation · ${{ matrix.job.name }}
preflight:
name: preflight · one dry run per package
needs: plan
if: ${{ needs.plan.result == 'success' && needs.plan.outputs.jobs != '[]' }}
runs-on: ubuntu-latest
timeout-minutes: 45
outputs:
coverage: ${{ steps.preflight.outputs.coverage }}
env:
OTEL_ENABLED: "true"
OTEL_SERVICE_NAME: stryker-js-mutation
OTEL_EXPORTER_OTLP_ENDPOINT: http://127.0.0.1:4318
OUT_DIR: mutation-telemetry
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- uses: denoland/setup-deno@v2
- name: Build
run: pnpm exec turbo run build --output-logs=errors-only --log-order=stream ${{ join(fromJSON(needs.plan.outputs.jobs).*.filters, ' ') }}

- uses: actions/cache/restore@v6
with:
path: .stryker-incremental
key: stryker-incremental-${{ github.run_id }}
restore-keys: |
stryker-incremental-
- name: Place incremental files
run: if [ -d .stryker-incremental ]; then cp -a .stryker-incremental/. .; fi

- name: Install process-compose (pinned)
run: |
curl -sSfL -o /tmp/pc.tar.gz https://github.com/F1bonacc1/process-compose/releases/download/v1.122.0/process-compose_linux_amd64.tar.gz
echo '9b6dbc38324c0b0481f1cd1dd828ffdc78117129ec797678f4bf8c4023311281 /tmp/pc.tar.gz' | sha256sum -c -
sudo tar xzf /tmp/pc.tar.gz -C /usr/local/bin process-compose
- name: Start LGTM (blocks until probe-ready)
run: |
echo "TRACE_WINDOW_START=$(date +%s)" >> "$GITHUB_ENV"
pnpm lgtm:up

- id: preflight
name: Preflight
env:
JOBS: ${{ needs.plan.outputs.jobs }}
run: $MUTATION_JOB preflight --cap-seconds "$TARGET_SECONDS"

- uses: actions/upload-artifact@v7
with:
name: mutation-preflight
path: preflight/
if-no-files-found: ignore
retention-days: 1
- uses: actions/upload-artifact@v7
if: failure()
with:
name: mutation-preflight-evidence
path: preflight-evidence/
if-no-files-found: ignore
retention-days: 30

- name: Export telemetry
if: always()
continue-on-error: true
run: ./test/e2e/scripts/export-traces.ts
- name: Package telemetry
if: always()
run: if [ -d mutation-telemetry ]; then tar czf mutation-telemetry.tar.gz mutation-telemetry; fi
- uses: actions/upload-artifact@v7
if: always()
with:
name: mutation-telemetry-${{ github.run_id }}-preflight
path: mutation-telemetry.tar.gz
if-no-files-found: ignore
retention-days: 30

mutation:
name: mutation · ${{ matrix.job.name }}
needs: [plan, preflight]
if: ${{ needs.preflight.result == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 75
env:
OTEL_ENABLED: "true"
OTEL_SERVICE_NAME: stryker-js-mutation
OTEL_EXPORTER_OTLP_ENDPOINT: http://127.0.0.1:4318
OUT_DIR: mutation-telemetry
strategy:
fail-fast: false
matrix:
Expand All @@ -81,6 +166,21 @@ jobs:
stryker-incremental-
- name: Place incremental files
run: if [ -d .stryker-incremental ]; then cp -a .stryker-incremental/. .; fi
- uses: actions/download-artifact@v8
if: ${{ needs.preflight.outputs.coverage == 'true' }}
with:
name: mutation-preflight
path: .

- name: Install process-compose (pinned)
run: |
curl -sSfL -o /tmp/pc.tar.gz https://github.com/F1bonacc1/process-compose/releases/download/v1.122.0/process-compose_linux_amd64.tar.gz
echo '9b6dbc38324c0b0481f1cd1dd828ffdc78117129ec797678f4bf8c4023311281 /tmp/pc.tar.gz' | sha256sum -c -
sudo tar xzf /tmp/pc.tar.gz -C /usr/local/bin process-compose
- name: Start LGTM (blocks until probe-ready)
run: |
echo "TRACE_WINDOW_START=$(date +%s)" >> "$GITHUB_ENV"
pnpm lgtm:up

- name: Mutation
env:
Expand All @@ -107,12 +207,27 @@ jobs:
name: mutation-part-${{ matrix.job.id }}
path: mutation-parts/
if-no-files-found: ignore
retention-days: 1
retention-days: 30

- name: Export telemetry
if: always()
continue-on-error: true
run: ./test/e2e/scripts/export-traces.ts
- name: Package telemetry
if: always()
run: if [ -d mutation-telemetry ]; then tar czf mutation-telemetry.tar.gz mutation-telemetry; fi
- uses: actions/upload-artifact@v7
if: always()
with:
name: mutation-telemetry-${{ github.run_id }}-${{ matrix.job.id }}
path: mutation-telemetry.tar.gz
if-no-files-found: ignore
retention-days: 30

timings:
name: record mutation timings
needs: [plan, mutation]
if: ${{ always() && needs.plan.result == 'success' }}
if: ${{ always() && needs.plan.result == 'success' && github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
Expand All @@ -138,13 +253,14 @@ jobs:

report:
name: Mutation report
needs: [plan, mutation]
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.jobs != '[]' }}
needs: [plan, preflight, mutation]
if: ${{ !cancelled() && needs.preflight.result == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 25
permissions:
contents: read
actions: write
security-events: write
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
Expand All @@ -168,7 +284,7 @@ jobs:
path: .stryker-incremental
merge-multiple: true
- uses: actions/cache/save@v6
if: ${{ hashFiles('.stryker-incremental/**') != '' }}
if: ${{ github.event_name != 'pull_request' && hashFiles('.stryker-incremental/**') != '' }}
with:
path: .stryker-incremental
key: stryker-incremental-${{ github.run_id }}
Expand All @@ -194,8 +310,53 @@ jobs:
path: mutation-merged/*
retention-days: 14

- name: Delete per-job artifacts
- name: Merge SARIF
if: always()
run: $MUTATION_JOB sarif --parts mutation-parts --out mutation-sarif/mutation.sarif
- uses: github/codeql-action/upload-sarif@v4
if: ${{ always() && hashFiles('mutation-sarif/mutation.sarif') != '' }}
with:
sarif_file: mutation-sarif/mutation.sarif
category: mutation

- name: Survivor baseline
if: ${{ env.MUTATION_REPORT_COMPLETE == 'true' && github.ref == 'refs/heads/main' }}
run: |
mkdir -p mutation-gate/reports/mutation
cp mutation-merged/mutation-report.json mutation-gate/reports/mutation/mutation.json
cd mutation-gate
"$GITHUB_WORKSPACE/packages/stryker-js/node_modules/.bin/stryker" gate --baseline mutation-survivor-baseline.json --update-baseline
- uses: actions/upload-artifact@v7
if: ${{ env.MUTATION_REPORT_COMPLETE == 'true' && github.ref == 'refs/heads/main' }}
with:
name: mutation-survivor-baseline
path: mutation-gate/mutation-survivor-baseline.json
retention-days: 90

- name: Survivor baseline from main
if: ${{ github.event_name == 'pull_request' }}
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p mutation-gate/reports/mutation
id=$(gh api "repos/$GITHUB_REPOSITORY/actions/artifacts?name=mutation-survivor-baseline&per_page=100" \
--jq '[.artifacts[] | select(.expired | not) | select(.workflow_run.head_branch == "main")][0].id // empty') || id=
if [ -n "$id" ] && gh api "repos/$GITHUB_REPOSITORY/actions/artifacts/$id/zip" > mutation-gate/baseline.zip; then
unzip -o -q mutation-gate/baseline.zip -d mutation-gate
fi
- name: Gate new survivors
if: ${{ github.event_name == 'pull_request' }}
run: |
if [ ! -f mutation-gate/mutation-survivor-baseline.json ]; then
echo "::warning title=Mutation gate skipped::main has published no survivor baseline yet, so this run cannot tell a new survivor from an accepted one"
exit 0
fi
cp mutation-merged/mutation-report.json mutation-gate/reports/mutation/mutation.json
cd mutation-gate
"$GITHUB_WORKSPACE/packages/stryker-js/node_modules/.bin/stryker" gate --baseline mutation-survivor-baseline.json

- name: Delete per-job artifacts
if: ${{ always() && needs.mutation.result == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
Expand Down
Loading
Loading