@bridge_gpt/mcp-server 0.2.66 → 0.2.68
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 +421 -83
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +70 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bounded-wait.js +67 -10
- package/build/bridge-permissions.js +829 -0
- package/build/claude-settings.js +18 -0
- package/build/cli-help.js +21 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +857 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +72 -29
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +98 -28
- package/build/conductor/errors.js +4 -3
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +113 -12
- package/build/conductor/readiness-sections.js +410 -9
- package/build/conductor/readiness.js +388 -33
- package/build/conductor/recovery-cli.js +310 -12
- package/build/conductor/recovery-operations.js +167 -3
- package/build/conductor/repair-contract.js +54 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +62 -3
- package/build/doctor.js +92 -1
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +129 -61
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1379 -28
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +33 -2
- package/build/executor/completion-contract.js +66 -10
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/deps.js +4 -1
- package/build/executor/env.js +19 -0
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +33 -1
- package/build/executor/job-runner.js +813 -87
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +37 -3
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +112 -26
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +111 -18
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +122 -13
- package/build/executor/service-unit.js +7 -1
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +113 -34
- package/build/executor/worker-guard-hook.js +316 -58
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +166 -8
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +1224 -1054
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +296 -41
- package/build/install-bridge.js +547 -40
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +34 -160
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +136 -2
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +83 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +52 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/review-generation.js +22 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/setup-epic.js +152 -22
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/tool-wrapper.js +39 -43
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +208 -36
- package/build/tool-error-envelope.js +61 -0
- package/build/tool-surface-gating.js +36 -0
- package/build/upgrade-cli.js +55 -1
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +4 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +145 -6
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
- package/docs/EPIC-IMPLEMENTER.md +728 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { access, lstat, realpath, readFile } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { WORKFLOW_HOSTS, buildPackagedAssetCatalog } from "./generated-assets-catalog.js";
|
|
5
|
+
import { authorizeAssetRepair, assetRootBinding } from "./generated-assets-context.js";
|
|
6
|
+
import { validateAssetRoot } from "./generated-assets-paths.js";
|
|
7
|
+
import { readMcpInstallState } from "./mcp-install-state.js";
|
|
8
|
+
import { repairPackagedAssets } from "./generated-assets-repair.js";
|
|
9
|
+
import { createConfinedAssetPublisher } from "./generated-assets-publisher.js";
|
|
10
|
+
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
11
|
+
import { VERSION } from "./version.generated.js";
|
|
12
|
+
export const REPAIR_ASSETS_HELP = `Usage: repair-assets [--project-root <directory>] [--host <claude|cursor|codex|copilot>]
|
|
13
|
+
Fill missing packaged workflow files only; preserve existing customized files.
|
|
14
|
+
Explicit --project-root plus --host authorizes provisioning without inventory.
|
|
15
|
+
Implicit selection requires root-bound inventory at exact cwd; subdirectories must
|
|
16
|
+
select --project-root. Never infer installed hosts from folders or selectedPlatforms.
|
|
17
|
+
Reports restored/preserved/unreadable/failed files; incomplete repair exits 1.
|
|
18
|
+
Linux procfs supports confined exclusive publication. Other platforms refuse writes
|
|
19
|
+
when a confined adapter is unavailable. Existing readable files remain preserved.
|
|
20
|
+
Excludes custom pipelines/instructions, registrations, credentials, generated outputs,
|
|
21
|
+
onboarding, API setup, and Windsurf. Refresh/reconnect after repair, then run doctor.
|
|
22
|
+
Unpinned npx -y ${MCP_PACKAGE_NAME} repair-assets intentionally selects the latest
|
|
23
|
+
package; use @${VERSION} to repair from this running version.\n`;
|
|
24
|
+
export async function runRepairAssetsCli(argv, options = {}) {
|
|
25
|
+
const log = options.log ?? (text => process.stdout.write(`${text}\n`));
|
|
26
|
+
const error = options.error ?? (text => process.stderr.write(`${text}\n`));
|
|
27
|
+
if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) {
|
|
28
|
+
log(REPAIR_ASSETS_HELP);
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
let requestedRoot;
|
|
32
|
+
const hosts = [];
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
const flag = argv[i];
|
|
35
|
+
const value = argv[++i];
|
|
36
|
+
if (!value || value.startsWith("--")) {
|
|
37
|
+
error("repair-assets: missing option value; use --help.");
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
if (flag === "--project-root" && !requestedRoot)
|
|
41
|
+
requestedRoot = value;
|
|
42
|
+
else if (flag === "--host" && WORKFLOW_HOSTS.includes(value))
|
|
43
|
+
hosts.push(value);
|
|
44
|
+
else {
|
|
45
|
+
error("repair-assets: unknown, duplicate, or invalid option; use --help.");
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const fs = options.fs ?? { lstat, realpath, readFile: (p) => readFile(p, "utf-8"), access: (p) => access(p, constants.R_OK) };
|
|
50
|
+
const root = await validateAssetRoot(requestedRoot ?? options.cwd ?? process.cwd(), options.home ?? os.homedir(), fs);
|
|
51
|
+
if (!root) {
|
|
52
|
+
error("repair-assets: select an existing canonical project directory, excluding home and filesystem roots.");
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const state = await readMcpInstallState(root, fs);
|
|
56
|
+
const inventory = state.status === "valid" ? state.state.packagedAssets : undefined;
|
|
57
|
+
const explicitPair = requestedRoot !== undefined && hosts.length > 0;
|
|
58
|
+
const selected = [...new Set(hosts.length ? hosts : inventory?.assets.map(a => a.host) ?? [])];
|
|
59
|
+
if (!authorizeAssetRepair(root, selected, explicitPair, inventory)) {
|
|
60
|
+
error("repair-assets: installation context is uncertain. Supply both --project-root and --host for explicit provisioning.");
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
log(`Selected project root: ${root}`);
|
|
64
|
+
log(`Invoking package: ${VERSION}; ${explicitPair ? "explicit fill-only provisioning" : "inventory-authorized repair"}.`);
|
|
65
|
+
const boundInventory = inventory?.rootBinding === assetRootBinding(root) ? inventory : undefined;
|
|
66
|
+
if (boundInventory)
|
|
67
|
+
log(`Recorded package: ${boundInventory.packageVersion}; invoking bundle may differ.`);
|
|
68
|
+
const catalog = buildPackagedAssetCatalog();
|
|
69
|
+
if (boundInventory) {
|
|
70
|
+
const historical = new Set(boundInventory.assets.map(a => a.id));
|
|
71
|
+
const introduced = catalog.assets.filter(a => a.hosts.some(h => selected.includes(h)) && !historical.has(a.id));
|
|
72
|
+
log(`Newly introduced catalog assets: ${introduced.length} (provisioning, not historical loss).`);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const result = await (options.repair ?? repairPackagedAssets)({ root, hosts: selected, explicitPair, inventory: boundInventory, fs,
|
|
76
|
+
catalog, publisher: options.publisher ?? createConfinedAssetPublisher(options.platform) });
|
|
77
|
+
for (const status of ["restored", "preserved", "unreadable", "failed"]) {
|
|
78
|
+
const items = result.findings.filter(f => f.status === status);
|
|
79
|
+
log(`${status}: ${items.length}${items.length ? ` (${items.slice(0, 5).map(f => `${f.destination}: ${f.reason}`).join(", ")}${items.length > 5 ? ", …" : ""})` : ""}`);
|
|
80
|
+
}
|
|
81
|
+
if (result.packageProblems.length)
|
|
82
|
+
error(`Package problems: ${result.packageProblems.join(", ")}`);
|
|
83
|
+
if (result.cleanupFailures.length)
|
|
84
|
+
error("Some owned temporary files could not be removed. Inspect .bridge-repair-*.tmp in the reported asset parents; do not remove other files.");
|
|
85
|
+
if (!result.complete)
|
|
86
|
+
error("repair-assets: requested availability restoration is incomplete. Resolve reported permission/path/platform problems before retrying.");
|
|
87
|
+
return result.complete ? 0 : 1;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
error("repair-assets: repair failed; no unsafe fallback was attempted.");
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -193,6 +193,28 @@ export class ReviewGenerationRegistry {
|
|
|
193
193
|
return null;
|
|
194
194
|
return { baseline: entry.baseline, expectedLegCount: entry.expectedLegCount };
|
|
195
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Consecutive all-`404` wait windows recorded for the live submission
|
|
198
|
+
* (BAPI-1382), or `0` when there is none or no live entry.
|
|
199
|
+
*/
|
|
200
|
+
notFoundWindowCount(repoName, ticketNumber) {
|
|
201
|
+
this.evictExpired();
|
|
202
|
+
return this.entries.get(baselineKey(repoName, ticketNumber))?.notFoundWindows ?? 0;
|
|
203
|
+
}
|
|
204
|
+
/** Count one more all-`404` wait window for the live submission. No-op without one. */
|
|
205
|
+
noteNotFoundWindow(repoName, ticketNumber) {
|
|
206
|
+
this.evictExpired();
|
|
207
|
+
const entry = this.entries.get(baselineKey(repoName, ticketNumber));
|
|
208
|
+
if (!entry)
|
|
209
|
+
return;
|
|
210
|
+
entry.notFoundWindows = (entry.notFoundWindows ?? 0) + 1;
|
|
211
|
+
}
|
|
212
|
+
/** A non-`404` observation resets the count. No-op without a live entry. */
|
|
213
|
+
resetNotFoundWindows(repoName, ticketNumber) {
|
|
214
|
+
const entry = this.entries.get(baselineKey(repoName, ticketNumber));
|
|
215
|
+
if (entry)
|
|
216
|
+
entry.notFoundWindows = 0;
|
|
217
|
+
}
|
|
196
218
|
/**
|
|
197
219
|
* Consume a baseline. Clearing means "durable persistence has happened",
|
|
198
220
|
* not "a terminal envelope was received" (BAPI-1129) — the anchor is
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* compiled module; the logic lives here (under src/, built by tsc) so it can
|
|
17
17
|
* be unit-tested with node:test like every other module in this package.
|
|
18
18
|
*/
|
|
19
|
-
import { readdirSync } from "fs";
|
|
19
|
+
import { readdirSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync, renameSync, rmSync, existsSync } from "fs";
|
|
20
|
+
import { tmpdir } from "os";
|
|
20
21
|
import { spawnSync } from "child_process";
|
|
21
22
|
import path from "path";
|
|
22
23
|
const DEFAULT_BUILD_DIR = "build";
|
|
@@ -76,6 +77,7 @@ export const MODULE_MOCK_MANIFEST = [
|
|
|
76
77
|
"build/connect-github.test.js",
|
|
77
78
|
"build/council-submission.test.js",
|
|
78
79
|
"build/council-wait-recovery.test.js",
|
|
80
|
+
"build/generated-assets-publisher.test.js",
|
|
79
81
|
"build/index-artifacts.test.js",
|
|
80
82
|
"build/index-council-filenames.test.js",
|
|
81
83
|
"build/index-generate-decision-page.integration.test.js",
|
|
@@ -314,27 +316,31 @@ export function selectTestGroup(mode, discovered) {
|
|
|
314
316
|
*/
|
|
315
317
|
export function partitionTestBatches(files, options = {}) {
|
|
316
318
|
const { execPath = process.execPath, nodeOptions = [], maxLength = MAX_COMMAND_LENGTH } = options;
|
|
317
|
-
const
|
|
319
|
+
const size = (batch, index) => {
|
|
320
|
+
const args = options.argumentBuilder?.(batch, index) ?? [...nodeOptions, ...batch];
|
|
321
|
+
// Quoted serialization conservatively includes spaces/backslashes/quotes on
|
|
322
|
+
// Windows; legacy callers without an argument builder keep their old budget.
|
|
323
|
+
return options.argumentBuilder
|
|
324
|
+
? [execPath, ...args].map((arg) => JSON.stringify(arg)).join(" ").length
|
|
325
|
+
: [execPath, ...args].join(" ").length;
|
|
326
|
+
};
|
|
318
327
|
const batches = [];
|
|
319
328
|
let current = [];
|
|
320
|
-
let currentLength = prefixLength;
|
|
321
329
|
for (const file of files) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
throw new Error(`run-unit-tests: "${file}" alone exceeds the command-length budget ` +
|
|
325
|
-
`(${prefixLength + addition} > ${maxLength} chars) — cannot batch it safely`);
|
|
330
|
+
if (size([file], batches.length) > maxLength) {
|
|
331
|
+
throw new Error(`run-unit-tests: single file alone exceeds the command-length budget`);
|
|
326
332
|
}
|
|
327
|
-
if (current.length
|
|
333
|
+
if (current.length && size([...current, file], batches.length) > maxLength) {
|
|
328
334
|
batches.push(current);
|
|
329
335
|
current = [];
|
|
330
|
-
|
|
336
|
+
if (size([file], batches.length) > maxLength) {
|
|
337
|
+
throw new Error(`run-unit-tests: single file alone exceeds the command-length budget`);
|
|
338
|
+
}
|
|
331
339
|
}
|
|
332
340
|
current.push(file);
|
|
333
|
-
currentLength += addition;
|
|
334
341
|
}
|
|
335
|
-
if (current.length
|
|
342
|
+
if (current.length)
|
|
336
343
|
batches.push(current);
|
|
337
|
-
}
|
|
338
344
|
return batches;
|
|
339
345
|
}
|
|
340
346
|
/** Discovers, selects, batches, and spawns the requested test group; returns an aggregate exit code. */
|
|
@@ -355,30 +361,7 @@ export function runSelectedGroup(mode, options) {
|
|
|
355
361
|
const batchable = selected.filter((file) => !quarantineSet.has(file));
|
|
356
362
|
const quarantined = selected.filter((file) => quarantineSet.has(file));
|
|
357
363
|
const extraFlags = mode === "module-mocks" ? ["--experimental-test-module-mocks"] : [];
|
|
358
|
-
|
|
359
|
-
const batches = partitionTestBatches(batchable, { execPath, nodeOptions });
|
|
360
|
-
log(`run-unit-tests: group="${mode}" files=${selected.length} batches=${batches.length}` +
|
|
361
|
-
(quarantined.length > 0 ? ` quarantined=${quarantined.length}` : ""));
|
|
362
|
-
let aggregateExit = 0;
|
|
363
|
-
const spawn = (args) => {
|
|
364
|
-
const result = spawnFn(execPath, args, {
|
|
365
|
-
cwd: root,
|
|
366
|
-
stdio: "inherit",
|
|
367
|
-
shell: false,
|
|
368
|
-
});
|
|
369
|
-
if ((result.status ?? 1) !== 0) {
|
|
370
|
-
aggregateExit = 1;
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
batches.forEach((batch, index) => {
|
|
374
|
-
log(`run-unit-tests: batch ${index + 1}/${batches.length} (${batch.length} file(s))`);
|
|
375
|
-
spawn([...nodeOptions, ...batch]);
|
|
376
|
-
});
|
|
377
|
-
quarantined.forEach((file, index) => {
|
|
378
|
-
log(`run-unit-tests: quarantined ${index + 1}/${quarantined.length} ${file} (single-file, --test-force-exit)`);
|
|
379
|
-
spawn([...nodeOptions, "--test-force-exit", file]);
|
|
380
|
-
});
|
|
381
|
-
return aggregateExit;
|
|
364
|
+
return runWithReports(mode, options, batchable, quarantined, [...extraFlags, "--test"], log);
|
|
382
365
|
}
|
|
383
366
|
/**
|
|
384
367
|
* Returns the integration lane's file list: every compiled `*.test.js` under
|
|
@@ -420,20 +403,118 @@ export function runIntegrationLane(options) {
|
|
|
420
403
|
throw new Error("run-integration-tests: discovered zero integration tests — refusing to run " +
|
|
421
404
|
"(did you forget 'npm run build'?)");
|
|
422
405
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
406
|
+
return runWithReports("integration", options, selected, [], ["--test"], log);
|
|
407
|
+
}
|
|
408
|
+
/** Shared reporter argument builder: sizing and execution cannot diverge. */
|
|
409
|
+
export function reporterArguments(nodeOptions, files, destination, forceExit = false) {
|
|
410
|
+
return [...nodeOptions, ...(forceExit ? ["--test-force-exit"] : []),
|
|
411
|
+
"--test-reporter=spec", "--test-reporter-destination=stdout",
|
|
412
|
+
"--test-reporter=junit", `--test-reporter-destination=${destination}`, ...files];
|
|
413
|
+
}
|
|
414
|
+
function runWithReports(group, options, ordinary, quarantined, nodeOptions, log) {
|
|
415
|
+
const { root, execPath = process.execPath, spawnFn = spawnSync } = options;
|
|
416
|
+
const retained = options.reportOutputDirectory !== undefined;
|
|
417
|
+
let directory;
|
|
426
418
|
let aggregateExit = 0;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if ((result.status ?? 1) !== 0) {
|
|
435
|
-
aggregateExit = 1;
|
|
419
|
+
try {
|
|
420
|
+
if (retained) {
|
|
421
|
+
if (!options.reportOutputDirectory?.trim())
|
|
422
|
+
throw new Error("empty report directory");
|
|
423
|
+
directory = path.resolve(options.reportOutputDirectory);
|
|
424
|
+
// Exclusive allocation refuses stale reports and concurrent invocations.
|
|
425
|
+
mkdirSync(directory, { recursive: false });
|
|
436
426
|
}
|
|
437
|
-
|
|
427
|
+
else {
|
|
428
|
+
directory = mkdtempSync(path.join(tmpdir(), "bridge-junit-"));
|
|
429
|
+
}
|
|
430
|
+
const outputDirectory = directory;
|
|
431
|
+
const reportName = (index) => `report-${index}.xml`;
|
|
432
|
+
const args = (files, index, forceExit = false) => reporterArguments(nodeOptions, files, path.join(outputDirectory, reportName(index)), forceExit);
|
|
433
|
+
// Source mapping comes from the build's actual rootDir/outDir, including
|
|
434
|
+
// authored scripts (which do not pass through tsc).
|
|
435
|
+
const config = JSON.parse(readFileSync(new URL("../tsconfig.json", import.meta.url), "utf8"));
|
|
436
|
+
const { rootDir, outDir } = config.compilerOptions;
|
|
437
|
+
if (rootDir !== "src" || outDir !== "build")
|
|
438
|
+
throw new Error("unsupported build source mapping");
|
|
439
|
+
const sourceFor = (file) => {
|
|
440
|
+
const posix = file.replaceAll("\\", "/");
|
|
441
|
+
if (posix.startsWith(`${outDir}/`) && posix.endsWith(".js")) {
|
|
442
|
+
return `mcp_server/${rootDir}/${posix.slice(outDir.length + 1, -3)}.ts`;
|
|
443
|
+
}
|
|
444
|
+
if (posix.startsWith("scripts/") && posix.endsWith(".js"))
|
|
445
|
+
return `mcp_server/${posix}`;
|
|
446
|
+
throw new Error("unmapped test source");
|
|
447
|
+
};
|
|
448
|
+
// Node 24.16 discovery confirmed explicit testcase file attribution.
|
|
449
|
+
// Older local/scheduled runtimes use one source per invocation, so the
|
|
450
|
+
// inventory provides an unambiguous fallback without adding concurrency.
|
|
451
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
452
|
+
const explicitAttribution = major > 24 || (major === 24 && minor >= 16);
|
|
453
|
+
const batches = explicitAttribution
|
|
454
|
+
? partitionTestBatches(ordinary, { execPath, nodeOptions, argumentBuilder: args })
|
|
455
|
+
: ordinary.map((file, index) => {
|
|
456
|
+
partitionTestBatches([file], { execPath, argumentBuilder: (files) => args(files, index) });
|
|
457
|
+
return [file];
|
|
458
|
+
});
|
|
459
|
+
const plans = [];
|
|
460
|
+
const addPlan = (files, forceExit) => {
|
|
461
|
+
const index = plans.length;
|
|
462
|
+
partitionTestBatches(files, { execPath, argumentBuilder: (values) => args(values, index, forceExit) });
|
|
463
|
+
const sources = Object.fromEntries(files.map((file) => [`mcp_server/${file}`, sourceFor(file)]));
|
|
464
|
+
// integrationSuite declares skipped suites inside the shared harness;
|
|
465
|
+
// Node attributes those cases to the wrapper, rather than its caller.
|
|
466
|
+
// Map that real declaration source using the same verified build config.
|
|
467
|
+
// Older runtimes still need exactly one entry for invocation attribution.
|
|
468
|
+
const harness = `${outDir}/integration/harness.js`;
|
|
469
|
+
if (group === "integration" && explicitAttribution &&
|
|
470
|
+
existsSync(path.join(root, harness)) &&
|
|
471
|
+
existsSync(path.join(root, rootDir, "integration/harness.ts"))) {
|
|
472
|
+
sources[`mcp_server/${harness}`] = sourceFor(harness);
|
|
473
|
+
}
|
|
474
|
+
plans.push({ report: reportName(index), files, forceExit, state: "planned", status: null,
|
|
475
|
+
sources });
|
|
476
|
+
};
|
|
477
|
+
batches.forEach((batch) => addPlan(batch, false));
|
|
478
|
+
quarantined.forEach((file) => addPlan([file], true));
|
|
479
|
+
const inventory = { version: 1, lane: group === "integration" ? "integration" : "unit", group,
|
|
480
|
+
nodeVersion: process.version, attribution: explicitAttribution ? "reporter-file" : "single-source", spawns: plans };
|
|
481
|
+
const publish = () => {
|
|
482
|
+
const temporary = path.join(outputDirectory, "inventory.json.tmp");
|
|
483
|
+
writeFileSync(temporary, JSON.stringify(inventory, null, 2));
|
|
484
|
+
renameSync(temporary, path.join(outputDirectory, "inventory.json"));
|
|
485
|
+
};
|
|
486
|
+
publish(); // Complete expected inventory is durable before the first spawn.
|
|
487
|
+
log(`run-unit-tests: group="${group}" files=${ordinary.length + quarantined.length} batches=${plans.length}`);
|
|
488
|
+
for (const [index, plan] of plans.entries()) {
|
|
489
|
+
plan.state = "started";
|
|
490
|
+
publish();
|
|
491
|
+
try {
|
|
492
|
+
const result = spawnFn(execPath, args(plan.files, index, plan.forceExit), { cwd: root, stdio: "inherit", shell: false });
|
|
493
|
+
plan.status = result.status;
|
|
494
|
+
plan.state = result.status === null || !existsSync(path.join(outputDirectory, plan.report)) ? "failed" : "completed";
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
plan.state = "failed";
|
|
498
|
+
}
|
|
499
|
+
if (plan.state !== "completed" || plan.status !== 0)
|
|
500
|
+
aggregateExit = 1;
|
|
501
|
+
publish();
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// No exception/environment dump reaches CI logs or exported evidence.
|
|
506
|
+
log("run-unit-tests: report allocation, inventory publication, or execution failed");
|
|
507
|
+
aggregateExit = 1;
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
if (!retained && directory) {
|
|
511
|
+
try {
|
|
512
|
+
rmSync(directory, { recursive: true, force: true });
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
aggregateExit = 1;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
438
519
|
return aggregateExit;
|
|
439
520
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Shared leaf scrubber for durable untrusted text. */
|
|
2
|
+
const REDACTED = "[REDACTED]";
|
|
3
|
+
/**
|
|
4
|
+
* Patterns for values that must never reach durable state or a log line.
|
|
5
|
+
*
|
|
6
|
+
* Each rewrites only the secret itself and keeps the surrounding key or scheme, so a
|
|
7
|
+
* redacted record still shows *that* a credential was involved — useful for audit,
|
|
8
|
+
* and materially different from dropping the field.
|
|
9
|
+
*/
|
|
10
|
+
const SECRET_PATTERNS = [
|
|
11
|
+
// Bearer / token schemes in an Authorization header or bare.
|
|
12
|
+
[/\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{8,}/gi, `$1 ${REDACTED}`],
|
|
13
|
+
// authorization: <anything>
|
|
14
|
+
[/\b(authorization)\s*[:=]\s*\S+/gi, `$1: ${REDACTED}`],
|
|
15
|
+
// Well-known provider key shapes.
|
|
16
|
+
[/\b(sk|pk|rk)-[A-Za-z0-9_-]{16,}/g, REDACTED],
|
|
17
|
+
[/\bgh[pousr]_[A-Za-z0-9]{16,}/g, REDACTED],
|
|
18
|
+
// key/secret/password/token assignments in prose, env, JSON, or CLI form.
|
|
19
|
+
[
|
|
20
|
+
/\b([A-Za-z0-9_-]*(?:api[_-]?key|secret|password|passwd|token|credential)[A-Za-z0-9_-]*)\s*[:=]\s*("[^"]*"|'[^']*'|\S+)/gi,
|
|
21
|
+
`$1=${REDACTED}`,
|
|
22
|
+
],
|
|
23
|
+
// Credentials embedded in a URL.
|
|
24
|
+
[/\b([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/gi, `$1${REDACTED}@`],
|
|
25
|
+
];
|
|
26
|
+
/** Redact secret-bearing substrings while leaving safe technical text intact. */
|
|
27
|
+
export function redactSecrets(value) {
|
|
28
|
+
let output = value;
|
|
29
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
30
|
+
output = output.replace(pattern, replacement);
|
|
31
|
+
}
|
|
32
|
+
return output;
|
|
33
|
+
}
|