testguard-cli 0.1.1__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/scripts/install-smoke.mjs +1 -1
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/workflows/supply-chain.yml +2 -2
- testguard_cli-0.1.3/CHANGELOG.md +148 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/PKG-INFO +48 -9
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/README.md +47 -8
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/action.yml +1 -1
- testguard_cli-0.1.3/cli/testguard.mjs +14 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/README.md +1 -0
- testguard_cli-0.1.3/fixtures/known-answer/expected.json +53 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/testguard.claims.json +155 -34
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/package-lock.json +2 -2
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/package.json +1 -1
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/packaging/homebrew/testguard.rb +2 -2
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/pyproject.toml +1 -1
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/GATE-SEMANTICS.md +12 -6
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/examples/baseline.json +2 -1
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/examples/evidence.json +9 -3
- testguard_cli-0.1.3/spec/conformance/invalid/evidence.undeclared-killers-without-reason.json +537 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/lib/validate.mjs +3 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/baseline.schema.json +4 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/claims.schema.json +1 -1
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/common.schema.json +1 -1
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/evidence.schema.json +26 -2
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/baseline/baseline.mjs +1 -1
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/brief/brief.mjs +1 -1
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/cli.mjs +11 -1
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/commands/baseline.mjs +4 -2
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/commands/claims.mjs +7 -4
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/commands/probe.mjs +19 -5
- testguard_cli-0.1.3/src/git.mjs +58 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/probe/classify.mjs +2 -1
- testguard_cli-0.1.3/src/probe/discover.mjs +12 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/probe/probe.mjs +87 -19
- testguard_cli-0.1.3/src/probe/rank.mjs +144 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/probe/runner-vitest.mjs +61 -2
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/probe/worktree.mjs +28 -11
- testguard_cli-0.1.3/src/render.mjs +39 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/brief.test.mjs +22 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/classify.test.mjs +5 -0
- testguard_cli-0.1.3/test/discover.test.mjs +22 -0
- testguard_cli-0.1.3/test/probe-preconditions.test.mjs +37 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/probe.fixture.test.mjs +91 -1
- testguard_cli-0.1.3/test/rank-aliases.test.mjs +35 -0
- testguard_cli-0.1.3/test/runner-command.test.mjs +34 -0
- testguard_cli-0.1.3/test/snapshot.test.mjs +38 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/worktree.test.mjs +28 -1
- testguard_cli-0.1.3/testguard.claims.json +316 -0
- testguard_cli-0.1.1/CHANGELOG.md +0 -57
- testguard_cli-0.1.1/cli/testguard.mjs +0 -6
- testguard_cli-0.1.1/fixtures/known-answer/expected.json +0 -15
- testguard_cli-0.1.1/src/git.mjs +0 -32
- testguard_cli-0.1.1/src/probe/rank.mjs +0 -51
- testguard_cli-0.1.1/src/render.mjs +0 -28
- testguard_cli-0.1.1/testguard.claims.json +0 -120
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.gitattributes +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/dependabot.yml +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/scripts/sync-release-version.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/workflows/auto-merge.yml +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/workflows/ci.yml +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/workflows/release.yml +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/workflows/scheduled-release.yml +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.gitignore +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.npmignore +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.npmrc +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.pre-commit-hooks.yaml +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/AGENTS.md +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/CLAUDE.md +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/CODE_OF_CONDUCT.md +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/CONTRIBUTING.md +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/LICENSE +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/PRIVACY.md +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/SECURITY.md +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/SUPPORT.md +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/bench/README.md +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/.gitignore +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/package.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/src/export.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/src/redact.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/test/flaky.test.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/test/redact.test.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/vitest.config.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/README.md +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/examples/brief.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/examples/calibration.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/examples/claims.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/examples/ignore.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/baseline.bad-fingerprint-key.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/baseline.zero-count.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/brief.text-without-heading.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/brief.unknown-verdict-key.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/calibration.p-outside-ci.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/calibration.positives-exceed-n.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/claims.absolute-path.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/claims.duplicate-fault-id.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/claims.missing-provenance.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/claims.noop-fault.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/claims.occurrence-exceeds-hits.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/claims.parent-traversal.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.flaky-defender-without-flakiness.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.killed-by-non-assertion.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.killed-on-red-baseline.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.killed-without-n-runs.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.rank-extra-property.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.unknown-verdict.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.unverifiable-without-reason.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.wrong-fingerprint.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/ignore.missing-reason.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/ignore.short-reason.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/schemas.test.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/lib/fingerprint.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/brief.schema.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/calibration.schema.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/ignore.schema.json +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/claims/annotations.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/claims/load.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/commands/brief.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/evidence/writer.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/probe/inject.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/util/glob.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/util/hash.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/annotations.test.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/baseline.test.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/glob.test.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/inject.test.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/runner-vitest.test.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/writer.test.mjs +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/testguard_cli/__init__.py +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/testguard_cli/wrapper.py +0 -0
- {testguard_cli-0.1.1 → testguard_cli-0.1.3}/vitest.config.mjs +0 -0
|
@@ -31,7 +31,7 @@ try {
|
|
|
31
31
|
const version = run(bin, ['--version'], consumer).trim();
|
|
32
32
|
const claims = run(bin, ['claims', '.'], consumer);
|
|
33
33
|
if (!/^\d+\.\d+\.\d+/.test(version)) throw new Error(`unexpected --version output: ${version}`);
|
|
34
|
-
if (
|
|
34
|
+
if (!/^\d+ claims in /.test(claims)) throw new Error(`claims did not list the fixture:\n${claims}`);
|
|
35
35
|
const deps = Object.keys(JSON.parse(run(npm, ['ls', '--omit=dev', '--json', '--depth=0'], consumer)).dependencies ?? {});
|
|
36
36
|
console.log(`install smoke OK — testguard ${version} runs from the installed tarball; consumer deps: ${deps.join(', ')}`);
|
|
37
37
|
} finally {
|
|
@@ -17,8 +17,8 @@ permissions:
|
|
|
17
17
|
jobs:
|
|
18
18
|
osv-scan-pr:
|
|
19
19
|
if: github.event_name == 'pull_request'
|
|
20
|
-
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@
|
|
20
|
+
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@a345acffa64b0eaede81a3d9aae6141214d9c8fc" # v2.6.0
|
|
21
21
|
|
|
22
22
|
osv-scan-scheduled:
|
|
23
23
|
if: github.event_name != 'pull_request'
|
|
24
|
-
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@
|
|
24
|
+
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@a345acffa64b0eaede81a3d9aae6141214d9c8fc" # v2.6.0
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.3] - 2026-09-17
|
|
11
|
+
|
|
12
|
+
From a second field report on a real codebase (456 tests, 27 claims, 35
|
|
13
|
+
faults; 13 survived on the first run, two critical claims with zero coverage).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Worktree mode probed HEAD while reading the claims file from the working
|
|
18
|
+
tree**, so uncommitted defender changes were silently ignored — the same
|
|
19
|
+
survivors came back with no hint why. `probe` now refuses (exit 2) when any
|
|
20
|
+
resolved defender or fault target has uncommitted changes, naming the files
|
|
21
|
+
and the commit it would have probed. Every summary names the commit probed.
|
|
22
|
+
- `killed-by-undeclared-tests` never said which tests killed the fault; the
|
|
23
|
+
author could not fix `defendedBy` without grepping the suite. Evidence now
|
|
24
|
+
carries `detail.undeclaredKillers` and the CLI names the files.
|
|
25
|
+
- `anchor-ambiguous` did not say how many hits; `detail.anchor { hits,
|
|
26
|
+
expected }` is recorded and printed.
|
|
27
|
+
- A replacement with an unbalanced paren was `suite-failed-to-load`, not
|
|
28
|
+
`replacement-does-not-compile`: esbuild/vitest wording is now matched.
|
|
29
|
+
- The claims schema promised defender discovery for an absent `defendedBy`;
|
|
30
|
+
the tool answered `nocover`. Discovery is implemented: the test files that
|
|
31
|
+
import the fault's target (relative or alias), recorded as
|
|
32
|
+
`defenders.discovered`. `nocover` now means exactly "no test file imports
|
|
33
|
+
this source".
|
|
34
|
+
- `baseline.json` records `dirty`, as evidence already did.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- `--include-dirty`: snapshot the working tree (tracked edits and untracked,
|
|
39
|
+
non-ignored files) into a throwaway commit and probe that. HEAD, index and
|
|
40
|
+
the user's tree are never touched; `run.repo.snapshot` records the commit.
|
|
41
|
+
- A one-line progress indicator on stderr (TTY only) so a minute of silence
|
|
42
|
+
is not mistaken for a hang.
|
|
43
|
+
- Fixture: a claim with no `defendedBy` whose defender is discovered.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- **Default output shows only unproven faults plus a killed count.**
|
|
48
|
+
`--verbose` restores the full stream.
|
|
49
|
+
- The `survived` hint reminds the author to check that the fault is
|
|
50
|
+
observable at all before writing a test for it.
|
|
51
|
+
- README: `$schema` path for consumers, `min-release-age` note, the
|
|
52
|
+
worktree-vs-working-tree rule, `brief` writes `brief.json` by default.
|
|
53
|
+
|
|
54
|
+
## [0.1.2] - 2026-09-17
|
|
55
|
+
|
|
56
|
+
From a field report on a real codebase (63 test files, 458 tests, 39 faults).
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **A runner that cannot be resolved was reported as `FLAKY-DEFENDER`.** In
|
|
61
|
+
the scratch worktree, a symlinked `node_modules` (the sibling/auto-worktree
|
|
62
|
+
layout) was invisible, vitest failed to load, and the load error was
|
|
63
|
+
classified as flaky tests — blaming the wrong party. Symlinked
|
|
64
|
+
`node_modules` are now linked to their resolved target; the runner is
|
|
65
|
+
checked before any verdict and an unresolvable one is a precondition
|
|
66
|
+
failure (exit 2) with the fix in the message; defenders that fail to
|
|
67
|
+
*load* during a baseline are `UNVERIFIABLE` (`defenders-failed-to-load`),
|
|
68
|
+
never flaky.
|
|
69
|
+
- **Blast radius ignored path aliases.** `tsconfig`/`jsconfig` `paths` (with
|
|
70
|
+
`baseUrl` and relative `extends`) and `package.json#imports` are resolved,
|
|
71
|
+
so a module imported 83 times via `@/…` no longer ranks as if nothing
|
|
72
|
+
imported it. Bare package specifiers remain ignored, as documented.
|
|
73
|
+
- Summary said "N unproven claims" when N counted faults; it now reports
|
|
74
|
+
unproven faults *and* the distinct claims they belong to.
|
|
75
|
+
- `testguard claims` never showed which claims carry a `@claim` annotation;
|
|
76
|
+
it now reports the count and marks each annotated row.
|
|
77
|
+
- Piping output to a closed reader (`testguard claims | head`) no longer
|
|
78
|
+
crashes with an `EPIPE` stack trace.
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
|
|
82
|
+
- `--claim <ID,ID>` probes only the named claims and writes
|
|
83
|
+
`.testguard/evidence-partial.json`, keeping the canonical evidence intact —
|
|
84
|
+
turns a fix-loop iteration from minutes into seconds.
|
|
85
|
+
- `--runner-cmd "<cmd>"` with `{files}` and `{out}` placeholders for
|
|
86
|
+
monorepos, custom configs and other package managers.
|
|
87
|
+
- `--node-modules <dir>` (or `TESTGUARD_NODE_MODULES`) to link a specific
|
|
88
|
+
`node_modules` into the scratch worktree.
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- The `--in-place` precondition message says what it means: only fault
|
|
93
|
+
target files must be clean; test files may be dirty.
|
|
94
|
+
- `testguard baseline` prints the two `.gitignore` lines for the regenerated
|
|
95
|
+
files instead of leaving it to the README.
|
|
96
|
+
- With no baseline, the ranked block is not printed a second time under the
|
|
97
|
+
per-fault stream.
|
|
98
|
+
- `--quiet` is documented precisely.
|
|
99
|
+
- Staged Homebrew formula carries the sha256 of the published 0.1.1 tarball.
|
|
100
|
+
|
|
101
|
+
## [0.1.1] - 2026-09-17
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
|
|
105
|
+
- **0.1.0 did not run when installed.** `ajv`, which validates every document
|
|
106
|
+
against the spec, was declared as a devDependency, so `testguard` crashed on
|
|
107
|
+
startup from npm, `npx`, `pip` and Homebrew. It is now an exact-pinned
|
|
108
|
+
runtime dependency — the project's one dependency.
|
|
109
|
+
- CI and the release job now pack the tarball, install it into a scratch
|
|
110
|
+
project with production dependencies only, and run the CLI from there
|
|
111
|
+
(`npm run test:install`). The suite alone runs from the checkout and could
|
|
112
|
+
not see this class of defect.
|
|
113
|
+
|
|
114
|
+
### Changed
|
|
115
|
+
|
|
116
|
+
- Homebrew formula carries the sha256 of the published tarball; description
|
|
117
|
+
shortened to satisfy `brew audit --strict`.
|
|
118
|
+
|
|
119
|
+
## [0.1.0] - 2026-09-17
|
|
120
|
+
|
|
121
|
+
First release. A claim verifier, not a test generator.
|
|
122
|
+
|
|
123
|
+
### Added
|
|
124
|
+
|
|
125
|
+
- **Contract spine** (`spec/`): six shared JSON Schemas — claims, evidence,
|
|
126
|
+
baseline, ignore, calibration, brief — identified as `urn:guard-spec:v1:*`,
|
|
127
|
+
with a validator that enforces the semantic rules a schema cannot express,
|
|
128
|
+
a single fingerprint derivation, `GATE-SEMANTICS.md`, and a conformance
|
|
129
|
+
corpus (one valid document per kind, 22 must-reject documents).
|
|
130
|
+
- **`testguard claims`** — validates the claims file and reports drift
|
|
131
|
+
against `@claim <ID>` annotations in source.
|
|
132
|
+
- **`testguard probe`** — applies each fault in a scratch git worktree,
|
|
133
|
+
confirms the defenders green N times, runs them N times with the fault,
|
|
134
|
+
escalates survivors to the whole suite with N-run attribution, restores,
|
|
135
|
+
classifies into a closed verdict set (`killed`, `survived`, `nocover`,
|
|
136
|
+
`unverifiable`, `timeout`, `fault-invalid`, `flaky-defender`), ranks, and
|
|
137
|
+
writes evidence validated against the spec. `--ref` pins the probed commit.
|
|
138
|
+
Verdicts are reused when the target, defenders and N are unchanged.
|
|
139
|
+
- **`testguard baseline`** — freezes every non-passing fingerprint so only
|
|
140
|
+
new findings gate; severity floor via `--severity`.
|
|
141
|
+
- **`testguard brief`** — a ranked, capped `## TEST BLINDSPOT CONTEXT` block
|
|
142
|
+
for an agent's session start; `--text` is hook-safe.
|
|
143
|
+
- **Known-answer fixture** with a genuine `objectContaining` blind spot and a
|
|
144
|
+
case for every verdict; probed end to end in the test suite and in CI.
|
|
145
|
+
- **Self-verification**: `testguard.claims.json` states invariants of the
|
|
146
|
+
tool itself, probed by the tool in CI.
|
|
147
|
+
- Distribution: npm (`testguard-cli`), PyPI wrapper (`testguard-cli`),
|
|
148
|
+
GitHub Action, Homebrew formula (staged), pre-commit hooks.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: testguard-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Proves a test suite actually defends the claims a project makes: injects the faults those claims forbid and reports every one the tests miss. Python wrapper for the Node.js CLI (requires Node.js 20+).
|
|
5
5
|
Project-URL: Homepage, https://github.com/raccioly/testguard
|
|
6
6
|
Project-URL: Documentation, https://github.com/raccioly/testguard#readme
|
|
@@ -59,6 +59,15 @@ on known-broken code. The largest gap was a compliance-critical path with
|
|
|
59
59
|
100% coverage, where the one assertion that mattered used
|
|
60
60
|
`expect.objectContaining({...})` and omitted the field carrying the data.
|
|
61
61
|
|
|
62
|
+
A second, independent run on a different AI-authored codebase (63 test
|
|
63
|
+
files, 458 tests, 24 hand-written security claims, 39 faults): **21 of 39
|
|
64
|
+
faults survived a fully green suite — 9 of them critical.** Super-admin
|
|
65
|
+
gating, membership checks, cookie flags and the whole authorization callback
|
|
66
|
+
could be disabled without a single test noticing. One test file had
|
|
67
|
+
re-implemented the authorization logic *inside the test* and asserted
|
|
68
|
+
against the copy: fifteen green tests, zero detection. After wrapper-level
|
|
69
|
+
tests were written against the survivors, 39/39 were killed.
|
|
70
|
+
|
|
62
71
|
## Install
|
|
63
72
|
|
|
64
73
|
| How | Command |
|
|
@@ -67,9 +76,13 @@ on known-broken code. The largest gap was a compliance-critical path with
|
|
|
67
76
|
| npm | `npm i -D testguard-cli` then `npx testguard probe` |
|
|
68
77
|
| pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
|
|
69
78
|
| Homebrew | `brew tap raccioly/tap && brew install testguard` |
|
|
70
|
-
| GitHub Action | `uses: raccioly/testguard@v0.1.
|
|
79
|
+
| GitHub Action | `uses: raccioly/testguard@v0.1.3` — see [`action.yml`](./action.yml) |
|
|
71
80
|
| pre-commit | `repo: https://github.com/raccioly/testguard`, hooks `testguard-claims`, `testguard-probe` |
|
|
72
81
|
|
|
82
|
+
Projects that set `min-release-age` in `.npmrc` cannot see a version published
|
|
83
|
+
less than that many days ago (`ENOVERSIONS`); install that one with
|
|
84
|
+
`npm i -D testguard-cli --min-release-age=0`.
|
|
85
|
+
|
|
73
86
|
## How it works
|
|
74
87
|
|
|
75
88
|
```bash
|
|
@@ -79,12 +92,14 @@ npx testguard-cli baseline # freeze today's unproven findings; from now on on
|
|
|
79
92
|
npx testguard-cli brief # tell the agent where the suite is blind, before it writes
|
|
80
93
|
```
|
|
81
94
|
|
|
82
|
-
1. **Claims** live in `testguard.claims.json
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
1. **Claims** live in `testguard.claims.json` (editors validate it against
|
|
96
|
+
`"$schema": "./node_modules/testguard-cli/spec/schemas/claims.schema.json"`):
|
|
97
|
+
a statement, where it comes from, which tests supposedly defend it, and
|
|
98
|
+
one or more *faults* — each a deterministic source change that would make
|
|
99
|
+
the statement false. Every
|
|
85
100
|
claim and every fault records who produced it. `testguard claims`
|
|
86
101
|
validates the file and reports drift against `@claim <ID>` annotations in
|
|
87
|
-
source.
|
|
102
|
+
source. Test files are deliberately not scanned — a claim asserted by a test is the authorship trap the tool exists for — and annotation ids must contain a hyphen so prose is never mistaken for one.
|
|
88
103
|
2. **Probe** confirms the defenders are green N times unmodified, applies
|
|
89
104
|
each fault in a scratch git worktree (your tree is never touched), runs
|
|
90
105
|
the defenders N times, re-runs survivors against the whole suite with
|
|
@@ -101,14 +116,38 @@ npx testguard-cli brief # tell the agent where the suite is blind, before
|
|
|
101
116
|
| `FLAKY-DEFENDER` | the defenders are not reliably green, or disagreed across runs |
|
|
102
117
|
|
|
103
118
|
Never a single score. Findings are ranked by severity, claim provenance
|
|
104
|
-
and blast radius
|
|
105
|
-
|
|
119
|
+
and blast radius (relative imports, `tsconfig` path aliases and
|
|
120
|
+
`package.json#imports` are resolved; bare package names are not), and
|
|
121
|
+
written to `.testguard/evidence.json` — validated against the spec before
|
|
122
|
+
it is written.
|
|
123
|
+
|
|
124
|
+
Worktree mode probes a **commit**. If a defender or target file has
|
|
125
|
+
uncommitted changes, `probe` refuses and says so — otherwise your new
|
|
126
|
+
tests would be silently absent and the same survivors would come back
|
|
127
|
+
with no hint why. `--include-dirty` snapshots the working tree (tracked
|
|
128
|
+
edits and new files) into a throwaway commit and probes that; your tree,
|
|
129
|
+
HEAD and index are never touched. Every summary names the commit probed.
|
|
130
|
+
|
|
131
|
+
A claim with no `defendedBy` has its defenders **discovered**: the test
|
|
132
|
+
files that import the fault's target, by relative path or resolved alias.
|
|
133
|
+
`NOCOVER` then means exactly "no test file imports this source".
|
|
134
|
+
|
|
135
|
+
Practical loop: first pass `--no-escalate` (escalation re-runs the whole
|
|
136
|
+
suite N times per survivor); iterate on one claim with `--claim <ID>` and
|
|
137
|
+
either `--include-dirty` or `--in-place` (only fault target files must be
|
|
138
|
+
clean there; test files may be dirty); final pass with defaults. By default
|
|
139
|
+
the stream shows only unproven faults plus a killed count — `--verbose`
|
|
140
|
+
shows every fault. A custom
|
|
141
|
+
runner (`pnpm --filter`, a specific config) goes in
|
|
142
|
+
`--runner-cmd "<cmd> {files} … {out}"`; if the scratch worktree cannot
|
|
143
|
+
see your `node_modules`, pass `--node-modules <dir>`.
|
|
106
144
|
3. **Baseline** freezes every non-passing fingerprint. Later probes suppress
|
|
107
145
|
what was already known and exit non-zero only on what is new. Claims whose
|
|
108
146
|
source and defenders are unchanged reuse their prior verdict, so a probe
|
|
109
147
|
in CI costs only what changed.
|
|
110
148
|
4. **Brief** turns evidence plus baseline into a ranked, capped
|
|
111
|
-
`## TEST BLINDSPOT CONTEXT` block
|
|
149
|
+
`## TEST BLINDSPOT CONTEXT` block, printed and also written to
|
|
150
|
+
`.testguard/brief.json` (`--text` prints only). Wire it into an agent's session start
|
|
112
151
|
— for Claude Code, in `.claude/settings.json`:
|
|
113
152
|
|
|
114
153
|
```json
|
|
@@ -35,6 +35,15 @@ on known-broken code. The largest gap was a compliance-critical path with
|
|
|
35
35
|
100% coverage, where the one assertion that mattered used
|
|
36
36
|
`expect.objectContaining({...})` and omitted the field carrying the data.
|
|
37
37
|
|
|
38
|
+
A second, independent run on a different AI-authored codebase (63 test
|
|
39
|
+
files, 458 tests, 24 hand-written security claims, 39 faults): **21 of 39
|
|
40
|
+
faults survived a fully green suite — 9 of them critical.** Super-admin
|
|
41
|
+
gating, membership checks, cookie flags and the whole authorization callback
|
|
42
|
+
could be disabled without a single test noticing. One test file had
|
|
43
|
+
re-implemented the authorization logic *inside the test* and asserted
|
|
44
|
+
against the copy: fifteen green tests, zero detection. After wrapper-level
|
|
45
|
+
tests were written against the survivors, 39/39 were killed.
|
|
46
|
+
|
|
38
47
|
## Install
|
|
39
48
|
|
|
40
49
|
| How | Command |
|
|
@@ -43,9 +52,13 @@ on known-broken code. The largest gap was a compliance-critical path with
|
|
|
43
52
|
| npm | `npm i -D testguard-cli` then `npx testguard probe` |
|
|
44
53
|
| pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
|
|
45
54
|
| Homebrew | `brew tap raccioly/tap && brew install testguard` |
|
|
46
|
-
| GitHub Action | `uses: raccioly/testguard@v0.1.
|
|
55
|
+
| GitHub Action | `uses: raccioly/testguard@v0.1.3` — see [`action.yml`](./action.yml) |
|
|
47
56
|
| pre-commit | `repo: https://github.com/raccioly/testguard`, hooks `testguard-claims`, `testguard-probe` |
|
|
48
57
|
|
|
58
|
+
Projects that set `min-release-age` in `.npmrc` cannot see a version published
|
|
59
|
+
less than that many days ago (`ENOVERSIONS`); install that one with
|
|
60
|
+
`npm i -D testguard-cli --min-release-age=0`.
|
|
61
|
+
|
|
49
62
|
## How it works
|
|
50
63
|
|
|
51
64
|
```bash
|
|
@@ -55,12 +68,14 @@ npx testguard-cli baseline # freeze today's unproven findings; from now on on
|
|
|
55
68
|
npx testguard-cli brief # tell the agent where the suite is blind, before it writes
|
|
56
69
|
```
|
|
57
70
|
|
|
58
|
-
1. **Claims** live in `testguard.claims.json
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
1. **Claims** live in `testguard.claims.json` (editors validate it against
|
|
72
|
+
`"$schema": "./node_modules/testguard-cli/spec/schemas/claims.schema.json"`):
|
|
73
|
+
a statement, where it comes from, which tests supposedly defend it, and
|
|
74
|
+
one or more *faults* — each a deterministic source change that would make
|
|
75
|
+
the statement false. Every
|
|
61
76
|
claim and every fault records who produced it. `testguard claims`
|
|
62
77
|
validates the file and reports drift against `@claim <ID>` annotations in
|
|
63
|
-
source.
|
|
78
|
+
source. Test files are deliberately not scanned — a claim asserted by a test is the authorship trap the tool exists for — and annotation ids must contain a hyphen so prose is never mistaken for one.
|
|
64
79
|
2. **Probe** confirms the defenders are green N times unmodified, applies
|
|
65
80
|
each fault in a scratch git worktree (your tree is never touched), runs
|
|
66
81
|
the defenders N times, re-runs survivors against the whole suite with
|
|
@@ -77,14 +92,38 @@ npx testguard-cli brief # tell the agent where the suite is blind, before
|
|
|
77
92
|
| `FLAKY-DEFENDER` | the defenders are not reliably green, or disagreed across runs |
|
|
78
93
|
|
|
79
94
|
Never a single score. Findings are ranked by severity, claim provenance
|
|
80
|
-
and blast radius
|
|
81
|
-
|
|
95
|
+
and blast radius (relative imports, `tsconfig` path aliases and
|
|
96
|
+
`package.json#imports` are resolved; bare package names are not), and
|
|
97
|
+
written to `.testguard/evidence.json` — validated against the spec before
|
|
98
|
+
it is written.
|
|
99
|
+
|
|
100
|
+
Worktree mode probes a **commit**. If a defender or target file has
|
|
101
|
+
uncommitted changes, `probe` refuses and says so — otherwise your new
|
|
102
|
+
tests would be silently absent and the same survivors would come back
|
|
103
|
+
with no hint why. `--include-dirty` snapshots the working tree (tracked
|
|
104
|
+
edits and new files) into a throwaway commit and probes that; your tree,
|
|
105
|
+
HEAD and index are never touched. Every summary names the commit probed.
|
|
106
|
+
|
|
107
|
+
A claim with no `defendedBy` has its defenders **discovered**: the test
|
|
108
|
+
files that import the fault's target, by relative path or resolved alias.
|
|
109
|
+
`NOCOVER` then means exactly "no test file imports this source".
|
|
110
|
+
|
|
111
|
+
Practical loop: first pass `--no-escalate` (escalation re-runs the whole
|
|
112
|
+
suite N times per survivor); iterate on one claim with `--claim <ID>` and
|
|
113
|
+
either `--include-dirty` or `--in-place` (only fault target files must be
|
|
114
|
+
clean there; test files may be dirty); final pass with defaults. By default
|
|
115
|
+
the stream shows only unproven faults plus a killed count — `--verbose`
|
|
116
|
+
shows every fault. A custom
|
|
117
|
+
runner (`pnpm --filter`, a specific config) goes in
|
|
118
|
+
`--runner-cmd "<cmd> {files} … {out}"`; if the scratch worktree cannot
|
|
119
|
+
see your `node_modules`, pass `--node-modules <dir>`.
|
|
82
120
|
3. **Baseline** freezes every non-passing fingerprint. Later probes suppress
|
|
83
121
|
what was already known and exit non-zero only on what is new. Claims whose
|
|
84
122
|
source and defenders are unchanged reuse their prior verdict, so a probe
|
|
85
123
|
in CI costs only what changed.
|
|
86
124
|
4. **Brief** turns evidence plus baseline into a ranked, capped
|
|
87
|
-
`## TEST BLINDSPOT CONTEXT` block
|
|
125
|
+
`## TEST BLINDSPOT CONTEXT` block, printed and also written to
|
|
126
|
+
`.testguard/brief.json` (`--text` prints only). Wire it into an agent's session start
|
|
88
127
|
— for Claude Code, in `.claude/settings.json`:
|
|
89
128
|
|
|
90
129
|
```json
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from '../src/cli.mjs';
|
|
3
|
+
|
|
4
|
+
// `testguard … | head` closes stdout early; that is not an error worth a stack trace.
|
|
5
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
6
|
+
stream.on('error', (err) => {
|
|
7
|
+
if (err.code === 'EPIPE') process.exit(0);
|
|
8
|
+
throw err;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
main(process.argv.slice(2)).then((code) => {
|
|
13
|
+
process.exitCode = code;
|
|
14
|
+
});
|
|
@@ -24,6 +24,7 @@ evidence comes from running against a real codebase with real history.
|
|
|
24
24
|
| REDACT-006 | `mask()` replaces with equal-length asterisks | F1 syntax error | fault-invalid |
|
|
25
25
|
| EXPORT-001 | Exported rows never include `content` | F1 keeps content | nocover |
|
|
26
26
|
| FLAKY-001 | (defender is flaky) | F1 anything | flaky-defender |
|
|
27
|
+
| DISCOVER-001 | Every match is replaced (no `defendedBy`) | F1 loop body removed | killed — defenders **discovered** by import |
|
|
27
28
|
|
|
28
29
|
## The exhibit — REDACT-001/F1
|
|
29
30
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"confirmRuns": 3,
|
|
3
|
+
"expected": {
|
|
4
|
+
"REDACT-001/F1": {
|
|
5
|
+
"verdict": "survived",
|
|
6
|
+
"why": "The audit-row test asserts with objectContaining and omits `content`; a row carrying the raw input passes."
|
|
7
|
+
},
|
|
8
|
+
"REDACT-001/F2": {
|
|
9
|
+
"verdict": "killed",
|
|
10
|
+
"why": "The masked-output test asserts the secret is absent."
|
|
11
|
+
},
|
|
12
|
+
"REDACT-002/F1": {
|
|
13
|
+
"verdict": "killed",
|
|
14
|
+
"why": "The invalid-rule test asserts compileRules does not throw."
|
|
15
|
+
},
|
|
16
|
+
"REDACT-003/F1": {
|
|
17
|
+
"verdict": "survived",
|
|
18
|
+
"why": "No test exercises a missing scope; the fail-closed guard is undefended."
|
|
19
|
+
},
|
|
20
|
+
"REDACT-004/F1": {
|
|
21
|
+
"verdict": "timeout",
|
|
22
|
+
"why": "Tests awaiting redact() hit the 1s test timeout; a timeout is not an assertion failure and is not a kill."
|
|
23
|
+
},
|
|
24
|
+
"REDACT-005/F1": {
|
|
25
|
+
"verdict": "unverifiable",
|
|
26
|
+
"reason": "anchor-missing",
|
|
27
|
+
"why": "The find string is not present in the source."
|
|
28
|
+
},
|
|
29
|
+
"REDACT-005/F2": {
|
|
30
|
+
"verdict": "unverifiable",
|
|
31
|
+
"reason": "anchor-ambiguous",
|
|
32
|
+
"why": "`return null;` occurs twice; expectHits is 1."
|
|
33
|
+
},
|
|
34
|
+
"REDACT-006/F1": {
|
|
35
|
+
"verdict": "fault-invalid",
|
|
36
|
+
"reason": "replacement-does-not-compile",
|
|
37
|
+
"why": "The replacement introduces a syntax error; the suite cannot load."
|
|
38
|
+
},
|
|
39
|
+
"EXPORT-001/F1": {
|
|
40
|
+
"verdict": "nocover",
|
|
41
|
+
"why": "test/export.test.mjs does not exist; no defender resolves."
|
|
42
|
+
},
|
|
43
|
+
"FLAKY-001/F1": {
|
|
44
|
+
"verdict": "flaky-defender",
|
|
45
|
+
"reason": "defenders-not-green",
|
|
46
|
+
"why": "test/flaky.test.mjs fails every other run; the baseline is not green N/N."
|
|
47
|
+
},
|
|
48
|
+
"DISCOVER-001/F1": {
|
|
49
|
+
"verdict": "killed",
|
|
50
|
+
"why": "No defendedBy; test/redact.test.mjs is discovered because it imports src/redact.mjs, and its masked-output test fails."
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|