opencode-swarm 7.19.2 → 7.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +109 -11
- package/dist/index.js +207 -23
- package/dist/test-impact/analyzer.d.ts +8 -2
- package/dist/tools/test-runner.d.ts +11 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -34,7 +34,7 @@ var package_default;
|
|
|
34
34
|
var init_package = __esm(() => {
|
|
35
35
|
package_default = {
|
|
36
36
|
name: "opencode-swarm",
|
|
37
|
-
version: "7.
|
|
37
|
+
version: "7.20.0",
|
|
38
38
|
description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
|
|
39
39
|
main: "dist/index.js",
|
|
40
40
|
types: "dist/index.d.ts",
|
|
@@ -46785,18 +46785,36 @@ async function buildImpactMap(cwd) {
|
|
|
46785
46785
|
await _internals22.saveImpactMap(cwd, impactMap);
|
|
46786
46786
|
return impactMap;
|
|
46787
46787
|
}
|
|
46788
|
-
async function loadImpactMap(cwd) {
|
|
46788
|
+
async function loadImpactMap(cwd, options) {
|
|
46789
46789
|
const cachePath = path34.join(cwd, ".swarm", "cache", "impact-map.json");
|
|
46790
46790
|
if (fs17.existsSync(cachePath)) {
|
|
46791
46791
|
try {
|
|
46792
46792
|
const content = fs17.readFileSync(cachePath, "utf-8");
|
|
46793
46793
|
const data = JSON.parse(content);
|
|
46794
|
-
|
|
46795
|
-
|
|
46796
|
-
|
|
46797
|
-
|
|
46794
|
+
if (data.map !== null && typeof data.map === "object" && !Array.isArray(data.map)) {
|
|
46795
|
+
const map3 = data.map;
|
|
46796
|
+
const hasValidValues = Object.values(map3).every((v) => Array.isArray(v) && v.every((item) => typeof item === "string"));
|
|
46797
|
+
if (hasValidValues) {
|
|
46798
|
+
const generatedAt = new Date(data.generatedAt).getTime();
|
|
46799
|
+
if (!_internals22.isCacheStale(map3, generatedAt)) {
|
|
46800
|
+
return map3;
|
|
46801
|
+
}
|
|
46802
|
+
if (options?.skipRebuild) {
|
|
46803
|
+
return map3;
|
|
46804
|
+
}
|
|
46805
|
+
}
|
|
46798
46806
|
}
|
|
46799
|
-
|
|
46807
|
+
if (options?.skipRebuild) {
|
|
46808
|
+
return {};
|
|
46809
|
+
}
|
|
46810
|
+
} catch {
|
|
46811
|
+
if (options?.skipRebuild) {
|
|
46812
|
+
return {};
|
|
46813
|
+
}
|
|
46814
|
+
}
|
|
46815
|
+
}
|
|
46816
|
+
if (options?.skipRebuild) {
|
|
46817
|
+
return {};
|
|
46800
46818
|
}
|
|
46801
46819
|
return _internals22.buildImpactMap(cwd);
|
|
46802
46820
|
}
|
|
@@ -46813,7 +46831,7 @@ async function saveImpactMap(cwd, impactMap) {
|
|
|
46813
46831
|
};
|
|
46814
46832
|
fs17.writeFileSync(cachePath, JSON.stringify(data, null, 2), "utf-8");
|
|
46815
46833
|
}
|
|
46816
|
-
async function analyzeImpact(changedFiles, cwd) {
|
|
46834
|
+
async function analyzeImpact(changedFiles, cwd, budget) {
|
|
46817
46835
|
if (!Array.isArray(changedFiles)) {
|
|
46818
46836
|
const emptyMap = {};
|
|
46819
46837
|
return {
|
|
@@ -46827,24 +46845,49 @@ async function analyzeImpact(changedFiles, cwd) {
|
|
|
46827
46845
|
const impactMap = await _internals22.loadImpactMap(cwd);
|
|
46828
46846
|
const impactedTestsSet = new Set;
|
|
46829
46847
|
const untestedFiles = [];
|
|
46848
|
+
let visitedCount = 0;
|
|
46849
|
+
let budgetExceeded = false;
|
|
46830
46850
|
for (const changedFile of validFiles) {
|
|
46851
|
+
if (budget !== undefined && visitedCount >= budget) {
|
|
46852
|
+
budgetExceeded = true;
|
|
46853
|
+
break;
|
|
46854
|
+
}
|
|
46831
46855
|
const normalizedChanged = normalizePath(path34.resolve(changedFile));
|
|
46832
46856
|
const tests = impactMap[normalizedChanged];
|
|
46833
46857
|
if (tests && tests.length > 0) {
|
|
46834
46858
|
for (const test of tests) {
|
|
46859
|
+
if (budget !== undefined && visitedCount >= budget) {
|
|
46860
|
+
budgetExceeded = true;
|
|
46861
|
+
break;
|
|
46862
|
+
}
|
|
46835
46863
|
impactedTestsSet.add(test);
|
|
46864
|
+
visitedCount++;
|
|
46836
46865
|
}
|
|
46866
|
+
if (budgetExceeded)
|
|
46867
|
+
break;
|
|
46837
46868
|
} else {
|
|
46838
46869
|
let found = false;
|
|
46839
46870
|
for (const [sourcePath, tests2] of Object.entries(impactMap)) {
|
|
46871
|
+
if (budget !== undefined && visitedCount >= budget) {
|
|
46872
|
+
budgetExceeded = true;
|
|
46873
|
+
break;
|
|
46874
|
+
}
|
|
46840
46875
|
if (sourcePath.endsWith(changedFile) || changedFile.endsWith(sourcePath)) {
|
|
46841
46876
|
for (const test of tests2) {
|
|
46877
|
+
if (budget !== undefined && visitedCount >= budget) {
|
|
46878
|
+
budgetExceeded = true;
|
|
46879
|
+
break;
|
|
46880
|
+
}
|
|
46842
46881
|
impactedTestsSet.add(test);
|
|
46882
|
+
visitedCount++;
|
|
46843
46883
|
}
|
|
46884
|
+
if (budgetExceeded)
|
|
46885
|
+
break;
|
|
46844
46886
|
found = true;
|
|
46845
|
-
break;
|
|
46846
46887
|
}
|
|
46847
46888
|
}
|
|
46889
|
+
if (budgetExceeded)
|
|
46890
|
+
break;
|
|
46848
46891
|
if (!found) {
|
|
46849
46892
|
untestedFiles.push(changedFile);
|
|
46850
46893
|
}
|
|
@@ -46862,7 +46905,8 @@ async function analyzeImpact(changedFiles, cwd) {
|
|
|
46862
46905
|
impactedTests,
|
|
46863
46906
|
unrelatedTests,
|
|
46864
46907
|
untestedFiles,
|
|
46865
|
-
impactMap
|
|
46908
|
+
impactMap,
|
|
46909
|
+
budgetExceeded
|
|
46866
46910
|
};
|
|
46867
46911
|
}
|
|
46868
46912
|
var IMPORT_REGEX_ES, IMPORT_REGEX_REQUIRE, IMPORT_REGEX_REEXPORT, TS_EXTENSIONS, PYTHON_EXTENSIONS, GO_EXTENSIONS, EXTENSIONS_TO_TRY, goModuleCache, _internals22;
|
|
@@ -47724,6 +47768,25 @@ var init_dispatch = __esm(() => {
|
|
|
47724
47768
|
// src/tools/test-runner.ts
|
|
47725
47769
|
import * as fs22 from "fs";
|
|
47726
47770
|
import * as path39 from "path";
|
|
47771
|
+
async function estimateFanOut(sourceFiles, cwd) {
|
|
47772
|
+
try {
|
|
47773
|
+
const impactMap = await loadImpactMap(cwd, { skipRebuild: true });
|
|
47774
|
+
const uniqueTestFiles = new Set;
|
|
47775
|
+
for (const sourceFile of sourceFiles) {
|
|
47776
|
+
const resolvedPath = path39.resolve(cwd, sourceFile);
|
|
47777
|
+
const normalizedPath = resolvedPath.replace(/\\/g, "/");
|
|
47778
|
+
const testFiles = impactMap[normalizedPath];
|
|
47779
|
+
if (testFiles) {
|
|
47780
|
+
for (const testFile of testFiles) {
|
|
47781
|
+
uniqueTestFiles.add(testFile);
|
|
47782
|
+
}
|
|
47783
|
+
}
|
|
47784
|
+
}
|
|
47785
|
+
return { estimatedCount: uniqueTestFiles.size };
|
|
47786
|
+
} catch {
|
|
47787
|
+
return { estimatedCount: 0 };
|
|
47788
|
+
}
|
|
47789
|
+
}
|
|
47727
47790
|
function isAbsolutePath(str) {
|
|
47728
47791
|
if (str.startsWith("/"))
|
|
47729
47792
|
return true;
|
|
@@ -49068,6 +49131,18 @@ var init_test_runner = __esm(() => {
|
|
|
49068
49131
|
};
|
|
49069
49132
|
return JSON.stringify(errorResult, null, 2);
|
|
49070
49133
|
}
|
|
49134
|
+
const estimate = await estimateFanOut(sourceFiles, workingDir);
|
|
49135
|
+
if (estimate.estimatedCount > MAX_SAFE_TEST_FILES) {
|
|
49136
|
+
const errorResult = {
|
|
49137
|
+
success: false,
|
|
49138
|
+
framework,
|
|
49139
|
+
scope,
|
|
49140
|
+
error: "Estimated test file count exceeds safe maximum",
|
|
49141
|
+
message: `Scope "graph" resolution would produce approximately ${estimate.estimatedCount} test files, which exceeds the safe limit of ${MAX_SAFE_TEST_FILES}. Break the source files into smaller batches and retry.`,
|
|
49142
|
+
outcome: "scope_exceeded"
|
|
49143
|
+
};
|
|
49144
|
+
return JSON.stringify(errorResult, null, 2);
|
|
49145
|
+
}
|
|
49071
49146
|
const graphTestFiles = await getTestFilesFromGraph(sourceFiles, workingDir);
|
|
49072
49147
|
if (graphTestFiles.length > 0) {
|
|
49073
49148
|
testFiles = graphTestFiles;
|
|
@@ -49106,8 +49181,31 @@ var init_test_runner = __esm(() => {
|
|
|
49106
49181
|
};
|
|
49107
49182
|
return JSON.stringify(errorResult, null, 2);
|
|
49108
49183
|
}
|
|
49184
|
+
const estimate = await estimateFanOut(sourceFiles, workingDir);
|
|
49185
|
+
if (estimate.estimatedCount > MAX_SAFE_TEST_FILES) {
|
|
49186
|
+
const errorResult = {
|
|
49187
|
+
success: false,
|
|
49188
|
+
framework,
|
|
49189
|
+
scope,
|
|
49190
|
+
error: "Estimated test file count exceeds safe maximum",
|
|
49191
|
+
message: `Scope "impact" resolution would produce approximately ${estimate.estimatedCount} test files, which exceeds the safe limit of ${MAX_SAFE_TEST_FILES}. Break the source files into smaller batches and retry.`,
|
|
49192
|
+
outcome: "scope_exceeded"
|
|
49193
|
+
};
|
|
49194
|
+
return JSON.stringify(errorResult, null, 2);
|
|
49195
|
+
}
|
|
49109
49196
|
try {
|
|
49110
|
-
const impactResult = await analyzeImpact(sourceFiles, workingDir);
|
|
49197
|
+
const impactResult = await analyzeImpact(sourceFiles, workingDir, MAX_SAFE_TEST_FILES);
|
|
49198
|
+
if (impactResult.budgetExceeded) {
|
|
49199
|
+
const errorResult = {
|
|
49200
|
+
success: false,
|
|
49201
|
+
framework,
|
|
49202
|
+
scope,
|
|
49203
|
+
error: "Budget exceeded during impact analysis",
|
|
49204
|
+
message: `Impact analysis exceeded safe budget of ${MAX_SAFE_TEST_FILES} test files.`,
|
|
49205
|
+
outcome: "scope_exceeded"
|
|
49206
|
+
};
|
|
49207
|
+
return JSON.stringify(errorResult, null, 2);
|
|
49208
|
+
}
|
|
49111
49209
|
if (impactResult.impactedTests.length > 0) {
|
|
49112
49210
|
testFiles = impactResult.impactedTests.map((absPath) => {
|
|
49113
49211
|
const relativePath = path39.relative(workingDir, absPath);
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ var package_default;
|
|
|
33
33
|
var init_package = __esm(() => {
|
|
34
34
|
package_default = {
|
|
35
35
|
name: "opencode-swarm",
|
|
36
|
-
version: "7.
|
|
36
|
+
version: "7.20.0",
|
|
37
37
|
description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
|
|
38
38
|
main: "dist/index.js",
|
|
39
39
|
types: "dist/index.d.ts",
|
|
@@ -24183,6 +24183,91 @@ function extractStatusCode(errorMsg) {
|
|
|
24183
24183
|
}
|
|
24184
24184
|
return null;
|
|
24185
24185
|
}
|
|
24186
|
+
function isPlainObject2(value) {
|
|
24187
|
+
return typeof value === "object" && value !== null && (value.constructor === Object || Object.getPrototypeOf(value) === null);
|
|
24188
|
+
}
|
|
24189
|
+
function readSignalField(source, key) {
|
|
24190
|
+
try {
|
|
24191
|
+
return source[key];
|
|
24192
|
+
} catch {
|
|
24193
|
+
return;
|
|
24194
|
+
}
|
|
24195
|
+
}
|
|
24196
|
+
function pushSignalValue(parts2, value) {
|
|
24197
|
+
if (typeof value === "string") {
|
|
24198
|
+
parts2.push(value);
|
|
24199
|
+
return;
|
|
24200
|
+
}
|
|
24201
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
24202
|
+
parts2.push(String(value));
|
|
24203
|
+
}
|
|
24204
|
+
}
|
|
24205
|
+
function appendSelectedFields(parts2, source, keys) {
|
|
24206
|
+
for (const key of keys) {
|
|
24207
|
+
pushSignalValue(parts2, readSignalField(source, key));
|
|
24208
|
+
}
|
|
24209
|
+
}
|
|
24210
|
+
function appendNestedErrorSignal(parts2, value) {
|
|
24211
|
+
if (typeof value === "string") {
|
|
24212
|
+
parts2.push(value);
|
|
24213
|
+
return;
|
|
24214
|
+
}
|
|
24215
|
+
if (value instanceof Error) {
|
|
24216
|
+
parts2.push(value.name, value.message);
|
|
24217
|
+
appendSelectedFields(parts2, value, [
|
|
24218
|
+
"code",
|
|
24219
|
+
"status",
|
|
24220
|
+
"statusCode"
|
|
24221
|
+
]);
|
|
24222
|
+
return;
|
|
24223
|
+
}
|
|
24224
|
+
if (!isPlainObject2(value))
|
|
24225
|
+
return;
|
|
24226
|
+
appendSelectedFields(parts2, value, [
|
|
24227
|
+
"code",
|
|
24228
|
+
"status",
|
|
24229
|
+
"statusCode",
|
|
24230
|
+
"message",
|
|
24231
|
+
"error_type"
|
|
24232
|
+
]);
|
|
24233
|
+
}
|
|
24234
|
+
function extractErrorSignal(errorContent) {
|
|
24235
|
+
if (typeof errorContent === "string")
|
|
24236
|
+
return errorContent;
|
|
24237
|
+
if (errorContent == null)
|
|
24238
|
+
return "";
|
|
24239
|
+
const parts2 = [];
|
|
24240
|
+
try {
|
|
24241
|
+
if (errorContent instanceof Error) {
|
|
24242
|
+
parts2.push(errorContent.name, errorContent.message);
|
|
24243
|
+
appendSelectedFields(parts2, errorContent, ["code", "status", "statusCode"]);
|
|
24244
|
+
return parts2.join(" ");
|
|
24245
|
+
}
|
|
24246
|
+
if (!isPlainObject2(errorContent))
|
|
24247
|
+
return "";
|
|
24248
|
+
appendSelectedFields(parts2, errorContent, [
|
|
24249
|
+
"code",
|
|
24250
|
+
"status",
|
|
24251
|
+
"statusCode",
|
|
24252
|
+
"message",
|
|
24253
|
+
"error_type"
|
|
24254
|
+
]);
|
|
24255
|
+
appendNestedErrorSignal(parts2, readSignalField(errorContent, "error"));
|
|
24256
|
+
const metadata2 = readSignalField(errorContent, "metadata");
|
|
24257
|
+
if (isPlainObject2(metadata2)) {
|
|
24258
|
+
appendSelectedFields(parts2, metadata2, [
|
|
24259
|
+
"code",
|
|
24260
|
+
"status",
|
|
24261
|
+
"statusCode",
|
|
24262
|
+
"error_type"
|
|
24263
|
+
]);
|
|
24264
|
+
}
|
|
24265
|
+
appendNestedErrorSignal(parts2, readSignalField(errorContent, "cause"));
|
|
24266
|
+
} catch {
|
|
24267
|
+
return parts2.join(" ");
|
|
24268
|
+
}
|
|
24269
|
+
return parts2.join(" ");
|
|
24270
|
+
}
|
|
24186
24271
|
function getStoredInputArgs(callID) {
|
|
24187
24272
|
return storedInputArgs.get(callID);
|
|
24188
24273
|
}
|
|
@@ -25392,16 +25477,17 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
|
|
|
25392
25477
|
if (hasError) {
|
|
25393
25478
|
const outputStr = typeof output.output === "string" ? output.output : "";
|
|
25394
25479
|
const errorContent = output.error ?? outputStr;
|
|
25395
|
-
const
|
|
25480
|
+
const errorSignal = extractErrorSignal(errorContent);
|
|
25481
|
+
const extractedStatus = extractStatusCode(errorSignal);
|
|
25396
25482
|
const isTransientStatusCode = extractedStatus !== null && TRANSIENT_STATUS_CODES.has(extractedStatus);
|
|
25397
|
-
const isTransientPatternMatch =
|
|
25483
|
+
const isTransientPatternMatch = TRANSIENT_MODEL_ERROR_PATTERN.test(errorSignal);
|
|
25398
25484
|
const isTransientMatch = isTransientStatusCode || isTransientPatternMatch;
|
|
25399
25485
|
const isTransient = !!session && isTransientMatch && window2.transientRetryCount < cfg.max_transient_retries;
|
|
25400
|
-
const isDegraded = !isTransient &&
|
|
25486
|
+
const isDegraded = !isTransient && DEGRADED_ERROR_PATTERN.test(errorSignal);
|
|
25401
25487
|
if (isTransient) {
|
|
25402
25488
|
window2.transientRetryCount++;
|
|
25403
25489
|
} else if (isDegraded) {
|
|
25404
|
-
const isContentFilter =
|
|
25490
|
+
const isContentFilter = CONTENT_FILTER_PATTERN.test(errorSignal);
|
|
25405
25491
|
if (session && !session.modelFallbackExhausted) {
|
|
25406
25492
|
session.model_fallback_index++;
|
|
25407
25493
|
const baseAgentName = session.agentName ? session.agentName.replace(/^[^_]+[_]/, "") : "";
|
|
@@ -26058,7 +26144,7 @@ var init_guardrails = __esm(() => {
|
|
|
26058
26144
|
]);
|
|
26059
26145
|
storedInputArgs = new Map;
|
|
26060
26146
|
TRANSIENT_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504, 529]);
|
|
26061
|
-
TRANSIENT_MODEL_ERROR_PATTERN = /rate.?limit|429|500|502|503|504|529|timeout|overloaded|model.?not.?found|temporarily.?unavailable|server.?error|connection.?(refused|reset|timeout)|bad.?gateway|gateway.?timeout|internal.?server.?error|service.?unavailable/i;
|
|
26147
|
+
TRANSIENT_MODEL_ERROR_PATTERN = /rate.?limit|429|500|502|503|504|529|timeout|overloaded|model.?not.?found|temporarily.?unavailable|provider.?unavailable|server.?error|connection.?(refused|reset|timeout|lost)|bad.?gateway|gateway.?timeout|internal.?server.?error|service.?unavailable/i;
|
|
26062
26148
|
DEGRADED_ERROR_PATTERN = /context.?length|token.?(limit|budget)|input.?too.?long|content.?filter|exceeds?.?(maximum.?)?tokens|maximum.?context|context.?window|too.?many.?tokens|prompt.?too.?long|message.?too.?long|request.?too.?large|max.?tokens/i;
|
|
26063
26149
|
CONTENT_FILTER_PATTERN = /content.?filter/i;
|
|
26064
26150
|
toolCallsSinceLastWrite = new Map;
|
|
@@ -28257,7 +28343,7 @@ __export(exports_util2, {
|
|
|
28257
28343
|
jsonStringifyReplacer: () => jsonStringifyReplacer2,
|
|
28258
28344
|
joinValues: () => joinValues2,
|
|
28259
28345
|
issue: () => issue2,
|
|
28260
|
-
isPlainObject: () =>
|
|
28346
|
+
isPlainObject: () => isPlainObject3,
|
|
28261
28347
|
isObject: () => isObject2,
|
|
28262
28348
|
hexToUint8Array: () => hexToUint8Array2,
|
|
28263
28349
|
getSizableOrigin: () => getSizableOrigin2,
|
|
@@ -28425,7 +28511,7 @@ function esc2(str) {
|
|
|
28425
28511
|
function isObject2(data) {
|
|
28426
28512
|
return typeof data === "object" && data !== null && !Array.isArray(data);
|
|
28427
28513
|
}
|
|
28428
|
-
function
|
|
28514
|
+
function isPlainObject3(o) {
|
|
28429
28515
|
if (isObject2(o) === false)
|
|
28430
28516
|
return false;
|
|
28431
28517
|
const ctor = o.constructor;
|
|
@@ -28440,7 +28526,7 @@ function isPlainObject2(o) {
|
|
|
28440
28526
|
return true;
|
|
28441
28527
|
}
|
|
28442
28528
|
function shallowClone2(o) {
|
|
28443
|
-
if (
|
|
28529
|
+
if (isPlainObject3(o))
|
|
28444
28530
|
return { ...o };
|
|
28445
28531
|
if (Array.isArray(o))
|
|
28446
28532
|
return [...o];
|
|
@@ -28566,7 +28652,7 @@ function omit2(schema, mask) {
|
|
|
28566
28652
|
return clone2(schema, def);
|
|
28567
28653
|
}
|
|
28568
28654
|
function extend2(schema, shape) {
|
|
28569
|
-
if (!
|
|
28655
|
+
if (!isPlainObject3(shape)) {
|
|
28570
28656
|
throw new Error("Invalid input to extend: expected a plain object");
|
|
28571
28657
|
}
|
|
28572
28658
|
const checks3 = schema._zod.def.checks;
|
|
@@ -28585,7 +28671,7 @@ function extend2(schema, shape) {
|
|
|
28585
28671
|
return clone2(schema, def);
|
|
28586
28672
|
}
|
|
28587
28673
|
function safeExtend2(schema, shape) {
|
|
28588
|
-
if (!
|
|
28674
|
+
if (!isPlainObject3(shape)) {
|
|
28589
28675
|
throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
28590
28676
|
}
|
|
28591
28677
|
const def = {
|
|
@@ -29968,7 +30054,7 @@ function mergeValues2(a, b) {
|
|
|
29968
30054
|
if (a instanceof Date && b instanceof Date && +a === +b) {
|
|
29969
30055
|
return { valid: true, data: a };
|
|
29970
30056
|
}
|
|
29971
|
-
if (
|
|
30057
|
+
if (isPlainObject3(a) && isPlainObject3(b)) {
|
|
29972
30058
|
const bKeys = Object.keys(b);
|
|
29973
30059
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
29974
30060
|
const newObj = { ...a, ...b };
|
|
@@ -31065,7 +31151,7 @@ var init_schemas3 = __esm(() => {
|
|
|
31065
31151
|
$ZodType2.init(inst, def);
|
|
31066
31152
|
inst._zod.parse = (payload, ctx) => {
|
|
31067
31153
|
const input = payload.value;
|
|
31068
|
-
if (!
|
|
31154
|
+
if (!isPlainObject3(input)) {
|
|
31069
31155
|
payload.issues.push({
|
|
31070
31156
|
expected: "record",
|
|
31071
31157
|
code: "invalid_type",
|
|
@@ -55321,18 +55407,36 @@ async function buildImpactMap(cwd) {
|
|
|
55321
55407
|
await _internals28.saveImpactMap(cwd, impactMap);
|
|
55322
55408
|
return impactMap;
|
|
55323
55409
|
}
|
|
55324
|
-
async function loadImpactMap(cwd) {
|
|
55410
|
+
async function loadImpactMap(cwd, options) {
|
|
55325
55411
|
const cachePath = path41.join(cwd, ".swarm", "cache", "impact-map.json");
|
|
55326
55412
|
if (fs24.existsSync(cachePath)) {
|
|
55327
55413
|
try {
|
|
55328
55414
|
const content = fs24.readFileSync(cachePath, "utf-8");
|
|
55329
55415
|
const data = JSON.parse(content);
|
|
55330
|
-
|
|
55331
|
-
|
|
55332
|
-
|
|
55333
|
-
|
|
55416
|
+
if (data.map !== null && typeof data.map === "object" && !Array.isArray(data.map)) {
|
|
55417
|
+
const map3 = data.map;
|
|
55418
|
+
const hasValidValues = Object.values(map3).every((v) => Array.isArray(v) && v.every((item) => typeof item === "string"));
|
|
55419
|
+
if (hasValidValues) {
|
|
55420
|
+
const generatedAt = new Date(data.generatedAt).getTime();
|
|
55421
|
+
if (!_internals28.isCacheStale(map3, generatedAt)) {
|
|
55422
|
+
return map3;
|
|
55423
|
+
}
|
|
55424
|
+
if (options?.skipRebuild) {
|
|
55425
|
+
return map3;
|
|
55426
|
+
}
|
|
55427
|
+
}
|
|
55334
55428
|
}
|
|
55335
|
-
|
|
55429
|
+
if (options?.skipRebuild) {
|
|
55430
|
+
return {};
|
|
55431
|
+
}
|
|
55432
|
+
} catch {
|
|
55433
|
+
if (options?.skipRebuild) {
|
|
55434
|
+
return {};
|
|
55435
|
+
}
|
|
55436
|
+
}
|
|
55437
|
+
}
|
|
55438
|
+
if (options?.skipRebuild) {
|
|
55439
|
+
return {};
|
|
55336
55440
|
}
|
|
55337
55441
|
return _internals28.buildImpactMap(cwd);
|
|
55338
55442
|
}
|
|
@@ -55349,7 +55453,7 @@ async function saveImpactMap(cwd, impactMap) {
|
|
|
55349
55453
|
};
|
|
55350
55454
|
fs24.writeFileSync(cachePath, JSON.stringify(data, null, 2), "utf-8");
|
|
55351
55455
|
}
|
|
55352
|
-
async function analyzeImpact(changedFiles, cwd) {
|
|
55456
|
+
async function analyzeImpact(changedFiles, cwd, budget) {
|
|
55353
55457
|
if (!Array.isArray(changedFiles)) {
|
|
55354
55458
|
const emptyMap = {};
|
|
55355
55459
|
return {
|
|
@@ -55363,24 +55467,49 @@ async function analyzeImpact(changedFiles, cwd) {
|
|
|
55363
55467
|
const impactMap = await _internals28.loadImpactMap(cwd);
|
|
55364
55468
|
const impactedTestsSet = new Set;
|
|
55365
55469
|
const untestedFiles = [];
|
|
55470
|
+
let visitedCount = 0;
|
|
55471
|
+
let budgetExceeded = false;
|
|
55366
55472
|
for (const changedFile of validFiles) {
|
|
55473
|
+
if (budget !== undefined && visitedCount >= budget) {
|
|
55474
|
+
budgetExceeded = true;
|
|
55475
|
+
break;
|
|
55476
|
+
}
|
|
55367
55477
|
const normalizedChanged = normalizePath(path41.resolve(changedFile));
|
|
55368
55478
|
const tests = impactMap[normalizedChanged];
|
|
55369
55479
|
if (tests && tests.length > 0) {
|
|
55370
55480
|
for (const test of tests) {
|
|
55481
|
+
if (budget !== undefined && visitedCount >= budget) {
|
|
55482
|
+
budgetExceeded = true;
|
|
55483
|
+
break;
|
|
55484
|
+
}
|
|
55371
55485
|
impactedTestsSet.add(test);
|
|
55486
|
+
visitedCount++;
|
|
55372
55487
|
}
|
|
55488
|
+
if (budgetExceeded)
|
|
55489
|
+
break;
|
|
55373
55490
|
} else {
|
|
55374
55491
|
let found = false;
|
|
55375
55492
|
for (const [sourcePath, tests2] of Object.entries(impactMap)) {
|
|
55493
|
+
if (budget !== undefined && visitedCount >= budget) {
|
|
55494
|
+
budgetExceeded = true;
|
|
55495
|
+
break;
|
|
55496
|
+
}
|
|
55376
55497
|
if (sourcePath.endsWith(changedFile) || changedFile.endsWith(sourcePath)) {
|
|
55377
55498
|
for (const test of tests2) {
|
|
55499
|
+
if (budget !== undefined && visitedCount >= budget) {
|
|
55500
|
+
budgetExceeded = true;
|
|
55501
|
+
break;
|
|
55502
|
+
}
|
|
55378
55503
|
impactedTestsSet.add(test);
|
|
55504
|
+
visitedCount++;
|
|
55379
55505
|
}
|
|
55506
|
+
if (budgetExceeded)
|
|
55507
|
+
break;
|
|
55380
55508
|
found = true;
|
|
55381
|
-
break;
|
|
55382
55509
|
}
|
|
55383
55510
|
}
|
|
55511
|
+
if (budgetExceeded)
|
|
55512
|
+
break;
|
|
55384
55513
|
if (!found) {
|
|
55385
55514
|
untestedFiles.push(changedFile);
|
|
55386
55515
|
}
|
|
@@ -55398,7 +55527,8 @@ async function analyzeImpact(changedFiles, cwd) {
|
|
|
55398
55527
|
impactedTests,
|
|
55399
55528
|
unrelatedTests,
|
|
55400
55529
|
untestedFiles,
|
|
55401
|
-
impactMap
|
|
55530
|
+
impactMap,
|
|
55531
|
+
budgetExceeded
|
|
55402
55532
|
};
|
|
55403
55533
|
}
|
|
55404
55534
|
var IMPORT_REGEX_ES, IMPORT_REGEX_REQUIRE, IMPORT_REGEX_REEXPORT, TS_EXTENSIONS, PYTHON_EXTENSIONS, GO_EXTENSIONS, EXTENSIONS_TO_TRY, goModuleCache, _internals28;
|
|
@@ -56260,6 +56390,25 @@ var init_dispatch = __esm(() => {
|
|
|
56260
56390
|
// src/tools/test-runner.ts
|
|
56261
56391
|
import * as fs29 from "node:fs";
|
|
56262
56392
|
import * as path46 from "node:path";
|
|
56393
|
+
async function estimateFanOut(sourceFiles, cwd) {
|
|
56394
|
+
try {
|
|
56395
|
+
const impactMap = await loadImpactMap(cwd, { skipRebuild: true });
|
|
56396
|
+
const uniqueTestFiles = new Set;
|
|
56397
|
+
for (const sourceFile of sourceFiles) {
|
|
56398
|
+
const resolvedPath = path46.resolve(cwd, sourceFile);
|
|
56399
|
+
const normalizedPath = resolvedPath.replace(/\\/g, "/");
|
|
56400
|
+
const testFiles = impactMap[normalizedPath];
|
|
56401
|
+
if (testFiles) {
|
|
56402
|
+
for (const testFile of testFiles) {
|
|
56403
|
+
uniqueTestFiles.add(testFile);
|
|
56404
|
+
}
|
|
56405
|
+
}
|
|
56406
|
+
}
|
|
56407
|
+
return { estimatedCount: uniqueTestFiles.size };
|
|
56408
|
+
} catch {
|
|
56409
|
+
return { estimatedCount: 0 };
|
|
56410
|
+
}
|
|
56411
|
+
}
|
|
56263
56412
|
function isAbsolutePath(str) {
|
|
56264
56413
|
if (str.startsWith("/"))
|
|
56265
56414
|
return true;
|
|
@@ -57604,6 +57753,18 @@ var init_test_runner = __esm(() => {
|
|
|
57604
57753
|
};
|
|
57605
57754
|
return JSON.stringify(errorResult, null, 2);
|
|
57606
57755
|
}
|
|
57756
|
+
const estimate = await estimateFanOut(sourceFiles, workingDir);
|
|
57757
|
+
if (estimate.estimatedCount > MAX_SAFE_TEST_FILES) {
|
|
57758
|
+
const errorResult = {
|
|
57759
|
+
success: false,
|
|
57760
|
+
framework,
|
|
57761
|
+
scope,
|
|
57762
|
+
error: "Estimated test file count exceeds safe maximum",
|
|
57763
|
+
message: `Scope "graph" resolution would produce approximately ${estimate.estimatedCount} test files, which exceeds the safe limit of ${MAX_SAFE_TEST_FILES}. Break the source files into smaller batches and retry.`,
|
|
57764
|
+
outcome: "scope_exceeded"
|
|
57765
|
+
};
|
|
57766
|
+
return JSON.stringify(errorResult, null, 2);
|
|
57767
|
+
}
|
|
57607
57768
|
const graphTestFiles = await getTestFilesFromGraph(sourceFiles, workingDir);
|
|
57608
57769
|
if (graphTestFiles.length > 0) {
|
|
57609
57770
|
testFiles = graphTestFiles;
|
|
@@ -57642,8 +57803,31 @@ var init_test_runner = __esm(() => {
|
|
|
57642
57803
|
};
|
|
57643
57804
|
return JSON.stringify(errorResult, null, 2);
|
|
57644
57805
|
}
|
|
57806
|
+
const estimate = await estimateFanOut(sourceFiles, workingDir);
|
|
57807
|
+
if (estimate.estimatedCount > MAX_SAFE_TEST_FILES) {
|
|
57808
|
+
const errorResult = {
|
|
57809
|
+
success: false,
|
|
57810
|
+
framework,
|
|
57811
|
+
scope,
|
|
57812
|
+
error: "Estimated test file count exceeds safe maximum",
|
|
57813
|
+
message: `Scope "impact" resolution would produce approximately ${estimate.estimatedCount} test files, which exceeds the safe limit of ${MAX_SAFE_TEST_FILES}. Break the source files into smaller batches and retry.`,
|
|
57814
|
+
outcome: "scope_exceeded"
|
|
57815
|
+
};
|
|
57816
|
+
return JSON.stringify(errorResult, null, 2);
|
|
57817
|
+
}
|
|
57645
57818
|
try {
|
|
57646
|
-
const impactResult = await analyzeImpact(sourceFiles, workingDir);
|
|
57819
|
+
const impactResult = await analyzeImpact(sourceFiles, workingDir, MAX_SAFE_TEST_FILES);
|
|
57820
|
+
if (impactResult.budgetExceeded) {
|
|
57821
|
+
const errorResult = {
|
|
57822
|
+
success: false,
|
|
57823
|
+
framework,
|
|
57824
|
+
scope,
|
|
57825
|
+
error: "Budget exceeded during impact analysis",
|
|
57826
|
+
message: `Impact analysis exceeded safe budget of ${MAX_SAFE_TEST_FILES} test files.`,
|
|
57827
|
+
outcome: "scope_exceeded"
|
|
57828
|
+
};
|
|
57829
|
+
return JSON.stringify(errorResult, null, 2);
|
|
57830
|
+
}
|
|
57647
57831
|
if (impactResult.impactedTests.length > 0) {
|
|
57648
57832
|
testFiles = impactResult.impactedTests.map((absPath) => {
|
|
57649
57833
|
const relativePath = path46.relative(workingDir, absPath);
|
|
@@ -3,6 +3,7 @@ export interface TestImpactResult {
|
|
|
3
3
|
unrelatedTests: string[];
|
|
4
4
|
untestedFiles: string[];
|
|
5
5
|
impactMap: Record<string, string[]>;
|
|
6
|
+
budgetExceeded?: boolean;
|
|
6
7
|
}
|
|
7
8
|
declare function normalizePath(p: string): string;
|
|
8
9
|
declare function isCacheStale(impactMap: Record<string, string[]>, generatedAtMs: number): boolean;
|
|
@@ -30,7 +31,12 @@ export declare const _internals: {
|
|
|
30
31
|
_clearGoModuleCache: typeof _clearGoModuleCache;
|
|
31
32
|
};
|
|
32
33
|
export declare function buildImpactMap(cwd: string): Promise<Record<string, string[]>>;
|
|
33
|
-
export
|
|
34
|
+
export interface LoadImpactMapOptions {
|
|
35
|
+
/** If true and cache is stale, return the stale map instead of rebuilding.
|
|
36
|
+
* Use for estimation-only reads where slight staleness is acceptable. */
|
|
37
|
+
skipRebuild?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare function loadImpactMap(cwd: string, options?: LoadImpactMapOptions): Promise<Record<string, string[]>>;
|
|
34
40
|
declare function saveImpactMap(cwd: string, impactMap: Record<string, string[]>): Promise<void>;
|
|
35
|
-
export declare function analyzeImpact(changedFiles: string[], cwd: string): Promise<TestImpactResult>;
|
|
41
|
+
export declare function analyzeImpact(changedFiles: string[], cwd: string, budget?: number): Promise<TestImpactResult>;
|
|
36
42
|
export {};
|
|
@@ -5,6 +5,17 @@ export declare const DEFAULT_TIMEOUT_MS = 60000;
|
|
|
5
5
|
export declare const MAX_TIMEOUT_MS = 300000;
|
|
6
6
|
export declare const MAX_SAFE_TEST_FILES = 50;
|
|
7
7
|
export declare const MAX_SAFE_SOURCE_FILES = 1;
|
|
8
|
+
/**
|
|
9
|
+
* Estimate the fan-out (number of unique test files) for given source files
|
|
10
|
+
* by reading the cached impact map without spawning a subprocess.
|
|
11
|
+
* This is a pre-resolution check to prevent session blocking.
|
|
12
|
+
*
|
|
13
|
+
* Completes in <100ms by design — reads only the cached JSON and performs
|
|
14
|
+
* in-memory Set collection.
|
|
15
|
+
*/
|
|
16
|
+
export declare function estimateFanOut(sourceFiles: string[], cwd: string): Promise<{
|
|
17
|
+
estimatedCount: number;
|
|
18
|
+
}>;
|
|
8
19
|
export declare const SUPPORTED_FRAMEWORKS: readonly ["bun", "vitest", "jest", "mocha", "pytest", "cargo", "pester", "go-test", "maven", "gradle", "dotnet-test", "ctest", "swift-test", "dart-test", "rspec", "minitest"];
|
|
9
20
|
export type TestFramework = (typeof SUPPORTED_FRAMEWORKS)[number] | 'none';
|
|
10
21
|
export interface TestRunnerArgs {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.20.0",
|
|
4
4
|
"description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|