testguard-cli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1 @@
1
+ """TestGuard CLI — Python wrapper around the Node.js `testguard-cli` package."""
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ TestGuard CLI — Python wrapper.
4
+
5
+ Runs the Node.js TestGuard CLI so Python-centric teams can `pip install
6
+ testguard-cli` and use `testguard` without touching npm directly.
7
+ Node.js 20+ is required.
8
+
9
+ Resolution order:
10
+ 1. A locally installed `node_modules/testguard-cli/cli/testguard.mjs`,
11
+ searched upward from the current directory — so a project that pins the
12
+ package runs the PINNED version, offline and reproducibly.
13
+ 2. `npx -y testguard-cli@latest`.
14
+
15
+ Usage:
16
+ pip install testguard-cli
17
+ testguard claims
18
+ testguard probe
19
+ """
20
+
21
+ import os
22
+ import shutil
23
+ import subprocess
24
+ import sys
25
+
26
+ NODE_FLOOR = 20
27
+
28
+
29
+ def find_node():
30
+ """Find a usable Node.js binary (>= NODE_FLOOR)."""
31
+ for cmd in ("node", "node22", "node20"):
32
+ path = shutil.which(cmd)
33
+ if not path:
34
+ continue
35
+ try:
36
+ version = subprocess.check_output(
37
+ [path, "--version"], text=True, stderr=subprocess.DEVNULL
38
+ ).strip()
39
+ if int(version.lstrip("v").split(".")[0]) >= NODE_FLOOR:
40
+ return path
41
+ except (subprocess.CalledProcessError, ValueError):
42
+ continue
43
+ return None
44
+
45
+
46
+ def find_local_cli():
47
+ """Resolve a locally installed CLI entry, walking up from cwd."""
48
+ directory = os.getcwd()
49
+ while True:
50
+ entry = os.path.join(directory, "node_modules", "testguard-cli", "cli", "testguard.mjs")
51
+ if os.path.isfile(entry):
52
+ return entry
53
+ parent = os.path.dirname(directory)
54
+ if parent == directory:
55
+ return None
56
+ directory = parent
57
+
58
+
59
+ def main():
60
+ """Entry point for the `testguard` command."""
61
+ args = sys.argv[1:]
62
+ node = find_node()
63
+ if not node:
64
+ print(
65
+ f"Error: Node.js {NODE_FLOOR}+ is required but not found.\n"
66
+ "Install from https://nodejs.org/ or via nvm: nvm install 22",
67
+ file=sys.stderr,
68
+ )
69
+ sys.exit(1)
70
+
71
+ local_cli = find_local_cli()
72
+ npx = shutil.which("npx")
73
+ if local_cli:
74
+ cmd = [node, local_cli] + args
75
+ elif npx:
76
+ cmd = [npx, "-y", "testguard-cli@latest"] + args
77
+ else:
78
+ print("Error: npx not found. Install Node.js 20+, which includes npm/npx.", file=sys.stderr)
79
+ sys.exit(1)
80
+
81
+ try:
82
+ sys.exit(subprocess.run(cmd, check=False).returncode)
83
+ except FileNotFoundError:
84
+ print(f"Error: could not execute: {' '.join(cmd)}", file=sys.stderr)
85
+ sys.exit(1)
86
+ except KeyboardInterrupt:
87
+ sys.exit(130)
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.5
2
+ Name: testguard-cli
3
+ Version: 0.1.0
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
+ Project-URL: Homepage, https://github.com/raccioly/testguard
6
+ Project-URL: Documentation, https://github.com/raccioly/testguard#readme
7
+ Project-URL: Repository, https://github.com/raccioly/testguard
8
+ Project-URL: Issues, https://github.com/raccioly/testguard/issues
9
+ Author-email: Ricardo Accioly <raccioly@gmail.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai-agents,claims,fault-injection,mutation-testing,quality-assurance,test-quality,testing,verification,vitest
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: JavaScript
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+
25
+ # TestGuard
26
+
27
+ [![CI](https://github.com/raccioly/testguard/actions/workflows/ci.yml/badge.svg)](https://github.com/raccioly/testguard/actions/workflows/ci.yml)
28
+ [![npm](https://img.shields.io/npm/v/testguard-cli.svg)](https://www.npmjs.com/package/testguard-cli)
29
+ [![PyPI](https://img.shields.io/pypi/v/testguard-cli.svg)](https://pypi.org/project/testguard-cli/)
30
+ [![node](https://img.shields.io/node/v/testguard-cli.svg)](https://nodejs.org)
31
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
32
+ [![zero deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen.svg)](./package.json)
33
+
34
+ > Proves that a test suite actually defends the claims a project makes — by
35
+ > injecting the faults those claims say cannot happen, and reporting every
36
+ > fault the tests fail to detect.
37
+
38
+ **Not a test generator. A claim verifier.** Test generation is what happens
39
+ after a claim turns out to be unfalsifiable.
40
+
41
+ Third tool following the Guard pattern, alongside
42
+ [`docguard-cli`](https://www.npmjs.com/package/docguard-cli) (docs ↔ code) and
43
+ [`websec-validator`](https://pypi.org/project/websec-validator/) (attack
44
+ surface ↔ code). All three run one loop:
45
+
46
+ > declare what must be true → try mechanically to falsify it → freeze a
47
+ > baseline → gate only the delta → brief the agent before it writes code.
48
+
49
+ ## Why
50
+
51
+ Coverage cannot tell a test that pins *correct* behaviour from one that pins
52
+ a *defect*. An agent that writes both the code and its tests encodes whatever
53
+ it believed — including its bugs — and the suite goes green.
54
+
55
+ Measured on a real, entirely AI-authored production codebase with ~4,900
56
+ disciplined tests (no snapshots, 0.4% zero-assertion): **8 of 9 real
57
+ historical bugs were invisible to the suite**, worst case 2,451 tests green
58
+ on known-broken code. The largest gap was a compliance-critical path with
59
+ 100% coverage, where the one assertion that mattered used
60
+ `expect.objectContaining({...})` and omitted the field carrying the data.
61
+
62
+ ## Install
63
+
64
+ | How | Command |
65
+ |---|---|
66
+ | npx (no install) | `npx testguard-cli probe` |
67
+ | npm | `npm i -D testguard-cli` then `npx testguard probe` |
68
+ | pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
69
+ | Homebrew | `brew tap raccioly/tap && brew install testguard` |
70
+ | GitHub Action | `uses: raccioly/testguard@v0.1.0` — see [`action.yml`](./action.yml) |
71
+ | pre-commit | `repo: https://github.com/raccioly/testguard`, hooks `testguard-claims`, `testguard-probe` |
72
+
73
+ ## How it works
74
+
75
+ ```bash
76
+ npx testguard-cli claims # what does this project claim, and is every claim probeable?
77
+ npx testguard-cli probe # try to falsify each claim; report what the tests missed
78
+ npx testguard-cli baseline # freeze today's unproven findings; from now on only new ones gate
79
+ npx testguard-cli brief # tell the agent where the suite is blind, before it writes
80
+ ```
81
+
82
+ 1. **Claims** live in `testguard.claims.json`: a statement, where it comes
83
+ from, which tests supposedly defend it, and one or more *faults* — each a
84
+ deterministic source change that would make the statement false. Every
85
+ claim and every fault records who produced it. `testguard claims`
86
+ validates the file and reports drift against `@claim <ID>` annotations in
87
+ source.
88
+ 2. **Probe** confirms the defenders are green N times unmodified, applies
89
+ each fault in a scratch git worktree (your tree is never touched), runs
90
+ the defenders N times, re-runs survivors against the whole suite with
91
+ N-run attribution, restores, and classifies. Verdicts are a closed set:
92
+
93
+ | Verdict | Meaning |
94
+ |---|---|
95
+ | `killed` | a test body rejected the behaviour, N/N — the only pass |
96
+ | `SURVIVED` | the defenders stayed green while the claim was false |
97
+ | `NOCOVER` | no test file defends the claim at all |
98
+ | `UNVERIFIABLE` | the fault's anchor is missing or ambiguous — loud, never a skip |
99
+ | `TIMEOUT` | the defenders hung; a hang is not a detection |
100
+ | `FAULT-INVALID` | the replacement does not load — a bad fault, not a finding |
101
+ | `FLAKY-DEFENDER` | the defenders are not reliably green, or disagreed across runs |
102
+
103
+ Never a single score. Findings are ranked by severity, claim provenance
104
+ and blast radius, and written to `.testguard/evidence.json` — validated
105
+ against the spec before it is written.
106
+ 3. **Baseline** freezes every non-passing fingerprint. Later probes suppress
107
+ what was already known and exit non-zero only on what is new. Claims whose
108
+ source and defenders are unchanged reuse their prior verdict, so a probe
109
+ in CI costs only what changed.
110
+ 4. **Brief** turns evidence plus baseline into a ranked, capped
111
+ `## TEST BLINDSPOT CONTEXT` block. Wire it into an agent's session start
112
+ — for Claude Code, in `.claude/settings.json`:
113
+
114
+ ```json
115
+ { "hooks": { "SessionStart": [ { "hooks": [
116
+ { "type": "command", "command": "npx testguard-cli brief --text" }
117
+ ] } ] } }
118
+ ```
119
+
120
+ `--text` prints only, and exits 0 silently when there is no evidence yet,
121
+ so the hook can never break a session.
122
+
123
+ **Commit `.testguard/baseline.json`; ignore `evidence.json` and `brief.json`.**
124
+ The baseline is the frozen contract; the other two are regenerated per run.
125
+
126
+ The fault model is the auditable artifact. You never reach 100% of
127
+ correctness; you reach **100% of stated claims verified**, and the statement
128
+ of claims is what an assessor reads. A claims file is code — its `replace`
129
+ strings run under your test runner — so review it like code.
130
+
131
+ ## Try it
132
+
133
+ The repository ships a known-answer fixture with a real blind spot:
134
+
135
+ ```bash
136
+ git clone <this repo> && cd testguard && npm install
137
+ npm test # includes probing the fixture end to end
138
+ ```
139
+
140
+ `fixtures/known-answer/` is a tiny project whose audit-row test asserts with
141
+ `expect.objectContaining({...})` and omits the `content` key. Swap the
142
+ redacted text for the raw input and the test stays green. `probe` reports it
143
+ as `SURVIVED`; the fixture's [README](fixtures/known-answer/README.md) walks
144
+ through every verdict.
145
+
146
+ ## Status
147
+
148
+ **v0.1.** Four commands, vitest runner, hand-authored faults. The contract
149
+ spine — six JSON Schemas shared with the other Guard tools — is under
150
+ [`spec/`](spec/). Zero runtime dependencies; Node ≥ 20.
151
+
152
+ Not yet: test generation (the two-gate acceptance loop), other runners,
153
+ mechanical fault producers, and calibration of fault classes against real
154
+ escaped bugs. Each is designed for; none is claimed.
155
+
156
+ ## Licence
157
+
158
+ MIT.
@@ -0,0 +1,7 @@
1
+ testguard_cli/__init__.py,sha256=8sGzMplmVqpmyu5Q1OrgDrs-AUUBVLqVEqCyXBoV_3k,83
2
+ testguard_cli/wrapper.py,sha256=24oBMXAiHuFz9K32ggrD1tk_8oq1iBLcWXIdjrLowbk,2581
3
+ testguard_cli-0.1.0.dist-info/METADATA,sha256=cC5fZuV1VwmO--67BY5S2CRs_DwJGh1eDSS6bx1LeCk,7716
4
+ testguard_cli-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
5
+ testguard_cli-0.1.0.dist-info/entry_points.txt,sha256=b-DAHthznWkLRH9h5URsjvAJNFlyqbNENKAwVun81IY,57
6
+ testguard_cli-0.1.0.dist-info/licenses/LICENSE,sha256=gQ9PJNsgxXTDVYfhspPfwlj1ba3MeDnbL8G0dqN2jLE,1072
7
+ testguard_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ testguard = testguard_cli.wrapper:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ricardo Accioly
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.