codehere 0.8.0 → 1.0.2
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/README.md +37 -184
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +329 -0
- package/dist/index.js +74 -1004
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
- package/dist/presentation/cli/commands/check-command.js +251 -0
- package/dist/presentation/cli/commands/help-command.js +22 -286
- package/dist/ui.js +19 -438
- package/dist/utils/version.js +0 -12
- package/package.json +18 -27
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/generate-final-report.js +0 -134
- package/dist/application/use-cases/generate-narration.js +0 -128
- package/dist/application/use-cases/generate-thread-title.js +0 -178
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/thread-export.js +0 -155
- package/dist/application/use-cases/verify-claim.js +0 -274
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/entities/task.js +0 -61
- package/dist/domain/services/codebase-detector.js +0 -91
- package/dist/error-handler.js +0 -251
- package/dist/infrastructure/completion/bash-completion.js +0 -25
- package/dist/infrastructure/completion/fish-completion.js +0 -22
- package/dist/infrastructure/completion/zsh-completion.js +0 -30
- package/dist/infrastructure/config/config-manager.js +0 -173
- package/dist/infrastructure/config/product-context.js +0 -67
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -163
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -119
- package/dist/infrastructure/context/registry-bootstrap.js +0 -99
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -113
- package/dist/infrastructure/cost/cost-router.js +0 -104
- package/dist/infrastructure/cost/spend-aggregator.js +0 -147
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
- package/dist/infrastructure/hooks/hook-mode.js +0 -71
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
- package/dist/infrastructure/orchestration/context-handoff.js +0 -144
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -77
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
- package/dist/infrastructure/orchestration/graphify-context.js +0 -254
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1091
- package/dist/infrastructure/orchestration/session-persistence.js +0 -258
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/protocols/verify-page.js +0 -983
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/security/permissions-config.js +0 -234
- package/dist/infrastructure/security/trust-scorer.js +0 -657
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
- package/dist/infrastructure/storage/project-thread-reader.js +0 -172
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/infrastructure/storage/task-repository.js +0 -206
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -126
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -198
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -578
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -116
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -387
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
- package/dist/presentation/cli/commands/setup-command.js +0 -307
- package/dist/presentation/cli/commands/status-command.js +0 -661
- package/dist/presentation/cli/commands/thread-command.js +0 -288
- package/dist/presentation/cli/commands/trust-command.js +0 -306
- package/dist/presentation/cli/commands/verify-command.js +0 -233
- package/dist/presentation/cli/error-display.js +0 -336
- package/dist/presentation/cli/repl.js +0 -797
- package/dist/presentation/cli/task-deliverable.js +0 -127
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -166
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 1.1 (verdict-trust punch list, 2026-06-09) · RE-RUN the tests, don't
|
|
3
|
+
* trust the agent's stdout.
|
|
4
|
+
*
|
|
5
|
+
* The gap this closes: `artifacts.tests` was regex-scraped from the AGENT's own
|
|
6
|
+
* captured stdout (`parseTestSummary(textAccum)`) and never re-executed — so
|
|
7
|
+
* "18/18 passed" was verified against the agent's own claim. A skeptic waves
|
|
8
|
+
* that off instantly. We already check FILE claims against ground truth
|
|
9
|
+
* (`git diff`); this extends the same "observe reality, don't trust the
|
|
10
|
+
* narration" rule to tests: codehere independently runs the project's test
|
|
11
|
+
* command on the real on-disk state and reads the REAL exit code.
|
|
12
|
+
*
|
|
13
|
+
* The authoritative signal is the EXIT CODE (a test runner exits non-zero on
|
|
14
|
+
* failure), not the scraped count — counts are display-only. So a report that
|
|
15
|
+
* claims "all tests pass" while the re-run exits non-zero becomes a caught lie
|
|
16
|
+
* (verify-claim flags it `broken` once `failed > 0`).
|
|
17
|
+
*
|
|
18
|
+
* Safe by construction: async (never blocks the daemon), timeout-bounded,
|
|
19
|
+
* opt-out via CODEHERE_RERUN_TESTS=0, and only ever runs the repo's OWN test
|
|
20
|
+
* script — strictly inside the trust boundary codehere already crosses by
|
|
21
|
+
* spawning agents that execute code in this repo.
|
|
22
|
+
*/
|
|
23
|
+
import { spawn } from 'node:child_process';
|
|
24
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { parseTestSummary } from '../../infrastructure/orchestration/artifacts-extractor.js';
|
|
27
|
+
const TIMEOUT_SETTING = Number(process.env.CODEHERE_RERUN_TIMEOUT_MS);
|
|
28
|
+
/**
|
|
29
|
+
* Whole milliseconds that a timer can hold; anything else falls back to two minutes. Node fires a NaN, fractional
|
|
30
|
+
* or over-2^31 timer after 1 ms, which would time out every run.
|
|
31
|
+
*/
|
|
32
|
+
const DEFAULT_TIMEOUT_MS = Number.isInteger(TIMEOUT_SETTING) && TIMEOUT_SETTING > 0 && TIMEOUT_SETTING <= 2 ** 31 - 1
|
|
33
|
+
? TIMEOUT_SETTING
|
|
34
|
+
: 120_000;
|
|
35
|
+
/** 500 → "0.5s", 120000 → "120s". */
|
|
36
|
+
const seconds = (ms) => `${Number.isInteger(ms / 1000) ? ms / 1000 : (ms / 1000).toFixed(1)}s`;
|
|
37
|
+
/** True when node's test runner printed a summary and every summary says 0 tests, or when go test found no test files. `node --test`
|
|
38
|
+
* exits 0 for a misnamed file (`calc.tests.js`), so the exit code alone reads as a pass.
|
|
39
|
+
* pytest already exits 5 in that case. */
|
|
40
|
+
export function reportedZeroTests(out) {
|
|
41
|
+
const counts = [...out.matchAll(/^[ \t]*(?:ℹ|#)[ \t]+tests[ \t]+(\d+)[ \t]*$/gm)].map((m) => Number(m[1]));
|
|
42
|
+
if (counts.length > 0)
|
|
43
|
+
return counts.every((n) => n === 0);
|
|
44
|
+
// `go test ./...` exits 0 and prints `[no test files]` for every package when the module has no _test.go files.
|
|
45
|
+
return /^\?[ \t]+.*\[no test files\]/m.test(out) && !/^(ok|FAIL|---)[ \t]/m.test(out);
|
|
46
|
+
}
|
|
47
|
+
/** True when the runner's own summary says tests were skipped and none passed or failed: node:test (`pass 0`,
|
|
48
|
+
* `skipped N`), pytest's final line, and the jest or vitest `Tests` line. A run like that tested nothing. */
|
|
49
|
+
export function reportedOnlySkipped(out) {
|
|
50
|
+
const last = (re) => [...out.matchAll(re)].pop();
|
|
51
|
+
const node = (k) => {
|
|
52
|
+
const m = last(new RegExp(`^[ \\t]*(?:ℹ|#)[ \\t]+${k}[ \\t]+(\\d+)[ \\t]*$`, 'gm'));
|
|
53
|
+
return m ? Number(m[1]) : null;
|
|
54
|
+
};
|
|
55
|
+
const pass = node('pass');
|
|
56
|
+
const skipped = node('skipped');
|
|
57
|
+
if (pass !== null && skipped !== null)
|
|
58
|
+
return pass === 0 && (node('fail') ?? 0) === 0 && skipped > 0;
|
|
59
|
+
const py = last(/^=+ (.*) in [\d.]+s(?: \([^)]*\))? =+$/gm);
|
|
60
|
+
if (py)
|
|
61
|
+
return /\b\d+ skipped\b/.test(py[1]) && !/\b\d+ (passed|failed|errors?|xpassed)\b/.test(py[1]);
|
|
62
|
+
const js = last(/^[ \t]*Tests:?[ \t]+(.*)$/gm);
|
|
63
|
+
if (js)
|
|
64
|
+
return /\b\d+ skipped\b/.test(js[1]) && !/\b\d+ (passed|failed)\b/.test(js[1]);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
/** True if `file` exists in `cwd` and its text contains `needle` (case-insensitive). */
|
|
68
|
+
function fileMentions(cwd, file, needle) {
|
|
69
|
+
try {
|
|
70
|
+
const p = join(cwd, file);
|
|
71
|
+
return existsSync(p) && readFileSync(p, 'utf8').toLowerCase().includes(needle.toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** True if `cwd` (top level, or a test/ or tests/ dir) holds a Node built-in
|
|
78
|
+
* test file — `*.test.{mjs,cjs,js}`. Lets codehere observe reality in a plain
|
|
79
|
+
* Node repo that uses `node --test` with no npm script or runner config (a
|
|
80
|
+
* common setup that previously left the verifier blind — DISCOVERED via a real
|
|
81
|
+
* V3 run, 2026-06-14). Bounded: top level + two conventional dirs, never a
|
|
82
|
+
* recursive walk. */
|
|
83
|
+
function hasNodeTestFiles(cwd) {
|
|
84
|
+
const isTest = (f) => /\.test\.(mjs|cjs|js)$/.test(f);
|
|
85
|
+
for (const dir of [cwd, join(cwd, 'test'), join(cwd, 'tests')]) {
|
|
86
|
+
try {
|
|
87
|
+
if (readdirSync(dir).some(isTest))
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
/* dir absent / unreadable → keep looking */
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Detect a runnable test command in `cwd`, across ecosystems — so "codehere
|
|
98
|
+
* re-runs your tests and catches a lying report" holds for Python/Go/Rust repos,
|
|
99
|
+
* not only npm. Conservative: each branch keys off an UNAMBIGUOUS marker; we
|
|
100
|
+
* return null (never guess) when there's nothing known to run.
|
|
101
|
+
*
|
|
102
|
+
* Precedence: explicit override → Node (npm test) → Go → Rust → Python (pytest)
|
|
103
|
+
* → a JS runner config without an npm script. First match wins.
|
|
104
|
+
*/
|
|
105
|
+
export function detectTestCommand(cwd) {
|
|
106
|
+
// 0. Explicit per-repo override — the escape hatch for anything we can't infer
|
|
107
|
+
// (monorepos, custom runners). e.g. CODEHERE_TEST_CMD="make test". Run via
|
|
108
|
+
// execFile (no shell), so args are whitespace-split — a quoted arg with
|
|
109
|
+
// spaces (CODEHERE_TEST_CMD='pytest -k "a b"') is NOT supported; use a
|
|
110
|
+
// wrapper script for that case.
|
|
111
|
+
const override = process.env.CODEHERE_TEST_CMD?.trim();
|
|
112
|
+
if (override) {
|
|
113
|
+
const [cmd, ...args] = override.split(/\s+/);
|
|
114
|
+
return { cmd, args, display: override };
|
|
115
|
+
}
|
|
116
|
+
// 1. Node — the project's declared `npm test` (dominant case + this repo).
|
|
117
|
+
const pj = join(cwd, 'package.json');
|
|
118
|
+
if (existsSync(pj)) {
|
|
119
|
+
try {
|
|
120
|
+
const json = JSON.parse(readFileSync(pj, 'utf8'));
|
|
121
|
+
const test = json.scripts?.test;
|
|
122
|
+
// npm's default placeholder script is not a real suite — skip it.
|
|
123
|
+
if (typeof test === 'string' && test.trim() && !/no test specified/i.test(test)) {
|
|
124
|
+
return { cmd: 'npm', args: ['test', '--silent'], display: 'npm test' };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* unreadable package.json → fall through to other ecosystems */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// 2. Go — go.mod present → the standard whole-module test run.
|
|
132
|
+
if (existsSync(join(cwd, 'go.mod'))) {
|
|
133
|
+
return { cmd: 'go', args: ['test', './...'], display: 'go test ./...' };
|
|
134
|
+
}
|
|
135
|
+
// 3. Rust — Cargo.toml → `cargo test`.
|
|
136
|
+
if (existsSync(join(cwd, 'Cargo.toml'))) {
|
|
137
|
+
return { cmd: 'cargo', args: ['test'], display: 'cargo test' };
|
|
138
|
+
}
|
|
139
|
+
// 4. Python — pytest is the dominant runner. Key off pytest-specific markers
|
|
140
|
+
// (conftest.py / pytest.ini are unambiguous). For pyproject/setup.cfg, match
|
|
141
|
+
// the actual pytest CONFIG SECTION header — not a bare "pytest" substring,
|
|
142
|
+
// which would fire on a comment or an unrelated dependency string and run
|
|
143
|
+
// pytest on a project that doesn't use it.
|
|
144
|
+
if (existsSync(join(cwd, 'pytest.ini')) ||
|
|
145
|
+
existsSync(join(cwd, 'conftest.py')) ||
|
|
146
|
+
fileMentions(cwd, 'pyproject.toml', '[tool.pytest') ||
|
|
147
|
+
fileMentions(cwd, 'setup.cfg', '[tool:pytest]')) {
|
|
148
|
+
return { cmd: 'pytest', args: ['-q'], display: 'pytest' };
|
|
149
|
+
}
|
|
150
|
+
// 5. A JS test runner with config but no npm `test` script.
|
|
151
|
+
//
|
|
152
|
+
// SECURITY (2026-08-08) · `--no-install` is load-bearing on THIS path above all
|
|
153
|
+
// others. Bare `npx vitest` falls back to the public registry when the runner
|
|
154
|
+
// is not installed locally, so codehere would DOWNLOAD a test runner and
|
|
155
|
+
// execute it against the user's repo — and then report the result as
|
|
156
|
+
// "verifier re-ran the suite", the strongest claim this product makes. The
|
|
157
|
+
// same fallback let `npx graphify-ts` fetch an unrelated stranger's package
|
|
158
|
+
// (fixed 86c7893); here the blast radius is the evidence itself.
|
|
159
|
+
//
|
|
160
|
+
// Refusing is also the semantically right answer: a runner the project does
|
|
161
|
+
// not have installed is not the project's test suite, so re-running with it
|
|
162
|
+
// proves nothing. When this returns nothing runnable, applyTestObservation
|
|
163
|
+
// records an honest `testsUnobserved` reason instead — which is exactly the
|
|
164
|
+
// abstention the eval harness scores 2/2 on.
|
|
165
|
+
if (existsSync(join(cwd, 'vitest.config.ts')) || existsSync(join(cwd, 'vitest.config.js'))) {
|
|
166
|
+
return { cmd: 'npx', args: ['--no-install', 'vitest', 'run'], display: 'npx vitest run' };
|
|
167
|
+
}
|
|
168
|
+
if (existsSync(join(cwd, 'jest.config.js')) || existsSync(join(cwd, 'jest.config.ts'))) {
|
|
169
|
+
return { cmd: 'npx', args: ['--no-install', 'jest'], display: 'npx jest' };
|
|
170
|
+
}
|
|
171
|
+
// 6. Node built-in test runner — `*.test.{mjs,cjs,js}` present but no npm
|
|
172
|
+
// script or runner config above. `node --test` self-discovers them. This is
|
|
173
|
+
// the plain-Node case that left codehere blind to test reality (the verifier
|
|
174
|
+
// can't catch a "tests pass" lie it never observed). Lowest precedence so a
|
|
175
|
+
// declared script/runner always wins.
|
|
176
|
+
if (hasNodeTestFiles(cwd)) {
|
|
177
|
+
return { cmd: 'node', args: ['--test'], display: 'node --test' };
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The one command that would make the suite runnable — returned as a STRING to
|
|
183
|
+
* SHOW the user, NEVER executed. The owner's call (2026-08-17): guide, never execute —
|
|
184
|
+
* auto-installing runs arbitrary postinstall code, which is exactly the posture
|
|
185
|
+
* a trust tool must not take (the same reason detectTestCommand keeps
|
|
186
|
+
* `--no-install`). Mirrors detectTestCommand's marker-probing: pick the
|
|
187
|
+
* ecosystem, name its install. Returns undefined when there's nothing to suggest.
|
|
188
|
+
*/
|
|
189
|
+
export function detectRemedy(cwd) {
|
|
190
|
+
const has = (f) => existsSync(join(cwd, f));
|
|
191
|
+
// Node — deps missing / runner not installed → install from the lockfile.
|
|
192
|
+
if (has('package.json')) {
|
|
193
|
+
if (has('pnpm-lock.yaml'))
|
|
194
|
+
return 'pnpm install';
|
|
195
|
+
if (has('yarn.lock'))
|
|
196
|
+
return 'yarn install';
|
|
197
|
+
if (has('package-lock.json'))
|
|
198
|
+
return 'npm ci';
|
|
199
|
+
return 'npm install';
|
|
200
|
+
}
|
|
201
|
+
// Python — set up a venv and install, then re-check.
|
|
202
|
+
if (has('requirements.txt')) {
|
|
203
|
+
return 'python -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt';
|
|
204
|
+
}
|
|
205
|
+
if (has('pyproject.toml') || has('setup.py') || has('setup.cfg')) {
|
|
206
|
+
return 'python -m venv .venv && . .venv/bin/activate && pip install -e .';
|
|
207
|
+
}
|
|
208
|
+
if (has('pytest.ini') || has('conftest.py'))
|
|
209
|
+
return 'pip install pytest';
|
|
210
|
+
if (has('go.mod'))
|
|
211
|
+
return 'go mod download';
|
|
212
|
+
if (has('Cargo.toml'))
|
|
213
|
+
return 'cargo fetch';
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Re-run the project's tests on the real on-disk state. Async + timeout-bounded.
|
|
218
|
+
* Resolves with `ran:false` + a reason when there's nothing to run or it's
|
|
219
|
+
* disabled — never throws.
|
|
220
|
+
*/
|
|
221
|
+
export function rerunTests(cwd) {
|
|
222
|
+
if (process.env.CODEHERE_RERUN_TESTS === '0') {
|
|
223
|
+
return Promise.resolve({ ran: false, kind: 'disabled', skippedReason: 'disabled (CODEHERE_RERUN_TESTS=0)' });
|
|
224
|
+
}
|
|
225
|
+
const detected = detectTestCommand(cwd);
|
|
226
|
+
if (!detected) {
|
|
227
|
+
return Promise.resolve({ ran: false, kind: 'no-command', skippedReason: 'no known test command in this project' });
|
|
228
|
+
}
|
|
229
|
+
return new Promise((resolve) => {
|
|
230
|
+
const start = Date.now();
|
|
231
|
+
let out = '';
|
|
232
|
+
let settled = false;
|
|
233
|
+
const done = (r) => {
|
|
234
|
+
if (settled)
|
|
235
|
+
return;
|
|
236
|
+
settled = true;
|
|
237
|
+
resolve(r);
|
|
238
|
+
};
|
|
239
|
+
let child;
|
|
240
|
+
try {
|
|
241
|
+
child = spawn(detected.cmd, detected.args, {
|
|
242
|
+
cwd,
|
|
243
|
+
env: { ...process.env, CI: '1', FORCE_COLOR: '0' },
|
|
244
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
245
|
+
// Own process group so a timeout can kill the whole TREE — `npm test`
|
|
246
|
+
// forks the real runner, and killing just the npm wrapper orphaned the
|
|
247
|
+
// grandchild, which kept running past the timeout (self-audit 2026-06-09).
|
|
248
|
+
detached: true,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
done({
|
|
253
|
+
ran: false,
|
|
254
|
+
kind: 'command-not-found',
|
|
255
|
+
command: detected.display,
|
|
256
|
+
skippedReason: `could not spawn test command: ${String(e).slice(0, 80)}`,
|
|
257
|
+
remedy: detectRemedy(cwd),
|
|
258
|
+
});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const timer = setTimeout(() => {
|
|
262
|
+
try {
|
|
263
|
+
// Negative pid = kill the whole process group (the detached tree), so the
|
|
264
|
+
// forked test runner dies too, not just the npm wrapper.
|
|
265
|
+
if (typeof child.pid === 'number')
|
|
266
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
267
|
+
else
|
|
268
|
+
child.kill('SIGKILL');
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
try {
|
|
272
|
+
child.kill('SIGKILL');
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
/* best-effort */
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
done({
|
|
279
|
+
ran: false,
|
|
280
|
+
kind: 'timed-out',
|
|
281
|
+
command: detected.display,
|
|
282
|
+
skippedReason: `test re-run timed out after ${seconds(DEFAULT_TIMEOUT_MS)}`,
|
|
283
|
+
});
|
|
284
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
285
|
+
const cap = (chunk) => {
|
|
286
|
+
out += String(chunk);
|
|
287
|
+
// bound memory — keep only the tail the parser reads
|
|
288
|
+
if (out.length > 200_000)
|
|
289
|
+
out = out.slice(-100_000);
|
|
290
|
+
};
|
|
291
|
+
child.stdout?.on('data', cap);
|
|
292
|
+
child.stderr?.on('data', cap);
|
|
293
|
+
child.on('error', (e) => {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
done({
|
|
296
|
+
ran: false,
|
|
297
|
+
kind: 'command-not-found',
|
|
298
|
+
command: detected.display,
|
|
299
|
+
skippedReason: `test command errored: ${String(e.message).slice(0, 80)}`,
|
|
300
|
+
remedy: detectRemedy(cwd),
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
child.on('close', (code) => {
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
const ec = code ?? -1;
|
|
306
|
+
const parsed = parseTestSummary(out);
|
|
307
|
+
// 126/127 = the wrapper launched but the runner/deps aren't installed —
|
|
308
|
+
// nothing was tested, so this is NOT a failing suite. Classify it as
|
|
309
|
+
// runner-missing (with the one-command remedy) so no surface renders a
|
|
310
|
+
// red measured-fail for an unrunnable suite.
|
|
311
|
+
const allSkipped = ec === 0 && reportedOnlySkipped(out);
|
|
312
|
+
const noTests = ec === 0 && (reportedZeroTests(out) || allSkipped);
|
|
313
|
+
const kind = ec === 126 || ec === 127 ? 'runner-missing' : ec === 0 && !noTests ? 'passed' : 'failed';
|
|
314
|
+
done({
|
|
315
|
+
ran: true,
|
|
316
|
+
kind,
|
|
317
|
+
command: detected.display,
|
|
318
|
+
exitCode: ec,
|
|
319
|
+
passed: parsed?.passed,
|
|
320
|
+
failed: parsed?.failed,
|
|
321
|
+
...(noTests ? { noTests: true } : {}),
|
|
322
|
+
...(allSkipped ? { allSkipped: true } : {}),
|
|
323
|
+
durationMs: Date.now() - start,
|
|
324
|
+
...(kind === 'runner-missing' ? { remedy: detectRemedy(cwd) } : {}),
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
//# sourceMappingURL=rerun-tests.js.map
|