testdelta 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TestDelta contributors
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.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # testdelta
2
+
3
+ > Prove that changed tests fail before a patch and pass after it.
4
+
5
+ `testdelta` is a local-first, language-independent CLI for regression-test proof. It runs the same changed tests against two isolated Git worktrees:
6
+
7
+ ```text
8
+ changed tests + base code -> must fail
9
+ changed tests + head code -> must pass
10
+ ```
11
+
12
+ If both runs pass, the test is green but does not prove the patch. That distinction matters when tests are generated quickly, written after the implementation, or produced by a coding agent optimizing for a green CI signal.
13
+
14
+ ```console
15
+ $ npx testdelta verify --base main --command "node --test {tests}"
16
+
17
+ testdelta PROVEN
18
+ The changed tests fail before the patch and pass after it.
19
+
20
+ ✓ before exit 1 438ms
21
+ ✓ after exit 0 301ms
22
+
23
+ tests math.test.js
24
+ command node --test math.test.js
25
+ base 1fcf8510a27a
26
+ ```
27
+
28
+ No API key, service, account, container, or language-specific plugin is required. The published package has zero runtime dependencies.
29
+
30
+ ## Why this exists
31
+
32
+ Normal CI answers one question: **does the current branch pass?**
33
+
34
+ For a regression test, reviewers need a second answer: **would this test have caught the bug before the fix?** A test that passes on both revisions may exercise code, but it cannot demonstrate that the production change caused the behavior to become correct.
35
+
36
+ Fail-to-pass testing is standard in software-engineering benchmarks, but it is still awkward to run on ordinary pull requests. `testdelta` turns it into one command.
37
+
38
+ ## Quick start
39
+
40
+ Requirements: Node.js 20+ and Git.
41
+
42
+ ```bash
43
+ npx testdelta init
44
+ ```
45
+
46
+ Edit the generated `.testdelta.json`, then commit your production and test changes before running:
47
+
48
+ ```bash
49
+ npx testdelta verify --base main --setup "npm ci"
50
+ ```
51
+
52
+ Or skip configuration:
53
+
54
+ ```bash
55
+ npx testdelta verify \
56
+ --base origin/main \
57
+ --command "pytest {tests}" \
58
+ --setup "python -m pip install -e ."
59
+ ```
60
+
61
+ The `{tests}` placeholder expands to shell-quoted paths for the added or modified test files. Commands without `{tests}` are also supported, which is useful for `go test ./...` and `cargo test`.
62
+
63
+ ### Try from source
64
+
65
+ ```bash
66
+ git clone <your-fork-url>/testdelta.git
67
+ cd testdelta
68
+ npm install
69
+ npm test
70
+ npm run demo
71
+ ```
72
+
73
+ ## Verdicts
74
+
75
+ | Verdict | Before | After | Exit | Meaning |
76
+ |---|---:|---:|---:|---|
77
+ | `PROVEN` | fail | pass | `0` | The changed tests distinguish the patch from its base. |
78
+ | `WEAK` | pass | pass | `2` | The tests pass without the production change. |
79
+ | `BROKEN` | fail | fail | `3` | The tests still fail on the new revision. |
80
+ | `REGRESSION` | pass | fail | `4` | The patch made previously passing behavior fail. |
81
+
82
+ Tool/configuration errors exit with `1`. This makes every result except `PROVEN` fail closed in CI.
83
+
84
+ ## How it works
85
+
86
+ 1. Resolve the merge base between `--base` and `--head`.
87
+ 2. Find added or modified files matching the test patterns.
88
+ 3. Create detached temporary worktrees for the merge base and head revision.
89
+ 4. Apply only the changed-test patch to the base worktree.
90
+ 5. Run the optional setup command and identical test command in both worktrees.
91
+ 6. Remove both worktrees and emit a deterministic verdict.
92
+
93
+ The production patch is never applied to the `before` worktree. The original checkout is never modified.
94
+
95
+ ## Configuration
96
+
97
+ `.testdelta.json`:
98
+
99
+ ```json
100
+ {
101
+ "command": "npm test -- {tests}",
102
+ "setupCommand": "npm ci",
103
+ "testPatterns": [
104
+ "**/*.test.*",
105
+ "**/*.spec.*",
106
+ "tests/**"
107
+ ],
108
+ "timeoutMs": 120000
109
+ }
110
+ ```
111
+
112
+ CLI options override configuration. Use repeated `--pattern` flags for custom layouts:
113
+
114
+ ```bash
115
+ testdelta verify --pattern "spec/**" --pattern "packages/*/checks/**"
116
+ ```
117
+
118
+ ### JSON and receipts
119
+
120
+ ```bash
121
+ testdelta verify --json
122
+ testdelta verify --report .testdelta-report.json
123
+ ```
124
+
125
+ The JSON report includes refs, merge base, test paths, command, exit codes, durations, output, timeout state, and schema version.
126
+
127
+ ## CI
128
+
129
+ The important GitHub Actions setting is `fetch-depth: 0`; the merge base must exist locally. A complete workflow is in [docs/github-actions.md](docs/github-actions.md).
130
+
131
+ For high-trust CI, pass `--command` in the protected workflow rather than allowing a pull request to redefine its own verifier.
132
+
133
+ ## What testdelta is not
134
+
135
+ - It is not a test framework; it wraps the command you already trust.
136
+ - It does not claim that a fail-to-pass test fully specifies the feature.
137
+ - It does not semantically judge assertions or detect every form of test weakening.
138
+ - It currently verifies committed changes between Git refs, not unstaged files.
139
+ - A new test that cannot even load on the base revision counts as a failure. Review the captured `before` output to distinguish a meaningful assertion failure from missing infrastructure.
140
+
141
+ ## Why not just inspect the diff?
142
+
143
+ Static test linters can detect `.skip`, `.only`, deleted assertions, and suspicious patterns. Command receipts can prove that a test command ran. Those are useful but answer different questions. `testdelta` executes the changed test in both realities and checks whether the production patch changes the outcome.
144
+
145
+ ## Project status
146
+
147
+ The MVP supports cross-platform Git worktrees, conventional test discovery, custom globs, setup commands, timeouts, four verdicts, JSON output, and a public TypeScript API. See [ROADMAP.md](ROADMAP.md) for the path to v1.
148
+
149
+ Contributions are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md) and the good-first-issue items in the roadmap.
150
+
151
+ ## License
152
+
153
+ MIT © TestDelta contributors
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { writeFile } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ import { writeStarterConfig } from "./core/config.js";
6
+ import { exitCodeFor, formatReport } from "./core/report.js";
7
+ import { verify } from "./core/verify.js";
8
+ const HELP = `testdelta — prove changed tests fail before and pass after a patch
9
+
10
+ Usage:
11
+ testdelta verify [options]
12
+ testdelta init
13
+
14
+ Options:
15
+ --base <ref> Base branch or commit (default: main)
16
+ --head <ref> Head branch or commit (default: HEAD)
17
+ --command <command> Test command; use {tests} for changed test paths
18
+ --setup <command> Optional dependency setup command for each worktree
19
+ --pattern <glob> Test-file glob; repeatable
20
+ --timeout <ms> Per-command timeout (default: 120000)
21
+ --json Print JSON instead of the terminal report
22
+ --report <path> Also write the JSON report to a file
23
+ --keep-worktrees Keep temporary worktrees for debugging
24
+ -h, --help Show help
25
+ -v, --version Show version
26
+
27
+ Exit codes: 0 proven, 1 tool error, 2 weak, 3 broken, 4 regression.
28
+ `;
29
+ async function main() {
30
+ const argv = process.argv.slice(2);
31
+ const subcommand = argv[0]?.startsWith("-") ? "verify" : (argv.shift() ?? "verify");
32
+ if (subcommand === "init") {
33
+ const path = await writeStarterConfig(process.cwd());
34
+ process.stdout.write(`Created ${path}\n`);
35
+ return;
36
+ }
37
+ if (subcommand !== "verify")
38
+ throw new Error(`Unknown command: ${subcommand}`);
39
+ const { values } = parseArgs({
40
+ args: argv,
41
+ options: {
42
+ base: { type: "string", default: "main" },
43
+ head: { type: "string", default: "HEAD" },
44
+ command: { type: "string" },
45
+ setup: { type: "string" },
46
+ pattern: { type: "string", multiple: true },
47
+ timeout: { type: "string" },
48
+ json: { type: "boolean", default: false },
49
+ report: { type: "string" },
50
+ "keep-worktrees": { type: "boolean", default: false },
51
+ help: { type: "boolean", short: "h", default: false },
52
+ version: { type: "boolean", short: "v", default: false },
53
+ },
54
+ strict: true,
55
+ });
56
+ if (values.help) {
57
+ process.stdout.write(HELP);
58
+ return;
59
+ }
60
+ if (values.version) {
61
+ process.stdout.write("0.1.0\n");
62
+ return;
63
+ }
64
+ const report = await verify({
65
+ cwd: process.cwd(),
66
+ base: values.base ?? "main",
67
+ head: values.head ?? "HEAD",
68
+ ...(values.command ? { command: values.command } : {}),
69
+ ...(values.setup ? { setupCommand: values.setup } : {}),
70
+ ...(values.pattern ? { testPatterns: values.pattern } : {}),
71
+ ...(values.timeout ? { timeoutMs: Number.parseInt(values.timeout, 10) } : {}),
72
+ keepWorktrees: values["keep-worktrees"] ?? false,
73
+ });
74
+ const json = `${JSON.stringify(report, null, 2)}\n`;
75
+ process.stdout.write(values.json ? json : formatReport(report));
76
+ if (values.report)
77
+ await writeFile(resolve(values.report), json);
78
+ process.exitCode = exitCodeFor(report.verdict);
79
+ }
80
+ main().catch((error) => {
81
+ process.stderr.write(`testdelta error: ${error.message}\n`);
82
+ process.exitCode = 1;
83
+ });
84
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { TestDeltaConfig } from "../types.js";
2
+ export declare function loadConfig(cwd: string): Promise<TestDeltaConfig>;
3
+ export declare function detectTestCommand(cwd: string): Promise<string | undefined>;
4
+ export declare function writeStarterConfig(cwd: string): Promise<string>;
@@ -0,0 +1,66 @@
1
+ import { access, readFile, writeFile } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { defaultTestPatterns } from "./patterns.js";
5
+ const CONFIG_NAME = ".testdelta.json";
6
+ export async function loadConfig(cwd) {
7
+ const defaults = {
8
+ testPatterns: defaultTestPatterns(),
9
+ timeoutMs: 120_000,
10
+ };
11
+ try {
12
+ const raw = await readFile(join(cwd, CONFIG_NAME), "utf8");
13
+ const parsed = JSON.parse(raw);
14
+ return {
15
+ ...defaults,
16
+ ...parsed,
17
+ testPatterns: parsed.testPatterns ?? defaults.testPatterns,
18
+ };
19
+ }
20
+ catch (error) {
21
+ const code = error.code;
22
+ if (code === "ENOENT")
23
+ return defaults;
24
+ throw new Error(`Could not read ${CONFIG_NAME}: ${error.message}`);
25
+ }
26
+ }
27
+ export async function detectTestCommand(cwd) {
28
+ const exists = async (path) => {
29
+ try {
30
+ await access(join(cwd, path), constants.F_OK);
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ };
37
+ if (await exists("package.json")) {
38
+ const manifest = JSON.parse(await readFile(join(cwd, "package.json"), "utf8"));
39
+ if (manifest.scripts?.test) {
40
+ if (await exists("pnpm-lock.yaml"))
41
+ return "pnpm test -- {tests}";
42
+ if (await exists("yarn.lock"))
43
+ return "yarn test {tests}";
44
+ return "npm test -- {tests}";
45
+ }
46
+ }
47
+ if ((await exists("pyproject.toml")) || (await exists("pytest.ini")))
48
+ return "pytest {tests}";
49
+ if (await exists("go.mod"))
50
+ return "go test ./...";
51
+ if (await exists("Cargo.toml"))
52
+ return "cargo test";
53
+ return undefined;
54
+ }
55
+ export async function writeStarterConfig(cwd) {
56
+ const path = join(cwd, CONFIG_NAME);
57
+ const detectedCommand = await detectTestCommand(cwd);
58
+ const config = {
59
+ command: detectedCommand ?? "YOUR_TEST_COMMAND {tests}",
60
+ testPatterns: defaultTestPatterns(),
61
+ timeoutMs: 120_000,
62
+ };
63
+ await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { flag: "wx" });
64
+ return path;
65
+ }
66
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/core/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export declare function assertGitRepository(cwd: string): Promise<void>;
2
+ export declare function resolveRef(ref: string, cwd: string): Promise<string>;
3
+ export declare function findMergeBase(base: string, head: string, cwd: string): Promise<string>;
4
+ export declare function changedFiles(base: string, head: string, cwd: string): Promise<string[]>;
5
+ export declare function createWorktree(ref: string, cwd: string, label: string): Promise<string>;
6
+ export declare function applyTestPatch(base: string, head: string, testFiles: string[], sourceRepo: string, targetWorktree: string): Promise<void>;
7
+ export declare function removeWorktree(path: string, cwd: string): Promise<void>;
@@ -0,0 +1,42 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { runProcess } from "./process.js";
5
+ async function git(args, cwd, allowFailure = false) {
6
+ return await runProcess("git", args, { cwd, allowFailure });
7
+ }
8
+ export async function assertGitRepository(cwd) {
9
+ await git(["rev-parse", "--show-toplevel"], cwd);
10
+ }
11
+ export async function resolveRef(ref, cwd) {
12
+ const result = await git(["rev-parse", "--verify", ref], cwd);
13
+ return result.stdout.trim();
14
+ }
15
+ export async function findMergeBase(base, head, cwd) {
16
+ const result = await git(["merge-base", base, head], cwd);
17
+ return result.stdout.trim();
18
+ }
19
+ export async function changedFiles(base, head, cwd) {
20
+ const result = await git(["diff", "--name-only", "--diff-filter=AM", "-z", base, head, "--"], cwd);
21
+ return result.stdout.split("\0").filter(Boolean);
22
+ }
23
+ export async function createWorktree(ref, cwd, label) {
24
+ const root = await mkdtemp(join(tmpdir(), `testdelta-${label}-`));
25
+ await git(["worktree", "add", "--detach", root, ref], cwd);
26
+ return root;
27
+ }
28
+ export async function applyTestPatch(base, head, testFiles, sourceRepo, targetWorktree) {
29
+ const patch = await git(["diff", "--binary", base, head, "--", ...testFiles], sourceRepo);
30
+ if (!patch.stdout.trim())
31
+ throw new Error("The selected test files produced an empty patch.");
32
+ await runProcess("git", ["apply", "--whitespace=nowarn", "-"], {
33
+ cwd: targetWorktree,
34
+ input: patch.stdout,
35
+ });
36
+ }
37
+ export async function removeWorktree(path, cwd) {
38
+ await git(["worktree", "remove", "--force", path], cwd, true);
39
+ await rm(path, { recursive: true, force: true });
40
+ await git(["worktree", "prune"], cwd, true);
41
+ }
42
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../../src/core/git.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export declare function defaultTestPatterns(): string[];
2
+ export declare function globToRegex(glob: string): RegExp;
3
+ export declare function selectTestFiles(files: string[], patterns: string[]): string[];
@@ -0,0 +1,44 @@
1
+ const DEFAULT_TEST_PATTERNS = [
2
+ "**/*.test.*",
3
+ "**/*.spec.*",
4
+ "**/test_*.py",
5
+ "**/*_test.py",
6
+ "**/*_test.go",
7
+ "tests/**",
8
+ "test/**",
9
+ "__tests__/**",
10
+ ];
11
+ export function defaultTestPatterns() {
12
+ return [...DEFAULT_TEST_PATTERNS];
13
+ }
14
+ function escapeRegex(character) {
15
+ return /[\\^$.*+?()[\]{}|]/.test(character) ? `\\${character}` : character;
16
+ }
17
+ export function globToRegex(glob) {
18
+ const normalized = glob.replaceAll("\\", "/");
19
+ let source = "^";
20
+ for (let index = 0; index < normalized.length; index += 1) {
21
+ const character = normalized[index] ?? "";
22
+ const next = normalized[index + 1];
23
+ if (character === "*" && next === "*") {
24
+ const followedBySlash = normalized[index + 2] === "/";
25
+ source += followedBySlash ? "(?:.*/)?" : ".*";
26
+ index += followedBySlash ? 2 : 1;
27
+ }
28
+ else if (character === "*") {
29
+ source += "[^/]*";
30
+ }
31
+ else if (character === "?") {
32
+ source += "[^/]";
33
+ }
34
+ else {
35
+ source += escapeRegex(character);
36
+ }
37
+ }
38
+ return new RegExp(`${source}$`);
39
+ }
40
+ export function selectTestFiles(files, patterns) {
41
+ const matchers = patterns.map(globToRegex);
42
+ return files.filter((file) => matchers.some((matcher) => matcher.test(file.replaceAll("\\", "/"))));
43
+ }
44
+ //# sourceMappingURL=patterns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../../src/core/patterns.ts"],"names":[],"mappings":"AAAA,MAAM,qBAAqB,GAAG;IAC5B,aAAa;IACb,aAAa;IACb,cAAc;IACd,cAAc;IACd,cAAc;IACd,UAAU;IACV,SAAS;IACT,cAAc;CACf,CAAC;AAEF,MAAM,UAAU,mBAAmB;IACjC,OAAO,CAAC,GAAG,qBAAqB,CAAC,CAAC;AACpC,CAAC;AAED,SAAS,WAAW,CAAC,SAAiB;IACpC,OAAO,oBAAoB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC9C,IAAI,MAAM,GAAG,GAAG,CAAC;IAEjB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,UAAU,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAC1D,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QACnC,IAAI,SAAS,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACtC,MAAM,eAAe,GAAG,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;YACtD,MAAM,IAAI,eAAe,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;YAC9C,KAAK,IAAI,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACnC,CAAC;aAAM,IAAI,SAAS,KAAK,GAAG,EAAE,CAAC;YAC7B,MAAM,IAAI,OAAO,CAAC;QACpB,CAAC;aAAM,IAAI,SAAS,KAAK,GAAG,EAAE,CAAC;YAC7B,MAAM,IAAI,MAAM,CAAC;QACnB,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,WAAW,CAAC,SAAS,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IAED,OAAO,IAAI,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAe,EAAE,QAAkB;IACjE,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC3C,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AACtG,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { CommandResult } from "../types.js";
2
+ interface RunOptions {
3
+ cwd: string;
4
+ timeoutMs?: number;
5
+ input?: string;
6
+ allowFailure?: boolean;
7
+ }
8
+ export declare function runProcess(executable: string, args: string[], options: RunOptions): Promise<CommandResult>;
9
+ export declare function runShell(command: string, cwd: string, timeoutMs: number): Promise<CommandResult>;
10
+ export {};
@@ -0,0 +1,59 @@
1
+ import { spawn } from "node:child_process";
2
+ export async function runProcess(executable, args, options) {
3
+ const startedAt = Date.now();
4
+ const env = { ...process.env };
5
+ // A nested `node --test` must start as a fresh runner, not inherit the parent
6
+ // runner's private worker marker (relevant when testdelta itself runs in CI tests).
7
+ delete env.NODE_TEST_CONTEXT;
8
+ return await new Promise((resolve, reject) => {
9
+ const child = spawn(executable, args, {
10
+ cwd: options.cwd,
11
+ env,
12
+ shell: false,
13
+ stdio: "pipe",
14
+ windowsHide: true,
15
+ });
16
+ let stdout = "";
17
+ let stderr = "";
18
+ let timedOut = false;
19
+ child.stdout.setEncoding("utf8");
20
+ child.stderr.setEncoding("utf8");
21
+ child.stdout.on("data", (chunk) => (stdout += chunk));
22
+ child.stderr.on("data", (chunk) => (stderr += chunk));
23
+ child.on("error", reject);
24
+ const timer = options.timeoutMs
25
+ ? setTimeout(() => {
26
+ timedOut = true;
27
+ child.kill("SIGTERM");
28
+ }, options.timeoutMs)
29
+ : undefined;
30
+ child.on("close", (code) => {
31
+ if (timer)
32
+ clearTimeout(timer);
33
+ const result = {
34
+ command: [executable, ...args].join(" "),
35
+ exitCode: timedOut ? 124 : (code ?? 1),
36
+ durationMs: Date.now() - startedAt,
37
+ stdout,
38
+ stderr,
39
+ timedOut,
40
+ };
41
+ if (!options.allowFailure && result.exitCode !== 0) {
42
+ reject(new Error(`${result.command} failed (${result.exitCode})\n${stderr || stdout}`));
43
+ return;
44
+ }
45
+ resolve(result);
46
+ });
47
+ if (options.input !== undefined)
48
+ child.stdin.end(options.input);
49
+ else
50
+ child.stdin.end();
51
+ });
52
+ }
53
+ export async function runShell(command, cwd, timeoutMs) {
54
+ const shell = process.platform === "win32" ? process.env.ComSpec ?? "cmd.exe" : "/bin/sh";
55
+ const args = process.platform === "win32" ? ["/d", "/s", "/c", command] : ["-c", command];
56
+ const result = await runProcess(shell, args, { cwd, timeoutMs, allowFailure: true });
57
+ return { ...result, command };
58
+ }
59
+ //# sourceMappingURL=process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.js","sourceRoot":"","sources":["../../../src/core/process.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { VerificationReport, Verdict } from "../types.js";
2
+ export declare function formatReport(report: VerificationReport): string;
3
+ export declare function exitCodeFor(verdict: Verdict): number;
@@ -0,0 +1,38 @@
1
+ const DESCRIPTIONS = {
2
+ PROVEN: "The changed tests fail before the patch and pass after it.",
3
+ WEAK: "The changed tests pass without the production patch; they do not prove the change.",
4
+ BROKEN: "The changed tests fail both before and after the patch.",
5
+ REGRESSION: "The changed tests pass before the patch but fail after it.",
6
+ };
7
+ function mark(exitCode, expectedPass) {
8
+ const passed = exitCode === 0;
9
+ return passed === expectedPass ? "✓" : "✗";
10
+ }
11
+ function failureDetails(label, exitCode, stdout, stderr) {
12
+ if (exitCode === 0)
13
+ return [];
14
+ const output = (stderr || stdout).trim().split(/\r?\n/).slice(-8);
15
+ return output.length > 0 ? ["", `${label} output:`, ...output.map((line) => ` ${line}`)] : [];
16
+ }
17
+ export function formatReport(report) {
18
+ const lines = [
19
+ "",
20
+ `testdelta ${report.verdict}`,
21
+ DESCRIPTIONS[report.verdict],
22
+ "",
23
+ `${mark(report.before.exitCode, false)} before exit ${report.before.exitCode} ${report.before.durationMs}ms`,
24
+ `${mark(report.after.exitCode, true)} after exit ${report.after.exitCode} ${report.after.durationMs}ms`,
25
+ "",
26
+ `tests ${report.testFiles.join(", ")}`,
27
+ `command ${report.command}`,
28
+ `base ${report.mergeBase.slice(0, 12)}`,
29
+ ...failureDetails("before", report.before.exitCode, report.before.stdout, report.before.stderr),
30
+ ...failureDetails("after", report.after.exitCode, report.after.stdout, report.after.stderr),
31
+ "",
32
+ ];
33
+ return lines.join("\n");
34
+ }
35
+ export function exitCodeFor(verdict) {
36
+ return { PROVEN: 0, WEAK: 2, BROKEN: 3, REGRESSION: 4 }[verdict];
37
+ }
38
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../../src/core/report.ts"],"names":[],"mappings":"AAEA,MAAM,YAAY,GAA4B;IAC5C,MAAM,EAAE,4DAA4D;IACpE,IAAI,EAAE,oFAAoF;IAC1F,MAAM,EAAE,yDAAyD;IACjE,UAAU,EAAE,4DAA4D;CACzE,CAAC;AAEF,SAAS,IAAI,CAAC,QAAgB,EAAE,YAAqB;IACnD,MAAM,MAAM,GAAG,QAAQ,KAAK,CAAC,CAAC;IAC9B,OAAO,MAAM,KAAK,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;AAC7C,CAAC;AAED,SAAS,cAAc,CAAC,KAAa,EAAE,QAAgB,EAAE,MAAc,EAAE,MAAc;IACrF,IAAI,QAAQ,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC9B,MAAM,MAAM,GAAG,CAAC,MAAM,IAAI,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAClE,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,KAAK,UAAU,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACjG,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAA0B;IACrD,MAAM,KAAK,GAAG;QACZ,EAAE;QACF,aAAa,MAAM,CAAC,OAAO,EAAE;QAC7B,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC;QAC5B,EAAE;QACF,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,iBAAiB,MAAM,CAAC,MAAM,CAAC,QAAQ,KAAK,MAAM,CAAC,MAAM,CAAC,UAAU,IAAI;QAC9G,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,iBAAiB,MAAM,CAAC,KAAK,CAAC,QAAQ,KAAK,MAAM,CAAC,KAAK,CAAC,UAAU,IAAI;QAC1G,EAAE;QACF,WAAW,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QACxC,WAAW,MAAM,CAAC,OAAO,EAAE;QAC3B,WAAW,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE;QAC1C,GAAG,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;QAC/F,GAAG,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;QAC3F,EAAE;KACH,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAgB;IAC1C,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;AACnE,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { VerificationReport, VerifyOptions } from "../types.js";
2
+ export declare function verify(options: VerifyOptions): Promise<VerificationReport>;
3
+ export declare function projectName(cwd: string): string;
@@ -0,0 +1,92 @@
1
+ import { basename } from "node:path";
2
+ import { applyTestPatch, assertGitRepository, changedFiles, createWorktree, findMergeBase, removeWorktree, resolveRef, } from "./git.js";
3
+ import { detectTestCommand, loadConfig } from "./config.js";
4
+ import { runShell } from "./process.js";
5
+ import { selectTestFiles } from "./patterns.js";
6
+ function quoteArgument(value) {
7
+ if (/^[A-Za-z0-9_./\\-]+$/.test(value))
8
+ return value;
9
+ if (process.platform === "win32")
10
+ return `"${value.replaceAll('"', '\\"')}"`;
11
+ return `'${value.replaceAll("'", "'\\''")}'`;
12
+ }
13
+ function expandCommand(template, testFiles) {
14
+ const tests = testFiles.map(quoteArgument).join(" ");
15
+ return template.includes("{tests}") ? template.replaceAll("{tests}", tests) : template;
16
+ }
17
+ function verdictFor(beforeExit, afterExit) {
18
+ if (beforeExit !== 0 && afterExit === 0)
19
+ return "PROVEN";
20
+ if (beforeExit === 0 && afterExit === 0)
21
+ return "WEAK";
22
+ if (beforeExit !== 0 && afterExit !== 0)
23
+ return "BROKEN";
24
+ return "REGRESSION";
25
+ }
26
+ export async function verify(options) {
27
+ await assertGitRepository(options.cwd);
28
+ const config = await loadConfig(options.cwd);
29
+ const baseRef = await resolveRef(options.base, options.cwd);
30
+ const headRef = await resolveRef(options.head, options.cwd);
31
+ const mergeBase = await findMergeBase(baseRef, headRef, options.cwd);
32
+ const files = await changedFiles(mergeBase, headRef, options.cwd);
33
+ const patterns = options.testPatterns ?? config.testPatterns;
34
+ const testFiles = selectTestFiles(files, patterns);
35
+ if (testFiles.length === 0) {
36
+ throw new Error(`No added or modified test files matched: ${patterns.join(", ")}`);
37
+ }
38
+ const commandTemplate = options.command ?? config.command ?? (await detectTestCommand(options.cwd));
39
+ if (!commandTemplate) {
40
+ throw new Error("No test command found. Pass --command or run testdelta init.");
41
+ }
42
+ const command = expandCommand(commandTemplate, testFiles);
43
+ const setupCommand = options.setupCommand ?? config.setupCommand;
44
+ const timeoutMs = options.timeoutMs ?? config.timeoutMs;
45
+ let beforeWorktree;
46
+ let afterWorktree;
47
+ try {
48
+ beforeWorktree = await createWorktree(mergeBase, options.cwd, "before");
49
+ afterWorktree = await createWorktree(headRef, options.cwd, "after");
50
+ await applyTestPatch(mergeBase, headRef, testFiles, options.cwd, beforeWorktree);
51
+ if (setupCommand) {
52
+ const beforeSetup = await runShell(setupCommand, beforeWorktree, timeoutMs);
53
+ if (beforeSetup.exitCode !== 0)
54
+ throw new Error(`Setup failed in before worktree:\n${beforeSetup.stderr || beforeSetup.stdout}`);
55
+ const afterSetup = await runShell(setupCommand, afterWorktree, timeoutMs);
56
+ if (afterSetup.exitCode !== 0)
57
+ throw new Error(`Setup failed in after worktree:\n${afterSetup.stderr || afterSetup.stdout}`);
58
+ }
59
+ const before = await runShell(command, beforeWorktree, timeoutMs);
60
+ const after = await runShell(command, afterWorktree, timeoutMs);
61
+ return {
62
+ schemaVersion: 1,
63
+ verdict: verdictFor(before.exitCode, after.exitCode),
64
+ base: options.base,
65
+ head: options.head,
66
+ mergeBase,
67
+ testFiles,
68
+ command,
69
+ before,
70
+ after,
71
+ generatedAt: new Date().toISOString(),
72
+ };
73
+ }
74
+ finally {
75
+ if (!options.keepWorktrees) {
76
+ if (beforeWorktree)
77
+ await removeWorktree(beforeWorktree, options.cwd);
78
+ if (afterWorktree)
79
+ await removeWorktree(afterWorktree, options.cwd);
80
+ }
81
+ else {
82
+ if (beforeWorktree)
83
+ process.stderr.write(`Kept before worktree: ${beforeWorktree}\n`);
84
+ if (afterWorktree)
85
+ process.stderr.write(`Kept after worktree: ${afterWorktree}\n`);
86
+ }
87
+ }
88
+ }
89
+ export function projectName(cwd) {
90
+ return basename(cwd);
91
+ }
92
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../src/core/verify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { verify } from "./core/verify.js";
2
+ export { formatReport, exitCodeFor } from "./core/report.js";
3
+ export { selectTestFiles, globToRegex, defaultTestPatterns } from "./core/patterns.js";
4
+ export type { CommandResult, TestDeltaConfig, VerificationReport, VerifyOptions, Verdict, } from "./types.js";
@@ -0,0 +1,4 @@
1
+ export { verify } from "./core/verify.js";
2
+ export { formatReport, exitCodeFor } from "./core/report.js";
3
+ export { selectTestFiles, globToRegex, defaultTestPatterns } from "./core/patterns.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,37 @@
1
+ export interface TestDeltaConfig {
2
+ command?: string;
3
+ setupCommand?: string;
4
+ testPatterns: string[];
5
+ timeoutMs: number;
6
+ }
7
+ export interface CommandResult {
8
+ command: string;
9
+ exitCode: number;
10
+ durationMs: number;
11
+ stdout: string;
12
+ stderr: string;
13
+ timedOut: boolean;
14
+ }
15
+ export type Verdict = "PROVEN" | "WEAK" | "BROKEN" | "REGRESSION";
16
+ export interface VerificationReport {
17
+ schemaVersion: 1;
18
+ verdict: Verdict;
19
+ base: string;
20
+ head: string;
21
+ mergeBase: string;
22
+ testFiles: string[];
23
+ command: string;
24
+ before: CommandResult;
25
+ after: CommandResult;
26
+ generatedAt: string;
27
+ }
28
+ export interface VerifyOptions {
29
+ cwd: string;
30
+ base: string;
31
+ head: string;
32
+ command?: string;
33
+ setupCommand?: string;
34
+ testPatterns?: string[];
35
+ timeoutMs?: number;
36
+ keepWorktrees?: boolean;
37
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "testdelta",
3
+ "version": "0.1.0",
4
+ "description": "Prove that changed tests fail before a patch and pass after it.",
5
+ "type": "module",
6
+ "bin": {
7
+ "testdelta": "dist/src/cli.js"
8
+ },
9
+ "exports": {
10
+ ".": "./dist/src/index.js"
11
+ },
12
+ "files": [
13
+ "dist/src",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "check": "tsc -p tsconfig.json --noEmit",
20
+ "test": "npm run build && node --test dist/tests/patterns.test.js dist/tests/report.test.js dist/tests/verify.integration.test.js",
21
+ "demo": "npm run build && node scripts/demo.mjs",
22
+ "prepublishOnly": "npm test"
23
+ },
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "keywords": [
28
+ "testing",
29
+ "regression",
30
+ "tdd",
31
+ "git",
32
+ "ci",
33
+ "coding-agents",
34
+ "fail-to-pass"
35
+ ],
36
+ "license": "MIT",
37
+ "devDependencies": {
38
+ "@types/node": "^24.0.0",
39
+ "typescript": "^5.8.0"
40
+ }
41
+ }