forge-workflow 0.1.0-beta.5 → 0.1.0-beta.6
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/AGENTS.md +4 -0
- package/CHANGELOG.md +36 -0
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/bin/forge.js +12 -1
- package/docs/guides/MIGRATION.md +3 -3
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +4 -4
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/shepherd.md +63 -13
- package/lib/adapters/pr-state-adapter.js +15 -2
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +48 -18
- package/lib/commands/clean.js +57 -1
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +54 -6
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +720 -73
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +2 -0
- package/lib/commands/preflight.js +10 -2
- package/lib/commands/push.js +108 -6
- package/lib/commands/recall.js +95 -61
- package/lib/commands/release.js +23 -2
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +132 -4
- package/lib/commands/shepherd.js +578 -72
- package/lib/commands/ship.js +15 -69
- package/lib/commands/skill.js +8 -0
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +163 -4
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +155 -19
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-issues.js +78 -0
- package/lib/gate-events.js +98 -10
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/hook-renderer.js +29 -1
- package/lib/issue-render.js +19 -0
- package/lib/kernel/broker.js +723 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3322 -183
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +94 -27
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +59 -0
- package/lib/merge-rules.js +135 -17
- package/lib/npm-publish-workflow.js +233 -40
- package/lib/package-root.js +2 -0
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +0 -37
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1101 -625
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +121 -24
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +28 -19
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +144 -38
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-shepherd.js +17 -3
- package/lib/project-memory.js +145 -3
- package/lib/protected-state-authority.js +799 -4
- package/lib/protected-state-surfaces.js +181 -3
- package/lib/release-readiness.js +2 -3
- package/lib/review-adapter.js +65 -0
- package/lib/skills-sync.js +65 -32
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +8 -4
- package/scripts/commitlint.js +13 -15
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runtime.js +3 -2
- package/scripts/process-tree.js +14 -2
- package/scripts/protected-state-check.js +440 -17
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/test-full-suite.js +704 -18
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +95 -14
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/review/evals/scorecard.json +2 -2
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +19 -2
- package/skills/shepherd/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
package/lib/commands/ship.js
CHANGED
|
@@ -14,6 +14,7 @@ const path = require('node:path');
|
|
|
14
14
|
|
|
15
15
|
const { fireAndForget } = require('../pr-monitor/reconcile-executor');
|
|
16
16
|
const { getResolvedRuntimeGraph } = require('../core/runtime-graph');
|
|
17
|
+
const { resolveBaseRemote, resolveBaseBranch } = require('../base-remote');
|
|
17
18
|
|
|
18
19
|
const AUTO_SHEPHERD_RAIL = 'rail.auto_shepherd';
|
|
19
20
|
|
|
@@ -65,72 +66,6 @@ function getQuietGitExecOptions(cwd = process.cwd()) {
|
|
|
65
66
|
return { ...getGitExecOptions(cwd), stdio: 'pipe' };
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
function resolveRemoteHeadTarget(exec = execFileSync, cwd = process.cwd(), remoteName) {
|
|
69
|
-
try {
|
|
70
|
-
const symbolicRef = exec('git', ['symbolic-ref', `refs/remotes/${remoteName}/HEAD`], getQuietGitExecOptions(cwd)).trim();
|
|
71
|
-
if (!symbolicRef) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
exec('git', ['rev-parse', '--verify', symbolicRef], getQuietGitExecOptions(cwd));
|
|
75
|
-
return symbolicRef;
|
|
76
|
-
} catch (_error) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function remoteHasTrackingBase(exec = execFileSync, cwd = process.cwd(), remoteName) {
|
|
82
|
-
if (resolveRemoteHeadTarget(exec, cwd, remoteName)) {
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
for (const candidate of ['main', 'master']) {
|
|
87
|
-
try {
|
|
88
|
-
exec('git', ['rev-parse', '--verify', `refs/remotes/${remoteName}/${candidate}`], getQuietGitExecOptions(cwd));
|
|
89
|
-
return true;
|
|
90
|
-
} catch (_error) {
|
|
91
|
-
// Probe next candidate.
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function resolveBaseRemote(exec = execFileSync, cwd = process.cwd()) {
|
|
99
|
-
for (const candidate of ['upstream', 'origin']) {
|
|
100
|
-
try {
|
|
101
|
-
exec('git', ['remote', 'get-url', candidate], getQuietGitExecOptions(cwd));
|
|
102
|
-
if (remoteHasTrackingBase(exec, cwd, candidate)) {
|
|
103
|
-
return candidate;
|
|
104
|
-
}
|
|
105
|
-
} catch (_error) {
|
|
106
|
-
// Probe next candidate.
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return 'origin';
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function resolveBaseBranch(exec = execFileSync, cwd = process.cwd(), remoteName = resolveBaseRemote(exec, cwd)) {
|
|
114
|
-
const symbolicRef = resolveRemoteHeadTarget(exec, cwd, remoteName);
|
|
115
|
-
if (symbolicRef) {
|
|
116
|
-
const match = new RegExp(`^refs/remotes/${remoteName}/(.+)$`).exec(symbolicRef);
|
|
117
|
-
if (match && match[1]) {
|
|
118
|
-
return match[1];
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
for (const candidate of ['main', 'master']) {
|
|
123
|
-
try {
|
|
124
|
-
exec('git', ['rev-parse', '--verify', `refs/remotes/${remoteName}/${candidate}`], getQuietGitExecOptions(cwd));
|
|
125
|
-
return candidate;
|
|
126
|
-
} catch (_error) {
|
|
127
|
-
// Probe next candidate.
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return 'master';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
69
|
function refreshBaseReference(exec = execFileSync, cwd = process.cwd(), remoteName, baseBranch) {
|
|
135
70
|
exec(
|
|
136
71
|
'git',
|
|
@@ -457,10 +392,13 @@ function validatePRTitle(title) {
|
|
|
457
392
|
|
|
458
393
|
async function createPR(options) { // NOSONAR S3776
|
|
459
394
|
const { title, body, dryRun = false, exec = execFileSync, cwd = process.cwd() } = options;
|
|
395
|
+
const gh = options.githubContext?.bound
|
|
396
|
+
? (args, runOptions) => options.githubContext.runGh(args, runOptions)
|
|
397
|
+
: (args, runOptions) => exec('gh', args, runOptions);
|
|
460
398
|
const titleValidation = validatePRTitle(title);
|
|
461
399
|
if (!titleValidation.valid) return { success: false, error: titleValidation.error };
|
|
462
400
|
try {
|
|
463
|
-
|
|
401
|
+
gh(['--version'], { ...getGhCheckOptions(), cwd });
|
|
464
402
|
} catch (error) {
|
|
465
403
|
if (isCommandNotFound(error)) {
|
|
466
404
|
return { success: false, error: 'GitHub CLI (gh) not found. Install from: https://cli.github.com/' };
|
|
@@ -495,7 +433,7 @@ async function createPR(options) { // NOSONAR S3776
|
|
|
495
433
|
return { success: true, message: '[DRY RUN] Would create PR with title: ' + title, prUrl: 'https://github.com/owner/repo/pull/1' };
|
|
496
434
|
}
|
|
497
435
|
try {
|
|
498
|
-
const result =
|
|
436
|
+
const result = gh(['pr', 'create', '--title', title, '--body', body], getGitExecOptions(cwd));
|
|
499
437
|
const urlMatch = /https:\/\/github\.com\/[^\s]+/.exec(result);
|
|
500
438
|
const prUrl = urlMatch ? urlMatch[0] : null;
|
|
501
439
|
const numberMatch = /\/pull\/(\d+)/.exec(result);
|
|
@@ -585,7 +523,10 @@ async function executeShip(options) {
|
|
|
585
523
|
testScenarios,
|
|
586
524
|
coverage,
|
|
587
525
|
});
|
|
588
|
-
const result = await createPr({
|
|
526
|
+
const result = await createPr({
|
|
527
|
+
title, body: prBody, dryRun,
|
|
528
|
+
exec: options.exec, cwd: options.cwd || projectRoot, githubContext: options.githubContext,
|
|
529
|
+
});
|
|
589
530
|
if (!result.success) return result;
|
|
590
531
|
maybeTriggerShepherdAfterShip({ dryRun, projectRoot, fireAndForget: trigger });
|
|
591
532
|
return {
|
|
@@ -601,6 +542,7 @@ async function executeShip(options) {
|
|
|
601
542
|
|
|
602
543
|
module.exports = {
|
|
603
544
|
name: 'ship',
|
|
545
|
+
githubAuth: true,
|
|
604
546
|
description: 'Create a pull request from validated feature work',
|
|
605
547
|
handler: async (args, flags = {}, projectRoot = process.cwd(), deps = {}) => {
|
|
606
548
|
const result = await executeShip({
|
|
@@ -609,6 +551,10 @@ module.exports = {
|
|
|
609
551
|
dryRun: Boolean(flags.dryRun || flags['--dry-run']),
|
|
610
552
|
projectRoot,
|
|
611
553
|
fireAndForget: deps.fireAndForget,
|
|
554
|
+
exec: deps.exec,
|
|
555
|
+
cwd: deps.cwd || projectRoot,
|
|
556
|
+
githubContext: deps.githubContext,
|
|
557
|
+
createPr: deps.createPr,
|
|
612
558
|
});
|
|
613
559
|
if (!result.success) {
|
|
614
560
|
return result;
|
package/lib/commands/skill.js
CHANGED
|
@@ -23,6 +23,7 @@ const { runBehavioralEvaluation } = require('../../scripts/lib/behavioral-eval-r
|
|
|
23
23
|
const { resolveBehavioralEvaluation } = require('../../scripts/lib/behavioral-eval-runtime');
|
|
24
24
|
const { loadPromotionEvidence } = require('../../scripts/lib/promotion-evidence-loader');
|
|
25
25
|
const { scorePromotion } = require('../../scripts/lib/promotion-scorecard');
|
|
26
|
+
const { createGithubContext } = require('../github-context');
|
|
26
27
|
|
|
27
28
|
const BEHAVIORAL_TIERS = Object.freeze([30, 100, 300]);
|
|
28
29
|
const BEHAVIORAL_TIER_VALUES = new Set(BEHAVIORAL_TIERS.map(String));
|
|
@@ -158,12 +159,19 @@ async function handleFullEval(rest, flags, projectRoot, opts) {
|
|
|
158
159
|
let behavioralOptions = opts.behavioralOptions || {};
|
|
159
160
|
if (!opts.runBehavioralEvaluation || opts.resolveBehavioralEvaluation) {
|
|
160
161
|
const resolver = opts.resolveBehavioralEvaluation || resolveBehavioralEvaluation;
|
|
162
|
+
let githubContext;
|
|
163
|
+
try {
|
|
164
|
+
githubContext = (opts.prepareGithubContext || createGithubContext)(projectRoot);
|
|
165
|
+
} catch {
|
|
166
|
+
return { success: false, error: 'Unable to prepare GitHub account context. Run forge github status for guidance.' };
|
|
167
|
+
}
|
|
161
168
|
const resolved = await resolver({
|
|
162
169
|
projectRoot,
|
|
163
170
|
skillName: name,
|
|
164
171
|
skillPath: path.join(ctx.skillsDir, name, 'SKILL.md'),
|
|
165
172
|
tier,
|
|
166
173
|
env: opts.env || process.env,
|
|
174
|
+
...(githubContext.bound ? { deps: { runGh: githubContext.runGh } } : {}),
|
|
167
175
|
});
|
|
168
176
|
if (!resolved.ok) return formatResult(scoreResult(resolved.result));
|
|
169
177
|
behavioralOptions = { ...resolved.options, ...behavioralOptions };
|
package/lib/commands/team.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
const { existsSync } = require('node:fs');
|
|
2
2
|
const { execFileSync } = require('node:child_process');
|
|
3
3
|
const path = require('node:path');
|
|
4
|
-
const { getPackageRoot } = require('../package-root');
|
|
4
|
+
const { getPackageRoot, isCompiledBinary } = require('../package-root');
|
|
5
|
+
const { firstPositionalIndex, stripGlobalFlags } = require('../global-flags');
|
|
6
|
+
|
|
7
|
+
function teamArguments(args) {
|
|
8
|
+
const subcommandIndex = firstPositionalIndex(args);
|
|
9
|
+
if (subcommandIndex === -1) return stripGlobalFlags(args);
|
|
10
|
+
const result = stripGlobalFlags(args.slice(0, subcommandIndex));
|
|
11
|
+
// Path is routing metadata anywhere; other flags after the subcommand belong to Team.
|
|
12
|
+
for (let index = subcommandIndex; index < args.length; index++) {
|
|
13
|
+
const arg = args[index];
|
|
14
|
+
if (arg.startsWith('--path=')) continue;
|
|
15
|
+
if (arg === '-p' || arg === '--path') {
|
|
16
|
+
if (args[index + 1] !== undefined && !args[index + 1].startsWith('-')) index++;
|
|
17
|
+
} else {
|
|
18
|
+
result.push(arg);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
5
23
|
|
|
6
24
|
// Resolve bash binary from fixed, known locations only (no PATH search).
|
|
7
25
|
// SonarCloud S4036: PATH must not be searched for OS commands.
|
|
@@ -22,28 +40,49 @@ function _resolveBash() {
|
|
|
22
40
|
return '/usr/bin/bash';
|
|
23
41
|
}
|
|
24
42
|
|
|
25
|
-
function handleTeam(args) {
|
|
43
|
+
function handleTeam(args, projectRoot = process.cwd(), deps = {}) {
|
|
26
44
|
// Route through the asset root so `forge team` finds the script in BOTH channels:
|
|
27
45
|
// the on-disk package (npm/npx) and a compiled binary (extracted embed dir).
|
|
28
|
-
const
|
|
46
|
+
const packageRoot = getPackageRoot();
|
|
47
|
+
const scriptPath = path.join(packageRoot, 'scripts', 'forge-team', 'index.sh');
|
|
29
48
|
const bashPath = _resolveBash();
|
|
49
|
+
const runFile = deps.execFileSync || execFileSync;
|
|
50
|
+
const options = { stdio: 'inherit' };
|
|
51
|
+
if (deps.githubContext?.bound) {
|
|
52
|
+
// Bash also runs Git/Forge/helpers. Only the re-entering gh bridge may resolve credentials.
|
|
53
|
+
const compiled = (deps.isCompiledBinary || isCompiledBinary)();
|
|
54
|
+
const env = { ...(deps.env || process.env) };
|
|
55
|
+
for (const key of Object.keys(env)) {
|
|
56
|
+
if (['gh_token', 'github_token', 'gh_host'].includes(key.toLowerCase())) delete env[key];
|
|
57
|
+
}
|
|
58
|
+
options.cwd = projectRoot;
|
|
59
|
+
options.env = {
|
|
60
|
+
...env,
|
|
61
|
+
GH_CMD: path.join(packageRoot, 'scripts', 'github-context-bridge.sh').replaceAll('\\', '/'),
|
|
62
|
+
FORGE_GITHUB_RUNTIME: process.execPath.replaceAll('\\', '/'),
|
|
63
|
+
FORGE_GITHUB_ENTRY: compiled ? '' : path.join(packageRoot, 'bin', 'forge.js').replaceAll('\\', '/'),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
30
66
|
|
|
31
67
|
try {
|
|
32
|
-
|
|
33
|
-
stdio: 'inherit',
|
|
34
|
-
});
|
|
68
|
+
runFile(bashPath, [scriptPath, ...teamArguments(args)], options);
|
|
35
69
|
} catch (err) {
|
|
36
70
|
process.exit(err.status || 1);
|
|
37
71
|
}
|
|
38
72
|
}
|
|
39
73
|
|
|
40
|
-
async function handler(args =
|
|
41
|
-
handleTeam(args);
|
|
74
|
+
async function handler(args, _flags, projectRoot, deps = {}) {
|
|
75
|
+
handleTeam(args || [], projectRoot, deps);
|
|
42
76
|
return { success: true };
|
|
43
77
|
}
|
|
44
78
|
|
|
45
79
|
module.exports = {
|
|
46
80
|
name: 'team',
|
|
81
|
+
githubAuth: (args = []) => {
|
|
82
|
+
const teamArgs = teamArguments(args);
|
|
83
|
+
return ['add', 'verify', 'sync', 'claim'].includes(teamArgs[0])
|
|
84
|
+
|| (teamArgs[0] === 'workload' && teamArgs.slice(1).some(arg => arg.replace(/\r$/, '') === '--me'));
|
|
85
|
+
},
|
|
47
86
|
description: 'Team coordination workflows and sync utilities',
|
|
48
87
|
handler,
|
|
49
88
|
handleTeam
|
package/lib/commands/test.js
CHANGED
|
@@ -32,6 +32,9 @@ const DIRECT_TEST_CANDIDATES = Object.freeze({
|
|
|
32
32
|
'test/cli-flags.test.js',
|
|
33
33
|
'test/forge-cli-registry.test.js',
|
|
34
34
|
'test/setup-runtime-flags.test.js',
|
|
35
|
+
'test/github-launcher.test.js',
|
|
36
|
+
'test/integration/github-account-context.test.js',
|
|
37
|
+
'test/structural/github-account-public-surface.test.js',
|
|
35
38
|
],
|
|
36
39
|
// The second CLI surface (command descriptions + shepherd wiring). Without an
|
|
37
40
|
// entry here it resolves to zero tests and pushes fall back to the full suite.
|
|
@@ -47,15 +50,143 @@ const DIRECT_TEST_CANDIDATES = Object.freeze({
|
|
|
47
50
|
'test/doc-assertions.test.js',
|
|
48
51
|
'test/scripts/doc-asserting-tests.test.js',
|
|
49
52
|
],
|
|
53
|
+
// The suites that assert the repo .gitignore's actual content. The path is
|
|
54
|
+
// already known to the classifier, so without candidates here it resolves
|
|
55
|
+
// zero tests and a one-line ignore addition pays the full-suite lane.
|
|
56
|
+
'.gitignore': [
|
|
57
|
+
'test/check-forge-token.test.js',
|
|
58
|
+
'test/cleanup/dropped-agent-config.test.js',
|
|
59
|
+
'test/coverage-config.test.js',
|
|
60
|
+
'test/eval/eval-history.test.js',
|
|
61
|
+
'test/freshness-token.test.js',
|
|
62
|
+
'test/mutation-config.test.js',
|
|
63
|
+
'test/test-dashboard.test.js',
|
|
64
|
+
],
|
|
50
65
|
'lib/lefthook-check.js': ['test/lefthook-check.test.js', 'test/runtime-health.test.js'],
|
|
51
66
|
'lib/runtime-health.js': ['test/runtime-health.test.js'],
|
|
52
67
|
// skill-eval hosts the accuracy-lint detectors (auditCommandDocumentation /
|
|
53
68
|
// auditRouterPrecision), so edits there must also run their detector suite.
|
|
54
69
|
'lib/skill-eval.js': ['test/skill-eval.test.js', 'test/skill-accuracy.test.js'],
|
|
70
|
+
// Schema is shared by every kernel authority; keep it on the full-suite lane.
|
|
71
|
+
'lib/kernel/schema.js': [],
|
|
72
|
+
// Migrations have consumers across the kernel; an empty mapping deliberately
|
|
73
|
+
// selects the full-suite lane, matching the shared SQLite driver.
|
|
74
|
+
'lib/kernel/migrations.js': [],
|
|
75
|
+
'lib/commands/shepherd.js': [
|
|
76
|
+
'test/commands/shepherd.test.js',
|
|
77
|
+
'test/commands/github-route-matrix.test.js',
|
|
78
|
+
'test/pr-monitor/arm-on-push.test.js',
|
|
79
|
+
'test/pr-monitor/shepherd-watch.test.js',
|
|
80
|
+
'test/pr-monitor/watch-owner-cutover.test.js',
|
|
81
|
+
'test/pr-monitor/watch-owner-launch-contention.test.js',
|
|
82
|
+
'test/pr-monitor/watch-owner-launch-gate.test.js',
|
|
83
|
+
],
|
|
84
|
+
'lib/commands/github.js': [
|
|
85
|
+
'test/commands/github.test.js',
|
|
86
|
+
'test/github-launcher.test.js',
|
|
87
|
+
'test/integration/github-account-context.test.js',
|
|
88
|
+
],
|
|
89
|
+
'lib/github-context.js': ['test/github-context.test.js', 'test/integration/github-account-context.test.js'],
|
|
90
|
+
'lib/commands/_registry.js': [
|
|
91
|
+
'test/commands/_registry.test.js',
|
|
92
|
+
'test/commands/_registry-github-context.test.js',
|
|
93
|
+
],
|
|
94
|
+
'lib/commands/pr.js': ['test/commands/pr.test.js', 'test/commands/github-route-matrix.test.js'],
|
|
95
|
+
'lib/commands/ship.js': ['test/commands/ship.test.js', 'test/commands/github-route-matrix.test.js'],
|
|
96
|
+
'lib/commands/merge.js': ['test/commands/merge.test.js', 'test/commands/github-route-matrix.test.js'],
|
|
97
|
+
'lib/commands/team.js': ['test/commands/team.test.js', 'test/commands/github-route-matrix.test.js'],
|
|
98
|
+
'lib/commands/clean.js': ['test/commands/clean.test.js', 'test/commands/github-route-matrix.test.js'],
|
|
99
|
+
'scripts/github-context-bridge.sh': ['test/commands/github-route-matrix.test.js'],
|
|
100
|
+
'lib/commands/push.js': ['test/commands/push.test.js', 'test/commands/github-indirect-routes.test.js'],
|
|
101
|
+
'lib/commands/hooks.js': ['test/commands/hooks.test.js', 'test/hooks-session-start.test.js', 'test/commands/github-indirect-routes.test.js'],
|
|
102
|
+
'lib/commands/serve.js': ['test/commands/serve.test.js', 'test/commands/github-indirect-routes.test.js'],
|
|
103
|
+
'lib/commands/skill.js': ['test/commands/skill.test.js', 'test/commands/github-indirect-routes.test.js'],
|
|
104
|
+
'scripts/lib/behavioral-eval-runtime.js': ['test/eval/behavioral-eval-runtime.test.js', 'test/commands/github-indirect-routes.test.js'],
|
|
105
|
+
'web/dashboard/generate-snapshot.mjs': ['test/commands/github-indirect-routes.test.js'],
|
|
106
|
+
// The shared SQLite driver has more consumers than a bounded direct list can
|
|
107
|
+
// safely represent; its empty mapping deliberately selects the full-suite lane.
|
|
108
|
+
'lib/kernel/sqlite-driver.js': [],
|
|
109
|
+
'lib/pr-monitor/watch-owner.js': [
|
|
110
|
+
'test/pr-monitor/watch-owner.test.js',
|
|
111
|
+
'test/kernel/watch-owner-transaction.test.js',
|
|
112
|
+
],
|
|
113
|
+
'lib/pr-monitor/reconcile.js': [
|
|
114
|
+
'test/pr-monitor/reconcile.test.js',
|
|
115
|
+
'test/pr-monitor/reconcile-owner-authority.test.js',
|
|
116
|
+
],
|
|
117
|
+
'lib/pr-monitor/reconcile-executor.js': [
|
|
118
|
+
'test/commands/github-indirect-routes.test.js',
|
|
119
|
+
'test/pr-monitor/auto-trigger-containment.test.js',
|
|
120
|
+
'test/pr-monitor/reconcile-executor-owner-authority.test.js',
|
|
121
|
+
'test/pr-monitor/reconcile-executor.test.js',
|
|
122
|
+
'test/pr-monitor/reconcile-daemon-owner-authority.test.js',
|
|
123
|
+
'test/pr-monitor/reconcile-migration-gate.test.js',
|
|
124
|
+
'test/pr-monitor/watch-owner-launch-contention.test.js',
|
|
125
|
+
'test/windows-hide-background-spawns.test.js',
|
|
126
|
+
],
|
|
127
|
+
'lib/pr-monitor/watch.js': [
|
|
128
|
+
'test/pr-monitor/watch-owner-cutover.test.js',
|
|
129
|
+
'test/pr-monitor/watch.test.js',
|
|
130
|
+
],
|
|
131
|
+
'lib/pr-monitor/watch-lifecycle.js': [
|
|
132
|
+
'test/commands/github-indirect-routes.test.js',
|
|
133
|
+
'test/pr-monitor/watch-lifecycle.test.js',
|
|
134
|
+
'test/pr-monitor/watch-owner-launch-contention.test.js',
|
|
135
|
+
'test/pr-monitor/watch-owner-launch-gate.test.js',
|
|
136
|
+
],
|
|
137
|
+
'lib/pr-monitor/shepherd-lease.js': [
|
|
138
|
+
'test/pr-monitor/shepherd-lease.test.js',
|
|
139
|
+
'test/pr-monitor/shepherd-lease-election-only.test.js',
|
|
140
|
+
],
|
|
141
|
+
'lib/pr-monitor/monitor.js': [
|
|
142
|
+
'test/pr-monitor/monitor.test.js',
|
|
143
|
+
'test/pr-monitor/monitor-owner-signal.test.js',
|
|
144
|
+
],
|
|
55
145
|
'lib/upgrade-safety.js': ['test/commands/upgrade.test.js'],
|
|
56
146
|
'scripts/test.js': ['test/scripts/test-runner.test.js'],
|
|
147
|
+
// `bun.lock` is deliberately absent: a dependency change can break anything,
|
|
148
|
+
// so it stays on the full-suite lane.
|
|
149
|
+
// lefthook.yml is the repository's hook wiring, so this must also cover the
|
|
150
|
+
// suites that assert the individual pre-commit/pre-push commands exist.
|
|
151
|
+
'lefthook.yml': [
|
|
152
|
+
'test/lefthook-check.test.js',
|
|
153
|
+
'test/lefthook-wiring.test.js',
|
|
154
|
+
'test/commands/lefthook-user-config.test.js',
|
|
155
|
+
'test/branch-protection.test.js',
|
|
156
|
+
'test/commitlint.test.js',
|
|
157
|
+
'test/cross-platform-install.test.js',
|
|
158
|
+
'test/sync-d20-audit-script.test.js',
|
|
159
|
+
],
|
|
160
|
+
'eslint.config.js': ['test/lint-script.test.js'],
|
|
161
|
+
'.coderabbit.yaml': ['test/coderabbit-config.test.js'],
|
|
162
|
+
'.claude-plugin/marketplace.json': [
|
|
163
|
+
'test/activation/plugin-scaffold.test.js',
|
|
164
|
+
'test/plugin-catalog.test.js',
|
|
165
|
+
],
|
|
57
166
|
});
|
|
58
167
|
|
|
168
|
+
// Shared authority modules must run the full suite even when another changed file
|
|
169
|
+
// resolves a targeted test. Keep this signal separate from test candidates: an
|
|
170
|
+
// empty candidate list is not enough once a mapped consumer is also changed.
|
|
171
|
+
const FULL_SUITE_REQUIRED_FILES = new Set([
|
|
172
|
+
'lib/kernel/schema.js',
|
|
173
|
+
'lib/kernel/migrations.js',
|
|
174
|
+
'lib/kernel/sqlite-driver.js',
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
// Directory-level mappings for source trees that own a dedicated suite. Without
|
|
178
|
+
// these a change under any of them resolves to zero tests and forces the full
|
|
179
|
+
// suite (see hasZeroResolvedTests in scripts/test.js).
|
|
180
|
+
const PREFIX_TEST_TARGETS = Object.freeze([
|
|
181
|
+
['validation/', ['test/validation/risk-manifest.test.js', 'test/validation/risk-manifest-generator.test.js']],
|
|
182
|
+
// `eval/corpus/` must come first: the corpus hash and manifest checks live in
|
|
183
|
+
// immutable-corpus, and eval-sets only reads `eval/commands/`.
|
|
184
|
+
['eval/corpus/', ['test/eval/immutable-corpus.test.js']],
|
|
185
|
+
['eval/', ['test/eval/eval-sets.test.js']],
|
|
186
|
+
['rules/', ['test/rules-sync.test.js', 'test/skill-dispatch-parity.test.js']],
|
|
187
|
+
['plugin/', ['test/activation/plugin-scaffold.test.js', 'test/plugin-catalog.test.js', 'test/release-readiness.test.js']],
|
|
188
|
+
]);
|
|
189
|
+
|
|
59
190
|
// Skill sources (canonical `skills/**` and the committed `.agents/skills/**` mirror)
|
|
60
191
|
// map to the fast skill suite. This is what keeps a skills-only PR on the targeted
|
|
61
192
|
// lane instead of the full ~1500-test suite. `skills-sync-drift` guards the mirror.
|
|
@@ -176,7 +307,9 @@ function getChangedFiles(execFileSync, options = {}) {
|
|
|
176
307
|
* @returns {string[]} Array of test file paths (e.g. ['test/foo.test.js'])
|
|
177
308
|
*/
|
|
178
309
|
function getAffectedTestFiles(projectRoot, execFileSync, fs = defaultFs, options = {}) {
|
|
179
|
-
const changedFiles =
|
|
310
|
+
const changedFiles = Array.isArray(options.changedFiles)
|
|
311
|
+
? options.changedFiles
|
|
312
|
+
: getChangedFiles(execFileSync, options);
|
|
180
313
|
const testFiles = new Set();
|
|
181
314
|
for (const file of changedFiles) {
|
|
182
315
|
for (const candidate of getTestCandidatesForChangedFile(file)) {
|
|
@@ -215,10 +348,24 @@ function getTestCandidatesForChangedFile(file) {
|
|
|
215
348
|
return [...SKILL_TEST_TARGETS];
|
|
216
349
|
}
|
|
217
350
|
|
|
351
|
+
// Maintainer-only contributor skills. They document the AGENTS.md product /
|
|
352
|
+
// maintainer split, so the docs-bleed gate is the suite that can catch a
|
|
353
|
+
// contradiction between them.
|
|
354
|
+
if (file.startsWith('.forge/contributor-skills/')) {
|
|
355
|
+
return ['test/structural/agents-docs-bleed.test.js'];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const [prefix, targets] of PREFIX_TEST_TARGETS) {
|
|
359
|
+
if (file.startsWith(prefix)) {
|
|
360
|
+
return [...targets];
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
218
364
|
if (file === 'README.md'
|
|
219
365
|
|| file === 'QUICKSTART.md'
|
|
220
366
|
|| file === 'CHANGELOG.md'
|
|
221
367
|
|| file === 'AGENTS.md'
|
|
368
|
+
|| file === 'CODING_STANDARDS.md'
|
|
222
369
|
|| file === 'DEVELOPMENT.md'
|
|
223
370
|
|| file === 'docs/INDEX.md'
|
|
224
371
|
|| file === 'docs/PROJECT_DESIGN.md'
|
|
@@ -226,7 +373,9 @@ function getTestCandidatesForChangedFile(file) {
|
|
|
226
373
|
|| file.startsWith('docs/guides/')
|
|
227
374
|
|| file.startsWith('docs/reference/')
|
|
228
375
|
|| file.startsWith('docs/work/')) {
|
|
229
|
-
return
|
|
376
|
+
return file === 'AGENTS.md'
|
|
377
|
+
? ['test/docs-consistency.test.js', 'test/structural/agents-docs-bleed.test.js']
|
|
378
|
+
: ['test/docs-consistency.test.js'];
|
|
230
379
|
}
|
|
231
380
|
|
|
232
381
|
if (file.startsWith('lib/') && file.endsWith('.js')) {
|
|
@@ -270,10 +419,15 @@ function getTestCandidatesForChangedFile(file) {
|
|
|
270
419
|
return [];
|
|
271
420
|
}
|
|
272
421
|
|
|
422
|
+
function isFullSuiteRequiredFile(file) {
|
|
423
|
+
return FULL_SUITE_REQUIRED_FILES.has(file);
|
|
424
|
+
}
|
|
425
|
+
|
|
273
426
|
module.exports = {
|
|
274
427
|
getChangedFiles,
|
|
275
428
|
getAffectedTestFiles,
|
|
276
429
|
getTestCandidatesForChangedFile,
|
|
430
|
+
isFullSuiteRequiredFile,
|
|
277
431
|
name: 'test',
|
|
278
432
|
description: 'Run tests with smart defaults (timeout, affected-only)',
|
|
279
433
|
usage: 'forge test [--affected]',
|
|
@@ -318,10 +472,15 @@ module.exports = {
|
|
|
318
472
|
|
|
319
473
|
// 4. --affected flag: find changed test files
|
|
320
474
|
if (flags['--affected'] || flags.affected) {
|
|
321
|
-
const
|
|
475
|
+
const diffOptions = {
|
|
322
476
|
sinceUpstream: flags.sinceUpstream || flags['--since-upstream'],
|
|
477
|
+
};
|
|
478
|
+
const changedFiles = getChangedFiles(execFileSync, diffOptions);
|
|
479
|
+
const affectedTests = getAffectedTestFiles(projectRoot, execFileSync, fs, {
|
|
480
|
+
...diffOptions,
|
|
481
|
+
changedFiles,
|
|
323
482
|
});
|
|
324
|
-
if (affectedTests.length > 0) {
|
|
483
|
+
if (!changedFiles.some(isFullSuiteRequiredFile) && affectedTests.length > 0) {
|
|
325
484
|
testArgs = ['run', 'test', ...affectedTests];
|
|
326
485
|
}
|
|
327
486
|
// If no affected tests found, fall back to running all tests
|
package/lib/commands/validate.js
CHANGED
|
@@ -44,10 +44,12 @@ const CONFLICT_DIVIDER_PATTERN = /^=======$/;
|
|
|
44
44
|
const CONFLICT_END_PATTERN = /^>>>>>>>.*$/;
|
|
45
45
|
const GIT_CONFLICT_SCAN_MAX_BUFFER = 16 * 1024 * 1024;
|
|
46
46
|
const VALIDATION_COMMAND_TIMEOUT_MS = 600000;
|
|
47
|
-
|
|
47
|
+
// Match the full-suite runner's measured headroom; its verbose output exceeds 1 MiB.
|
|
48
|
+
const FULL_SUITE_TIMEOUT_MS = 25 * 60 * 1000;
|
|
49
|
+
const FULL_SUITE_MAX_BUFFER = 16 * 1024 * 1024;
|
|
48
50
|
|
|
49
|
-
function validationTimeoutMessage(label) {
|
|
50
|
-
return `${label} timed out after ${
|
|
51
|
+
function validationTimeoutMessage(label, timeoutMs = VALIDATION_COMMAND_TIMEOUT_MS) {
|
|
52
|
+
return `${label} timed out after ${Math.round(timeoutMs / 60000)} minutes`;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
function listConflictScanFiles(rootDir) {
|
|
@@ -80,8 +82,15 @@ function shouldSkipConflictScanPath(relativePath) {
|
|
|
80
82
|
));
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
/** Build bounded process options, with extra headroom only for Forge's full suite. */
|
|
86
|
+
function getExecOptions(rootDir = process.cwd(), fullSuite = false) {
|
|
87
|
+
const options = {
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
cwd: rootDir,
|
|
90
|
+
timeout: fullSuite ? FULL_SUITE_TIMEOUT_MS : VALIDATION_COMMAND_TIMEOUT_MS,
|
|
91
|
+
};
|
|
92
|
+
if (fullSuite) options.maxBuffer = FULL_SUITE_MAX_BUFFER;
|
|
93
|
+
return options;
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
const ERROR_PATTERNS = {
|
|
@@ -130,7 +139,7 @@ function parseNumber(match, index = 1, defaultValue = 0) {
|
|
|
130
139
|
*/
|
|
131
140
|
function getCheckStatus(check) {
|
|
132
141
|
if (!check) return null;
|
|
133
|
-
if (check.skipped) return 'SKIPPED';
|
|
142
|
+
if (check.skipped === true) return 'SKIPPED';
|
|
134
143
|
return check.success ? 'PASS' : 'FAIL';
|
|
135
144
|
}
|
|
136
145
|
|
|
@@ -343,17 +352,18 @@ async function runTypeCheck() {
|
|
|
343
352
|
* Run ESLint
|
|
344
353
|
* Executes eslint . to check code quality
|
|
345
354
|
*
|
|
355
|
+
* @param {typeof execFileSync} exec - Synchronous process runner
|
|
346
356
|
* @returns {Promise<{success: boolean, duration: number, warnings?: number, errors?: number, message?: string}>} Lint result
|
|
347
357
|
* @example
|
|
348
358
|
* const result = await runLint();
|
|
349
359
|
* console.log(`Warnings: ${result.warnings}, Errors: ${result.errors}`);
|
|
350
360
|
*/
|
|
351
|
-
async function runLint() {
|
|
361
|
+
async function runLint(exec = execFileSync) {
|
|
352
362
|
const startTime = Date.now();
|
|
353
363
|
|
|
354
364
|
try {
|
|
355
365
|
// Run eslint with no output (exit code determines success)
|
|
356
|
-
|
|
366
|
+
exec('eslint', ['.'], getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
357
367
|
|
|
358
368
|
return {
|
|
359
369
|
success: true,
|
|
@@ -556,7 +566,7 @@ async function runSecurityScan() { // NOSONAR S3776
|
|
|
556
566
|
|
|
557
567
|
/**
|
|
558
568
|
* Run all tests
|
|
559
|
-
*
|
|
569
|
+
* Uses Forge's full-suite runner when present, with raw Bun as an external-repository fallback
|
|
560
570
|
*
|
|
561
571
|
* @returns {Promise<{success: boolean, duration: number, passed: number, failed: number, total: number, message?: string}>} Test execution result
|
|
562
572
|
* @example
|
|
@@ -565,6 +575,22 @@ async function runSecurityScan() { // NOSONAR S3776
|
|
|
565
575
|
*/
|
|
566
576
|
function parseTestCounts(output) {
|
|
567
577
|
const text = String(output || '');
|
|
578
|
+
const aggregateMatches = [...text.matchAll(
|
|
579
|
+
/Full suite aggregate: status=(PASS|FAIL|INCOMPLETE) tests=(\d+) assertions=\d+ passed=(\d+) failed=(\d+) errors=(\d+) skipped=(\d+)/g,
|
|
580
|
+
)]; // NOSONAR S5852 - fixed tokens and bounded numeric fields
|
|
581
|
+
const aggregate = aggregateMatches.at(-1);
|
|
582
|
+
if (aggregate) {
|
|
583
|
+
const total = parseNumber(aggregate, 2);
|
|
584
|
+
return {
|
|
585
|
+
status: aggregate[1],
|
|
586
|
+
passed: parseNumber(aggregate, 3),
|
|
587
|
+
failed: parseNumber(aggregate, 4),
|
|
588
|
+
errors: parseNumber(aggregate, 5),
|
|
589
|
+
skipped: parseNumber(aggregate, 6),
|
|
590
|
+
total,
|
|
591
|
+
testsFound: total > 0,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
568
594
|
const passed = parseNumber(/(\d+) pass/.exec(text)); // NOSONAR S5852 - bounded \d+ pattern
|
|
569
595
|
const failed = parseNumber(/(\d+) fail/.exec(text)); // NOSONAR S5852 - bounded \d+ pattern
|
|
570
596
|
const skipped = parseNumber(/(\d+) skip/.exec(text)); // NOSONAR S5852 - bounded \d+ pattern
|
|
@@ -573,13 +599,31 @@ function parseTestCounts(output) {
|
|
|
573
599
|
return { passed, failed, skipped, total, testsFound: total > 0 };
|
|
574
600
|
}
|
|
575
601
|
|
|
576
|
-
|
|
602
|
+
function isForgeCheckout(rootDir) {
|
|
603
|
+
try {
|
|
604
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'));
|
|
605
|
+
return manifest.name === 'forge-workflow'
|
|
606
|
+
&& manifest.bin?.forge === 'bin/forge.js'
|
|
607
|
+
&& manifest.scripts?.['test:full:parallel'] === 'node scripts/test-full-suite.js'
|
|
608
|
+
&& fs.existsSync(path.join(rootDir, 'scripts', 'test-full-suite.js'));
|
|
609
|
+
} catch (error) {
|
|
610
|
+
if (error instanceof SyntaxError || error.code === 'ENOENT' || error.code === 'ENOTDIR') return false;
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function runAllTests(exec = execFileSync, rootDir = process.cwd()) {
|
|
577
616
|
const startTime = Date.now();
|
|
617
|
+
const useFullSuite = isForgeCheckout(rootDir);
|
|
618
|
+
const command = useFullSuite ? 'node' : 'bun';
|
|
619
|
+
const args = useFullSuite ? ['scripts/test-full-suite.js'] : ['test', '--timeout', '30000'];
|
|
620
|
+
const execOptions = getExecOptions(rootDir, useFullSuite);
|
|
578
621
|
|
|
579
622
|
try {
|
|
580
|
-
const result = exec(
|
|
623
|
+
const result = exec(command, args, execOptions); // NOSONAR S4036 - fixed CLI commands, no user input, developer tool context
|
|
581
624
|
|
|
582
|
-
const { passed, failed, skipped, total, testsFound } = parseTestCounts(result);
|
|
625
|
+
const { status, passed, failed, errors = 0, skipped, total, testsFound } = parseTestCounts(result);
|
|
626
|
+
const success = failed === 0 && errors === 0 && (status === undefined || status === 'PASS');
|
|
583
627
|
|
|
584
628
|
// B2 (N1): a bun run that executes 0 tests must NEVER report PASS — that
|
|
585
629
|
// manufactures false confidence. Surface it as an explicit SKIP instead.
|
|
@@ -597,27 +641,27 @@ async function runAllTests(exec = execFileSync) {
|
|
|
597
641
|
}
|
|
598
642
|
|
|
599
643
|
return {
|
|
600
|
-
success
|
|
644
|
+
success,
|
|
601
645
|
testsFound: true,
|
|
602
646
|
duration: Date.now() - startTime,
|
|
603
647
|
passed,
|
|
604
648
|
failed,
|
|
605
649
|
skipped,
|
|
606
650
|
total,
|
|
607
|
-
message:
|
|
651
|
+
message: success
|
|
608
652
|
? `All ${total} tests passed`
|
|
609
|
-
: `${failed}/${total} tests failed`,
|
|
653
|
+
: `${failed + errors}/${total} tests failed`,
|
|
610
654
|
};
|
|
611
655
|
} catch (error) {
|
|
612
656
|
// Check for timeout
|
|
613
|
-
if (error.killed && error.signal === 'SIGTERM') {
|
|
657
|
+
if (error.code === 'ETIMEDOUT' || (error.killed && error.signal === 'SIGTERM')) {
|
|
614
658
|
return {
|
|
615
659
|
success: false,
|
|
616
660
|
duration: Date.now() - startTime,
|
|
617
661
|
passed: 0,
|
|
618
662
|
failed: 0,
|
|
619
663
|
total: 0,
|
|
620
|
-
message: validationTimeoutMessage('Test execution'),
|
|
664
|
+
message: validationTimeoutMessage('Test execution', execOptions.timeout),
|
|
621
665
|
};
|
|
622
666
|
}
|
|
623
667
|
|
|
@@ -631,7 +675,7 @@ async function runAllTests(exec = execFileSync) {
|
|
|
631
675
|
passed: 0,
|
|
632
676
|
failed: 0,
|
|
633
677
|
total: 0,
|
|
634
|
-
message: 'Tests skipped:
|
|
678
|
+
message: 'Tests skipped: test runner not found. Install Node.js and Bun to run tests.',
|
|
635
679
|
};
|
|
636
680
|
}
|
|
637
681
|
|
|
@@ -641,7 +685,7 @@ async function runAllTests(exec = execFileSync) {
|
|
|
641
685
|
const passed = counts.passed;
|
|
642
686
|
// bun exited non-zero: if the output shows no explicit failure count, treat
|
|
643
687
|
// it as at least one failure rather than swallowing the non-zero exit.
|
|
644
|
-
const failed = counts.failed > 0 ? counts.failed : Math.max(1, counts.
|
|
688
|
+
const failed = counts.failed > 0 ? counts.failed : Math.max(1, counts.errors || 0);
|
|
645
689
|
const total = counts.total > 0 ? counts.total : passed + failed + counts.skipped;
|
|
646
690
|
|
|
647
691
|
return {
|
|
@@ -753,8 +797,8 @@ async function executeValidate(options = {}) { // NOSONAR S3776
|
|
|
753
797
|
// 4. Tests
|
|
754
798
|
if (!skip.includes(CHECK_TYPES.TESTS)) {
|
|
755
799
|
try {
|
|
756
|
-
checks.tests = await runAllTests();
|
|
757
|
-
if (!checks.tests.success &&
|
|
800
|
+
checks.tests = await runAllTests(execFileSync, rootDir);
|
|
801
|
+
if (!checks.tests.success && checks.tests.skipped !== true) {
|
|
758
802
|
failedChecks.push(CHECK_TYPES.TESTS);
|
|
759
803
|
if (!continueOnError) {
|
|
760
804
|
return buildResult(checks, failedChecks, errors, startTime);
|