@opengsd/gsd-core 1.5.0-rc.2 → 1.5.0-rc.4
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/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-writer.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-mempalace-curator.md +47 -0
- package/agents/gsd-nyquist-auditor.md +1 -0
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-roadmapper.md +55 -2
- package/agents/gsd-security-auditor.md +1 -0
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +13 -2
- package/bin/install.js +61 -64
- package/commands/gsd/mempalace-capture.md +71 -0
- package/commands/gsd/mempalace-recall.md +102 -0
- package/commands/gsd/ns-context.md +4 -2
- package/commands/gsd/progress.md +2 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +277 -95
- package/gsd-core/bin/lib/active-workstream-store.cjs +6 -0
- package/gsd-core/bin/lib/capability-activation.cjs +86 -0
- package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
- package/gsd-core/bin/lib/capability-state.cjs +128 -21
- package/gsd-core/bin/lib/capability-writer.cjs +354 -0
- package/gsd-core/bin/lib/check-command-router.cjs +328 -1
- package/gsd-core/bin/lib/clusters.cjs +2 -0
- package/gsd-core/bin/lib/command-roster.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +33 -10
- package/gsd-core/bin/lib/config-loader.cjs +7 -8
- package/gsd-core/bin/lib/config-schema.cjs +32 -3
- package/gsd-core/bin/lib/config.cjs +81 -26
- package/gsd-core/bin/lib/core.cjs +5 -2
- package/gsd-core/bin/lib/edge-probe.cjs +25 -2
- package/gsd-core/bin/lib/frontmatter.cjs +53 -1
- package/gsd-core/bin/lib/git-base-branch.cjs +194 -0
- package/gsd-core/bin/lib/init.cjs +36 -11
- package/gsd-core/bin/lib/install-profiles.cjs +57 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
- package/gsd-core/bin/lib/model-resolver.cjs +47 -5
- package/gsd-core/bin/lib/phase.cjs +99 -23
- package/gsd-core/bin/lib/plan-drift-guard.cjs +117 -0
- package/gsd-core/bin/lib/probe-core.cjs +117 -1
- package/gsd-core/bin/lib/profile-output.cjs +45 -4
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +13 -3
- package/gsd-core/bin/lib/roadmap.cjs +97 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1946 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +54 -30
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
- package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
- package/gsd-core/bin/lib/state-command-router.cjs +15 -3
- package/gsd-core/bin/lib/state-document.cjs +46 -1
- package/gsd-core/bin/lib/state.cjs +461 -94
- package/gsd-core/bin/lib/verify.cjs +92 -8
- package/gsd-core/bin/lib/worktree-safety.cjs +2 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
- package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
- package/gsd-core/bin/shared/model-catalog.json +1 -0
- package/gsd-core/references/edge-probe.md +11 -0
- package/gsd-core/references/loop-hook-dispatch.md +61 -0
- package/gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json +14 -0
- package/gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json +4 -0
- package/gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json +32 -0
- package/gsd-core/references/prohibition-probe.md +248 -0
- package/gsd-core/templates/config.json +1 -1
- package/gsd-core/templates/spec.md +14 -0
- package/gsd-core/workflows/audit-milestone.md +5 -3
- package/gsd-core/workflows/autonomous.md +10 -5
- package/gsd-core/workflows/code-review-fix.md +9 -7
- package/gsd-core/workflows/code-review.md +8 -6
- package/gsd-core/workflows/complete-milestone.md +1 -5
- package/gsd-core/workflows/discuss-phase.md +14 -0
- package/gsd-core/workflows/execute-phase.md +86 -146
- package/gsd-core/workflows/execute-plan.md +21 -6
- package/gsd-core/workflows/help/modes/full.md +7 -1
- package/gsd-core/workflows/new-project.md +3 -3
- package/gsd-core/workflows/next.md +50 -2
- package/gsd-core/workflows/pause-work.md +7 -1
- package/gsd-core/workflows/plan-phase.md +91 -221
- package/gsd-core/workflows/plan-review-convergence.md +14 -4
- package/gsd-core/workflows/pr-branch.md +4 -2
- package/gsd-core/workflows/profile-user.md +3 -1
- package/gsd-core/workflows/progress.md +58 -1
- package/gsd-core/workflows/quick.md +12 -8
- package/gsd-core/workflows/resume-project.md +17 -1
- package/gsd-core/workflows/review.md +19 -2
- package/gsd-core/workflows/secure-phase.md +4 -2
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/settings.md +27 -1
- package/gsd-core/workflows/ship.md +58 -5
- package/gsd-core/workflows/spec-phase.md +75 -0
- package/gsd-core/workflows/validate-phase.md +4 -2
- package/gsd-core/workflows/verify-phase.md +14 -4
- package/gsd-core/workflows/verify-work.md +27 -11
- package/hooks/dist/gsd-ensure-canonical-path.js +305 -0
- package/hooks/dist/gsd-statusline.js +1 -1
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-ensure-canonical-path.js +305 -0
- package/hooks/gsd-statusline.js +1 -1
- package/hooks/hooks.json +1 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +5 -4
- package/scripts/affected-tests-lib.cjs +16 -4
- package/scripts/build-hooks.js +7 -0
- package/scripts/changeset/new.cjs +17 -3
- package/scripts/fix-slash-commands.cjs +15 -3
- package/scripts/gen-capability-registry.cjs +373 -49
- package/scripts/gen-inventory-manifest.cjs +1 -4
- package/scripts/gen-loop-host-contract.cjs +55 -0
- package/scripts/issue-version-gate.cjs +140 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +327 -0
- package/scripts/lint-allow-test-rule-refs.cjs +162 -0
- package/scripts/lint-test-file-count.allowlist.json +14 -0
- package/scripts/mutation-matrix.cjs +108 -7
- package/scripts/pr-target-policy.cjs +63 -0
- package/scripts/release-tarball-smoke.cjs +7 -1
- package/scripts/research-profiles.cjs +5 -5
- package/scripts/run-tests.cjs +178 -17
package/scripts/run-tests.cjs
CHANGED
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
// node scripts/run-tests.cjs --suite slow # *.slow.test.cjs
|
|
14
14
|
// node scripts/run-tests.cjs --files "a.test.cjs b.test.cjs"
|
|
15
15
|
// node scripts/run-tests.cjs --files-from /tmp/selected-tests.txt
|
|
16
|
+
// node scripts/run-tests.cjs --suite unit --shard 1/3 # shard 1 of 3 (#1212)
|
|
17
|
+
//
|
|
18
|
+
// Sharding (issue #1212): --shard <i>/<n> runs a deterministic, balanced
|
|
19
|
+
// round-robin slice of the SORTED selected file list (file index k → shard
|
|
20
|
+
// k % n). i is 1-based (1..n); n >= 1; n=1 is a pure no-op (all files). The
|
|
21
|
+
// CI windows full-test lane shards across N parallel runners so per-job
|
|
22
|
+
// wall-clock scales as O(total/N) and stops hitting the job time cap. Sharding
|
|
23
|
+
// composes with --suite (it slices the post-filter selection) and preserves
|
|
24
|
+
// the existing 28K argv chunking WITHIN each shard.
|
|
16
25
|
//
|
|
17
26
|
// Suite grouping convention: filename suffix marker before `.test.cjs`.
|
|
18
27
|
// A file named `foo.security.test.cjs` belongs to the `security` suite.
|
|
@@ -21,7 +30,7 @@
|
|
|
21
30
|
'use strict';
|
|
22
31
|
|
|
23
32
|
const { readdirSync } = require('fs');
|
|
24
|
-
const { join } = require('path');
|
|
33
|
+
const { join, basename } = require('path');
|
|
25
34
|
const { execFileSync } = require('child_process');
|
|
26
35
|
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
27
36
|
|
|
@@ -112,14 +121,90 @@ function ensureBuiltArtifacts(overrides = {}) {
|
|
|
112
121
|
}
|
|
113
122
|
const MARKED_SUITES = ['integration', 'install', 'security', 'slow'];
|
|
114
123
|
|
|
124
|
+
// Recursively collect *.test.cjs files under dir, returning paths relative to dir.
|
|
125
|
+
// Skips node_modules to avoid accidentally picking up decoy files.
|
|
126
|
+
function walkTestFiles(dir, relBase) {
|
|
127
|
+
const results = [];
|
|
128
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
129
|
+
if (entry.isDirectory()) {
|
|
130
|
+
if (entry.name === 'node_modules') continue;
|
|
131
|
+
results.push(...walkTestFiles(join(dir, entry.name), relBase ? `${relBase}/${entry.name}` : entry.name));
|
|
132
|
+
} else if (entry.name.endsWith('.test.cjs')) {
|
|
133
|
+
results.push(relBase ? `${relBase}/${entry.name}` : entry.name);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Parse a `--shard i/n` value into { index, total } or { error }.
|
|
140
|
+
// i is 1-based and must satisfy 1 <= i <= n; n must be >= 1. Both parts must be
|
|
141
|
+
// plain non-negative integers (no decimals, signs, or surrounding whitespace).
|
|
142
|
+
// `n=1` is the pure no-op (every file). This is the strict-input boundary
|
|
143
|
+
// (Postel's Law: be strict in what a CLI flag accepts so a typo fails loudly
|
|
144
|
+
// rather than silently running the wrong slice of the suite).
|
|
145
|
+
function parseShardArg(value) {
|
|
146
|
+
if (typeof value !== 'string') {
|
|
147
|
+
return { error: `--shard requires a value of the form i/n` };
|
|
148
|
+
}
|
|
149
|
+
const m = /^(\d+)\/(\d+)$/.exec(value);
|
|
150
|
+
if (!m) {
|
|
151
|
+
return { error: `--shard value "${value}" must be of the form i/n (e.g. 1/3)` };
|
|
152
|
+
}
|
|
153
|
+
const index = Number(m[1]);
|
|
154
|
+
const total = Number(m[2]);
|
|
155
|
+
if (!Number.isInteger(total) || total < 1) {
|
|
156
|
+
return { error: `--shard total n must be an integer >= 1, got "${m[2]}"` };
|
|
157
|
+
}
|
|
158
|
+
if (!Number.isInteger(index) || index < 1 || index > total) {
|
|
159
|
+
return { error: `--shard index i must be an integer in 1..${total}, got "${m[1]}"` };
|
|
160
|
+
}
|
|
161
|
+
return { index, total };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Deterministic, balanced round-robin partition of an ALREADY-SORTED file list.
|
|
165
|
+
// Shard `index` (1-based) receives every file whose position k in the sorted
|
|
166
|
+
// list satisfies k % total === index - 1. Round-robin (not contiguous blocks)
|
|
167
|
+
// spreads duration variance across shards and guarantees shard sizes differ by
|
|
168
|
+
// at most 1. Selection keys off array INDEX, never off the path string, so the
|
|
169
|
+
// partition is byte-identical across Windows/macOS/Linux as long as the caller
|
|
170
|
+
// sorts the list with the same (locale-independent) comparator. `total=1`
|
|
171
|
+
// returns the input unchanged (pure no-op). A shard with no files (total >
|
|
172
|
+
// file count) returns [] and is a legitimate result, not an error.
|
|
173
|
+
function selectShard(sortedFiles, { index, total }) {
|
|
174
|
+
if (total === 1) return sortedFiles;
|
|
175
|
+
return sortedFiles.filter((_, k) => k % total === index - 1);
|
|
176
|
+
}
|
|
177
|
+
|
|
115
178
|
function parseArgs(argv) {
|
|
116
179
|
let suite = null;
|
|
117
180
|
let seen = false;
|
|
118
181
|
let files = null;
|
|
119
182
|
let filesFrom = null;
|
|
183
|
+
let shard = null;
|
|
184
|
+
let shardSeen = false;
|
|
120
185
|
for (let i = 0; i < argv.length; i++) {
|
|
121
186
|
const a = argv[i];
|
|
122
|
-
if (a === '--
|
|
187
|
+
if (a === '--shard' || a.startsWith('--shard=')) {
|
|
188
|
+
if (shardSeen) {
|
|
189
|
+
return { error: 'duplicate --shard flag' };
|
|
190
|
+
}
|
|
191
|
+
shardSeen = true;
|
|
192
|
+
let v;
|
|
193
|
+
if (a === '--shard') {
|
|
194
|
+
v = argv[i + 1];
|
|
195
|
+
if (v === undefined || (typeof v === 'string' && v.startsWith('--'))) {
|
|
196
|
+
return { error: '--shard requires a value of the form i/n' };
|
|
197
|
+
}
|
|
198
|
+
i++;
|
|
199
|
+
} else {
|
|
200
|
+
v = a.slice('--shard='.length);
|
|
201
|
+
}
|
|
202
|
+
const parsed = parseShardArg(v);
|
|
203
|
+
if (parsed.error) {
|
|
204
|
+
return { error: parsed.error };
|
|
205
|
+
}
|
|
206
|
+
shard = parsed;
|
|
207
|
+
} else if (a === '--suite') {
|
|
123
208
|
if (seen) {
|
|
124
209
|
return { error: 'duplicate --suite flag' };
|
|
125
210
|
}
|
|
@@ -182,15 +267,18 @@ function parseArgs(argv) {
|
|
|
182
267
|
if (files !== null && filesFrom !== null) {
|
|
183
268
|
return { error: '--files and --files-from cannot be combined' };
|
|
184
269
|
}
|
|
185
|
-
return { suite, files, filesFrom };
|
|
270
|
+
return { suite, files, filesFrom, shard };
|
|
186
271
|
}
|
|
187
272
|
|
|
188
273
|
// Return the marked suite name embedded in a filename, or null if it's unmarked.
|
|
189
274
|
// foo.security.test.cjs -> "security"
|
|
190
275
|
// foo.test.cjs -> null (unit)
|
|
276
|
+
// Accepts either a bare filename or a relative subdir path; classification is
|
|
277
|
+
// based on the basename only so subdir paths classify identically to root files.
|
|
191
278
|
function suiteOf(filename) {
|
|
192
|
-
|
|
193
|
-
|
|
279
|
+
const name = basename(filename);
|
|
280
|
+
if (!name.endsWith('.test.cjs')) return null;
|
|
281
|
+
const base = name.slice(0, -'.test.cjs'.length);
|
|
194
282
|
const lastDot = base.lastIndexOf('.');
|
|
195
283
|
if (lastDot === -1) return null;
|
|
196
284
|
const marker = base.slice(lastDot + 1);
|
|
@@ -213,7 +301,8 @@ function splitFileList(value) {
|
|
|
213
301
|
.split(/[,\s]+/)
|
|
214
302
|
.map(v => v.trim())
|
|
215
303
|
.filter(Boolean)
|
|
216
|
-
.map(v => v.replace(
|
|
304
|
+
.map(v => v.replace(/\\/g, '/')) // normalize Windows backslashes
|
|
305
|
+
.map(v => v.replace(/^tests\//, ''));
|
|
217
306
|
}
|
|
218
307
|
|
|
219
308
|
function selectExplicitFiles(allFiles, filesValue, filesFrom) {
|
|
@@ -222,8 +311,19 @@ function selectExplicitFiles(allFiles, filesValue, filesFrom) {
|
|
|
222
311
|
? splitFileList(fs.readFileSync(filesFrom, 'utf8'))
|
|
223
312
|
: splitFileList(filesValue);
|
|
224
313
|
const available = new Set(allFiles);
|
|
314
|
+
|
|
315
|
+
// Build a basename -> [relpath, ...] index for bare-basename resolution.
|
|
316
|
+
// A bare basename (no directory separator) may match exactly one subdir file.
|
|
317
|
+
const basenameIndex = new Map();
|
|
318
|
+
for (const f of allFiles) {
|
|
319
|
+
const b = basename(f);
|
|
320
|
+
if (!basenameIndex.has(b)) basenameIndex.set(b, []);
|
|
321
|
+
basenameIndex.get(b).push(f);
|
|
322
|
+
}
|
|
323
|
+
|
|
225
324
|
const selected = [];
|
|
226
325
|
const missing = [];
|
|
326
|
+
const errors = [];
|
|
227
327
|
for (const file of requested) {
|
|
228
328
|
// If the token is a bare suite name (e.g. "unit" written by ci-test-scope
|
|
229
329
|
// as the #408 fallback sentinel), delegate to the existing suite resolver
|
|
@@ -234,11 +334,27 @@ function selectExplicitFiles(allFiles, filesValue, filesFrom) {
|
|
|
234
334
|
selected.push(f);
|
|
235
335
|
}
|
|
236
336
|
} else if (available.has(file)) {
|
|
337
|
+
// Exact relpath match (e.g. "installer-migrations/001-legacy-orphan-files.test.cjs").
|
|
237
338
|
selected.push(file);
|
|
339
|
+
} else if (!file.includes('/')) {
|
|
340
|
+
// Bare basename (no directory separator): resolve via index.
|
|
341
|
+
const candidates = basenameIndex.get(file);
|
|
342
|
+
if (!candidates || candidates.length === 0) {
|
|
343
|
+
missing.push(file);
|
|
344
|
+
} else if (candidates.length > 1) {
|
|
345
|
+
errors.push(
|
|
346
|
+
`ambiguous basename "${file}" matches multiple files: ${candidates.join(', ')} — pass the subdir path instead`,
|
|
347
|
+
);
|
|
348
|
+
} else {
|
|
349
|
+
selected.push(candidates[0]);
|
|
350
|
+
}
|
|
238
351
|
} else {
|
|
239
352
|
missing.push(file);
|
|
240
353
|
}
|
|
241
354
|
}
|
|
355
|
+
if (errors.length > 0) {
|
|
356
|
+
return { error: errors.join('; ') };
|
|
357
|
+
}
|
|
242
358
|
if (missing.length > 0) {
|
|
243
359
|
return {
|
|
244
360
|
error: `requested test file(s) not found: ${missing.join(', ')}`,
|
|
@@ -266,17 +382,16 @@ function main() {
|
|
|
266
382
|
? process.env.GSD_TEST_DIR
|
|
267
383
|
: join(__dirname, '..', 'tests');
|
|
268
384
|
|
|
269
|
-
const allFiles =
|
|
270
|
-
.filter(f => f.endsWith('.test.cjs'))
|
|
271
|
-
.sort();
|
|
385
|
+
const allFiles = walkTestFiles(testDir, '').sort();
|
|
272
386
|
|
|
273
387
|
if (allFiles.length === 0) {
|
|
274
388
|
console.error(`No test files found in ${testDir}`);
|
|
275
389
|
throw new ExitError(1);
|
|
276
390
|
}
|
|
277
391
|
|
|
392
|
+
const usingExplicitFiles = parsed.files !== null || parsed.filesFrom !== null;
|
|
278
393
|
let selectedNames;
|
|
279
|
-
if (
|
|
394
|
+
if (usingExplicitFiles) {
|
|
280
395
|
const explicit = selectExplicitFiles(allFiles, parsed.files, parsed.filesFrom);
|
|
281
396
|
if (explicit.error) {
|
|
282
397
|
console.error(`run-tests: ${explicit.error}`);
|
|
@@ -286,14 +401,54 @@ function main() {
|
|
|
286
401
|
} else {
|
|
287
402
|
selectedNames = selectFiles(allFiles, suite);
|
|
288
403
|
}
|
|
404
|
+
|
|
405
|
+
// Shard partitioning (#1212): when --shard i/n is given, keep only this
|
|
406
|
+
// shard's deterministic round-robin slice of the selected list. Applied
|
|
407
|
+
// AFTER suite/explicit selection so it composes with --suite (each shard
|
|
408
|
+
// runs i/n of the post-filter selection).
|
|
409
|
+
//
|
|
410
|
+
// The partition keys off array index, so the slice is only reproducible if
|
|
411
|
+
// the input is in a stable order. --suite/default selections are already
|
|
412
|
+
// sorted (allFiles came from walkTestFiles(...).sort() and selectFiles
|
|
413
|
+
// preserves that order), but --files/--files-from preserve REQUEST order.
|
|
414
|
+
// Sort here so --shard is deterministic regardless of how the selection was
|
|
415
|
+
// produced — the runner's documented contract is a sorted partition.
|
|
416
|
+
//
|
|
417
|
+
// emptyBeforeShard distinguishes "this shard legitimately got zero files
|
|
418
|
+
// from a non-empty list" (total > file count — a valid no-op) from "the
|
|
419
|
+
// selection was already empty before sharding" (a genuinely empty suite,
|
|
420
|
+
// which must still hit the discovery hard-error below — Codex #1212 review).
|
|
421
|
+
const usingShard = parsed.shard !== null;
|
|
422
|
+
let emptyBeforeShard = false;
|
|
423
|
+
if (usingShard) {
|
|
424
|
+
emptyBeforeShard = selectedNames.length === 0;
|
|
425
|
+
selectedNames = selectShard([...selectedNames].sort(), parsed.shard);
|
|
426
|
+
}
|
|
427
|
+
|
|
289
428
|
const selected = selectedNames.map(f => join(testDir, f));
|
|
290
429
|
|
|
291
430
|
if (selected.length === 0) {
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
431
|
+
// A legitimately-empty shard: --shard was given, the pre-shard selection
|
|
432
|
+
// had files, but this shard index drew zero (total > file count). Exit 0.
|
|
433
|
+
const legitimatelyEmptyShard = usingShard && !emptyBeforeShard;
|
|
434
|
+
if (usingExplicitFiles || legitimatelyEmptyShard) {
|
|
435
|
+
// Empty file list from --files/--files-from (e.g. CI passes an empty
|
|
436
|
+
// .ci-selected-tests.txt on docs-only/inert PRs) OR a legitimately-empty
|
|
437
|
+
// shard: both are expected. Exit 0 silently rather than taking the
|
|
438
|
+
// "discovery broken" hard-error path below. An EMPTY suite that was
|
|
439
|
+
// empty BEFORE sharding falls through to the hard error so a broken
|
|
440
|
+
// --suite filter is still caught even with --shard present.
|
|
441
|
+
console.error(`run-tests: no tests in suite "${suite || 'all'}"`);
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
// Empty suite/default run: this means discovery or the suite filter is broken.
|
|
445
|
+
// Allow GSD_ALLOW_EMPTY_SUITE=1 as an escape hatch (downgrades to a warning).
|
|
446
|
+
if (process.env.GSD_ALLOW_EMPTY_SUITE === '1') {
|
|
447
|
+
console.error(`run-tests: WARNING: 0 test files selected for suite "${suite || 'all'}" — discovery or suite filter may be broken (GSD_ALLOW_EMPTY_SUITE=1 suppressed the error)`);
|
|
448
|
+
return 0;
|
|
449
|
+
}
|
|
450
|
+
console.error(`run-tests: ERROR: 0 test files selected for suite "${suite || 'all'}" — discovery or suite filter is broken`);
|
|
451
|
+
throw new ExitError(1);
|
|
297
452
|
}
|
|
298
453
|
|
|
299
454
|
// Build the gitignored bin/lib artifact if absent, before any test requires it.
|
|
@@ -342,6 +497,9 @@ function main() {
|
|
|
342
497
|
const MAX_CMDLINE_CHARS = process.env.RUN_TESTS_MAX_CMDLINE_CHARS
|
|
343
498
|
? Number(process.env.RUN_TESTS_MAX_CMDLINE_CHARS)
|
|
344
499
|
: 28000; // headroom below the 32,767 Windows ceiling
|
|
500
|
+
const MAX_FILES_PER_CHUNK = process.env.RUN_TESTS_MAX_FILES_PER_CHUNK
|
|
501
|
+
? Number(process.env.RUN_TESTS_MAX_FILES_PER_CHUNK)
|
|
502
|
+
: 180;
|
|
345
503
|
|
|
346
504
|
// node:test does not exit until the event loop drains. A unit test that leaks
|
|
347
505
|
// an open handle (un-terminated Worker, un-killed child_process, ref'd timer)
|
|
@@ -362,7 +520,10 @@ function main() {
|
|
|
362
520
|
let currentLen = FIXED_OVERHEAD;
|
|
363
521
|
for (const file of selected) {
|
|
364
522
|
const add = file.length + 1; // +1 for the inter-arg separator
|
|
365
|
-
if (
|
|
523
|
+
if (
|
|
524
|
+
current.length > 0 &&
|
|
525
|
+
(currentLen + add > MAX_CMDLINE_CHARS || current.length >= MAX_FILES_PER_CHUNK)
|
|
526
|
+
) {
|
|
366
527
|
chunks.push(current);
|
|
367
528
|
current = [];
|
|
368
529
|
currentLen = FIXED_OVERHEAD;
|
|
@@ -424,4 +585,4 @@ if (require.main === module) {
|
|
|
424
585
|
runMain(main);
|
|
425
586
|
}
|
|
426
587
|
|
|
427
|
-
module.exports = { suiteOf, ensureBuiltArtifacts };
|
|
588
|
+
module.exports = { suiteOf, ensureBuiltArtifacts, parseShardArg, selectShard };
|