opencode-swarm 7.25.1 → 7.26.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/README.md +41 -8
- package/dist/cli/index.js +103 -289
- package/dist/hooks/guardrails.d.ts +22 -0
- package/dist/hooks/shell-write-detect.d.ts +110 -0
- package/dist/index.js +2515 -1343
- package/dist/scope/scope-persistence.d.ts +4 -3
- package/dist/tools/test-runner.d.ts +0 -10
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -38,6 +38,8 @@ Most AI coding tools let one model write code and ask that same model whether th
|
|
|
38
38
|
- 🔁 **Resumable sessions** — all state saved to `.swarm/`; pick up any project any day
|
|
39
39
|
- 🌐 **20 languages** — TypeScript, Python, Go, Rust, Java, Kotlin, C/C++, C#, Ruby, Swift, Dart, PHP, JavaScript, CSS, Bash, PowerShell, INI, Regex (extending: see [docs/adding-a-language.md](docs/adding-a-language.md))
|
|
40
40
|
- 🛡️ **Built-in security** — SAST, secrets scanning, dependency audit per task
|
|
41
|
+
- 📝 **Shell write detection** — Static analysis of POSIX/PowerShell/cmd commands to detect file writes (redirects, builtins, in-place editors, network downloads, archive extraction, git destructive ops) before execution
|
|
42
|
+
- 🔒 **Scope enforcement** — Validates write targets against declared scope with cross-process persistence and TTL expiry
|
|
41
43
|
- 🆓 **Free tier** — works with OpenCode Zen's free model roster
|
|
42
44
|
- ⚙️ **Fully configurable** — override any agent's model, disable agents, tune guardrails
|
|
43
45
|
|
|
@@ -45,16 +47,44 @@ Most AI coding tools let one model write code and ask that same model whether th
|
|
|
45
47
|
|
|
46
48
|
---
|
|
47
49
|
|
|
48
|
-
##
|
|
50
|
+
## Shell Write Detection
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
Swarm includes comprehensive static analysis for shell commands to detect and intercept file write operations before execution.
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
- **
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
54
|
+
### Shell Write Detection Features
|
|
55
|
+
|
|
56
|
+
- **POSIX shell detection** — Parses commands with `bash-parser` AST for accurate detection of:
|
|
57
|
+
- Redirect operators (`>`, `>>`, `>|`, `<<`, `<<-`)
|
|
58
|
+
- Here-documents and here-strings
|
|
59
|
+
- Write-effect builtins (`cp`, `mv`, `install`, `ln`, `truncate`, `dd`)
|
|
60
|
+
- In-place editors (`sed -i`, `perl -i`, `awk -i`)
|
|
61
|
+
- Interpreter eval (`python -c`, `node -e`, `bun -e`, `ruby -e`, `php -r`)
|
|
62
|
+
- Network downloaders (`curl -o`, `wget -O`, `scp`)
|
|
63
|
+
- Archive extraction (`tar -x`, `unzip`, `gunzip`)
|
|
64
|
+
- Git destructive operations (`git clean -fd`, `git reset --hard`)
|
|
65
|
+
|
|
66
|
+
- **Windows shell detection** — Uses regex heuristics for PowerShell and cmd.exe:
|
|
67
|
+
- PowerShell cmdlets: `Out-File`, `Set-Content`, `Add-Content`, `Copy-Item`, `Move-Item`
|
|
68
|
+
- cmd.exe builtins: `copy`, `move`, `ren`, `del`, `rd`, `md`
|
|
69
|
+
- Redirect operators (`>`, `>>`)
|
|
70
|
+
|
|
71
|
+
- **Interactive session denial** — Blocks commands that create persistent or open-ended sessions:
|
|
72
|
+
- POSIX: `watch`, `screen`, `tmux new-session`
|
|
73
|
+
- PowerShell: `Start-Process`
|
|
74
|
+
|
|
75
|
+
- **Cross-process scope enforcement** — Declared scope is persisted to `.swarm/scopes/scope-{taskId}.json` with:
|
|
76
|
+
- TTL expiry (default 24 hours)
|
|
77
|
+
- Symlink guards (O_NOFOLLOW + realpath containment)
|
|
78
|
+
- Schema versioning and fail-closed validation
|
|
79
|
+
|
|
80
|
+
### Security Patterns
|
|
81
|
+
|
|
82
|
+
The guardrails system blocks destructive shell commands targeting:
|
|
83
|
+
- System paths (`/root`, `/etc`, `C:\Windows`, etc.)
|
|
84
|
+
- Symlink/junction creation with external targets
|
|
85
|
+
- File operations under `.swarm/` directory
|
|
86
|
+
- Fork bombs and infinite loops
|
|
87
|
+
- Disk wiping and ransomware-grade operations
|
|
58
88
|
|
|
59
89
|
---
|
|
60
90
|
|
|
@@ -349,6 +379,9 @@ graph TB
|
|
|
349
379
|
| Plan reviewed before coding | ✅ | ❌ | ❌ |
|
|
350
380
|
| Every task reviewed + tested | ✅ | ❌ | ❌ |
|
|
351
381
|
| Different model for review vs. code | ✅ | ❌ | ❌ |
|
|
382
|
+
| Shell write detection (POSIX/PowerShell/cmd) | ✅ | ❌ | ❌ |
|
|
383
|
+
| Scope enforcement with cross-process persistence | ✅ | ❌ | ❌ |
|
|
384
|
+
| Interactive session detection and blocking | ✅ | ❌ | ❌ |
|
|
352
385
|
| Resumable sessions | ✅ | ❌ | ❌ |
|
|
353
386
|
| Built-in security scanning | ✅ | ❌ | ❌ |
|
|
354
387
|
| Learns from mistakes | ✅ | ❌ | ❌ |
|
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.26.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",
|
|
@@ -72,7 +72,7 @@ var init_package = __esm(() => {
|
|
|
72
72
|
],
|
|
73
73
|
scripts: {
|
|
74
74
|
clean: `bun -e "require('fs').rmSync('dist',{recursive:true,force:true})"`,
|
|
75
|
-
build: "bun run clean && bun run scripts/copy-grammars.ts && bun build src/index.ts --outdir dist --target node --format esm && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm && bun run scripts/copy-grammars.ts --to-dist && tsc --emitDeclarationOnly",
|
|
75
|
+
build: "bun run clean && bun run scripts/copy-grammars.ts && bun build src/index.ts --outdir dist --target node --format esm --external bash-parser && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --external bash-parser && bun run scripts/copy-grammars.ts --to-dist && tsc --emitDeclarationOnly",
|
|
76
76
|
typecheck: "tsc --noEmit",
|
|
77
77
|
test: "bun test",
|
|
78
78
|
lint: "biome lint .",
|
|
@@ -86,6 +86,7 @@ var init_package = __esm(() => {
|
|
|
86
86
|
"@opencode-ai/plugin": "^1.1.53",
|
|
87
87
|
"@opencode-ai/sdk": "^1.1.53",
|
|
88
88
|
"@vscode/tree-sitter-wasm": "^0.3.0",
|
|
89
|
+
"bash-parser": "^0.5.0",
|
|
89
90
|
"p-limit": "^7.3.0",
|
|
90
91
|
picomatch: "^4.0.4",
|
|
91
92
|
"proper-lockfile": "^4.1.2",
|
|
@@ -20894,6 +20895,73 @@ var init_scope_persistence = __esm(() => {
|
|
|
20894
20895
|
]);
|
|
20895
20896
|
});
|
|
20896
20897
|
|
|
20898
|
+
// src/hooks/shell-write-detect.ts
|
|
20899
|
+
import parse5 from "bash-parser";
|
|
20900
|
+
var REDIRECT_WRITE_TOKENS, REDIRECT_HERE_TOKENS, REDIRECT_ALL_WRITE_TOKENS, BUILTIN_WRITE_COMMANDS, INPLACE_EDIT_COMMANDS, INTERPRETER_EVAL_COMMANDS, NETWORK_DOWNLOAD_COMMANDS, ARCHIVE_EXTRACT_COMMANDS, PS_WRITE_CMDLETS, PS_WRITE_ALIASES, CMD_WRITE_BUILTINS;
|
|
20901
|
+
var init_shell_write_detect = __esm(() => {
|
|
20902
|
+
REDIRECT_WRITE_TOKENS = new Set([
|
|
20903
|
+
"GREAT",
|
|
20904
|
+
"DGREAT",
|
|
20905
|
+
"CLOBBER",
|
|
20906
|
+
"LESSGREAT"
|
|
20907
|
+
]);
|
|
20908
|
+
REDIRECT_HERE_TOKENS = new Set(["DLESS", "DLESSDASH"]);
|
|
20909
|
+
REDIRECT_ALL_WRITE_TOKENS = new Set([
|
|
20910
|
+
...REDIRECT_WRITE_TOKENS,
|
|
20911
|
+
...REDIRECT_HERE_TOKENS
|
|
20912
|
+
]);
|
|
20913
|
+
BUILTIN_WRITE_COMMANDS = new Set([
|
|
20914
|
+
"cp",
|
|
20915
|
+
"mv",
|
|
20916
|
+
"install",
|
|
20917
|
+
"ln",
|
|
20918
|
+
"truncate"
|
|
20919
|
+
]);
|
|
20920
|
+
INPLACE_EDIT_COMMANDS = new Set(["sed", "perl", "awk"]);
|
|
20921
|
+
INTERPRETER_EVAL_COMMANDS = new Set([
|
|
20922
|
+
"python",
|
|
20923
|
+
"python3",
|
|
20924
|
+
"python2",
|
|
20925
|
+
"node",
|
|
20926
|
+
"bun",
|
|
20927
|
+
"ruby",
|
|
20928
|
+
"perl",
|
|
20929
|
+
"php"
|
|
20930
|
+
]);
|
|
20931
|
+
NETWORK_DOWNLOAD_COMMANDS = new Set(["curl", "wget", "scp"]);
|
|
20932
|
+
ARCHIVE_EXTRACT_COMMANDS = new Set([
|
|
20933
|
+
"tar",
|
|
20934
|
+
"unzip",
|
|
20935
|
+
"gunzip",
|
|
20936
|
+
"gzip",
|
|
20937
|
+
"bzip2",
|
|
20938
|
+
"xz",
|
|
20939
|
+
"7z",
|
|
20940
|
+
"rar"
|
|
20941
|
+
]);
|
|
20942
|
+
PS_WRITE_CMDLETS = new Set([
|
|
20943
|
+
"Out-File",
|
|
20944
|
+
"Set-Content",
|
|
20945
|
+
"Add-Content",
|
|
20946
|
+
"Clear-Content",
|
|
20947
|
+
"Copy-Item",
|
|
20948
|
+
"Move-Item",
|
|
20949
|
+
"Remove-Item",
|
|
20950
|
+
"Invoke-WebRequest",
|
|
20951
|
+
"Start-Process"
|
|
20952
|
+
]);
|
|
20953
|
+
PS_WRITE_ALIASES = new Set(["echo", "write"]);
|
|
20954
|
+
CMD_WRITE_BUILTINS = new Set([
|
|
20955
|
+
"copy",
|
|
20956
|
+
"move",
|
|
20957
|
+
"type",
|
|
20958
|
+
"del",
|
|
20959
|
+
"rd",
|
|
20960
|
+
"md",
|
|
20961
|
+
"ren"
|
|
20962
|
+
]);
|
|
20963
|
+
});
|
|
20964
|
+
|
|
20897
20965
|
// src/hooks/conflict-resolution.ts
|
|
20898
20966
|
var init_conflict_resolution = __esm(() => {
|
|
20899
20967
|
init_state();
|
|
@@ -20968,6 +21036,7 @@ var init_guardrails = __esm(() => {
|
|
|
20968
21036
|
init_state();
|
|
20969
21037
|
init_telemetry();
|
|
20970
21038
|
init_utils();
|
|
21039
|
+
init_shell_write_detect();
|
|
20971
21040
|
init_bun_compat();
|
|
20972
21041
|
init_logger();
|
|
20973
21042
|
init_conflict_resolution();
|
|
@@ -22370,7 +22439,7 @@ var _parse2 = (_Err) => (schema, value, _ctx, _params) => {
|
|
|
22370
22439
|
throw e;
|
|
22371
22440
|
}
|
|
22372
22441
|
return result.value;
|
|
22373
|
-
},
|
|
22442
|
+
}, parse6, _parseAsync2 = (_Err) => async (schema, value, _ctx, params) => {
|
|
22374
22443
|
const ctx = _ctx ? Object.assign(_ctx, { async: true }) : { async: true };
|
|
22375
22444
|
let result = schema._zod.run({ value, issues: [] }, ctx);
|
|
22376
22445
|
if (result instanceof Promise)
|
|
@@ -22425,7 +22494,7 @@ var init_parse3 = __esm(() => {
|
|
|
22425
22494
|
init_core3();
|
|
22426
22495
|
init_errors4();
|
|
22427
22496
|
init_util2();
|
|
22428
|
-
|
|
22497
|
+
parse6 = /* @__PURE__ */ _parse2($ZodRealError2);
|
|
22429
22498
|
parseAsync3 = /* @__PURE__ */ _parseAsync2($ZodRealError2);
|
|
22430
22499
|
safeParse3 = /* @__PURE__ */ _safeParse2($ZodRealError2);
|
|
22431
22500
|
safeParseAsync3 = /* @__PURE__ */ _safeParseAsync2($ZodRealError2);
|
|
@@ -24915,10 +24984,10 @@ var init_schemas3 = __esm(() => {
|
|
|
24915
24984
|
throw new Error("implement() must be called with a function");
|
|
24916
24985
|
}
|
|
24917
24986
|
return function(...args) {
|
|
24918
|
-
const parsedArgs = inst._def.input ?
|
|
24987
|
+
const parsedArgs = inst._def.input ? parse6(inst._def.input, args) : args;
|
|
24919
24988
|
const result = Reflect.apply(func, this, parsedArgs);
|
|
24920
24989
|
if (inst._def.output) {
|
|
24921
|
-
return
|
|
24990
|
+
return parse6(inst._def.output, result);
|
|
24922
24991
|
}
|
|
24923
24992
|
return result;
|
|
24924
24993
|
};
|
|
@@ -32494,7 +32563,7 @@ __export(exports_core4, {
|
|
|
32494
32563
|
regexes: () => exports_regexes2,
|
|
32495
32564
|
prettifyError: () => prettifyError2,
|
|
32496
32565
|
parseAsync: () => parseAsync3,
|
|
32497
|
-
parse: () =>
|
|
32566
|
+
parse: () => parse6,
|
|
32498
32567
|
locales: () => exports_locales2,
|
|
32499
32568
|
isValidJWT: () => isValidJWT2,
|
|
32500
32569
|
isValidBase64URL: () => isValidBase64URL2,
|
|
@@ -32847,11 +32916,11 @@ var init_errors5 = __esm(() => {
|
|
|
32847
32916
|
});
|
|
32848
32917
|
|
|
32849
32918
|
// node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/parse.js
|
|
32850
|
-
var
|
|
32919
|
+
var parse8, parseAsync4, safeParse4, safeParseAsync4, encode4, decode4, encodeAsync4, decodeAsync4, safeEncode4, safeDecode4, safeEncodeAsync4, safeDecodeAsync4;
|
|
32851
32920
|
var init_parse4 = __esm(() => {
|
|
32852
32921
|
init_core4();
|
|
32853
32922
|
init_errors5();
|
|
32854
|
-
|
|
32923
|
+
parse8 = /* @__PURE__ */ _parse2(ZodRealError2);
|
|
32855
32924
|
parseAsync4 = /* @__PURE__ */ _parseAsync2(ZodRealError2);
|
|
32856
32925
|
safeParse4 = /* @__PURE__ */ _safeParse2(ZodRealError2);
|
|
32857
32926
|
safeParseAsync4 = /* @__PURE__ */ _safeParseAsync2(ZodRealError2);
|
|
@@ -33323,7 +33392,7 @@ var init_schemas4 = __esm(() => {
|
|
|
33323
33392
|
reg.add(inst, meta3);
|
|
33324
33393
|
return inst;
|
|
33325
33394
|
};
|
|
33326
|
-
inst.parse = (data, params) =>
|
|
33395
|
+
inst.parse = (data, params) => parse8(inst, data, params, { callee: inst.parse });
|
|
33327
33396
|
inst.safeParse = (data, params) => safeParse4(inst, data, params);
|
|
33328
33397
|
inst.parseAsync = async (data, params) => parseAsync4(inst, data, params, { callee: inst.parseAsync });
|
|
33329
33398
|
inst.safeParseAsync = async (data, params) => safeParseAsync4(inst, data, params);
|
|
@@ -33963,7 +34032,7 @@ __export(exports_external2, {
|
|
|
33963
34032
|
pipe: () => pipe2,
|
|
33964
34033
|
partialRecord: () => partialRecord2,
|
|
33965
34034
|
parseAsync: () => parseAsync4,
|
|
33966
|
-
parse: () =>
|
|
34035
|
+
parse: () => parse8,
|
|
33967
34036
|
overwrite: () => _overwrite2,
|
|
33968
34037
|
optional: () => optional2,
|
|
33969
34038
|
object: () => object2,
|
|
@@ -45999,14 +46068,7 @@ function defaultBuildTestCommand(profile, framework, files, dir = ".", opts = {}
|
|
|
45999
46068
|
return args;
|
|
46000
46069
|
}
|
|
46001
46070
|
case "vitest": {
|
|
46002
|
-
const args = [
|
|
46003
|
-
"npx",
|
|
46004
|
-
"vitest",
|
|
46005
|
-
"run",
|
|
46006
|
-
"--reporter=json",
|
|
46007
|
-
"--outputFile",
|
|
46008
|
-
".swarm/cache/test-runner-vitest.json"
|
|
46009
|
-
];
|
|
46071
|
+
const args = ["npx", "vitest", "run"];
|
|
46010
46072
|
if (coverage)
|
|
46011
46073
|
args.push("--coverage");
|
|
46012
46074
|
if (scope !== "all" && files.length > 0)
|
|
@@ -46014,7 +46076,7 @@ function defaultBuildTestCommand(profile, framework, files, dir = ".", opts = {}
|
|
|
46014
46076
|
return args;
|
|
46015
46077
|
}
|
|
46016
46078
|
case "jest": {
|
|
46017
|
-
const args = ["npx", "jest"
|
|
46079
|
+
const args = ["npx", "jest"];
|
|
46018
46080
|
if (coverage)
|
|
46019
46081
|
args.push("--coverage");
|
|
46020
46082
|
if (scope !== "all" && files.length > 0)
|
|
@@ -47249,42 +47311,6 @@ function sanitizeChangedFiles(changedFiles) {
|
|
|
47249
47311
|
const validFiles = changedFiles.filter((f) => typeof f === "string" && f.length > 0 && !DANGEROUS_PROPERTY_NAMES.has(f));
|
|
47250
47312
|
return validFiles.slice(0, MAX_CHANGED_FILES);
|
|
47251
47313
|
}
|
|
47252
|
-
function isTestRunResult(value) {
|
|
47253
|
-
return value === "pass" || value === "fail" || value === "skip";
|
|
47254
|
-
}
|
|
47255
|
-
function parseStoredRecord(value) {
|
|
47256
|
-
if (typeof value !== "object" || value === null)
|
|
47257
|
-
return null;
|
|
47258
|
-
const record3 = value;
|
|
47259
|
-
if (typeof record3.testFile !== "string" || record3.testFile.length === 0) {
|
|
47260
|
-
return null;
|
|
47261
|
-
}
|
|
47262
|
-
if (typeof record3.testName !== "string" || record3.testName.length === 0) {
|
|
47263
|
-
return null;
|
|
47264
|
-
}
|
|
47265
|
-
if (typeof record3.taskId !== "string" || record3.taskId.length === 0) {
|
|
47266
|
-
return null;
|
|
47267
|
-
}
|
|
47268
|
-
if (!isTestRunResult(record3.result))
|
|
47269
|
-
return null;
|
|
47270
|
-
if (typeof record3.durationMs !== "number" || !Number.isFinite(record3.durationMs)) {
|
|
47271
|
-
return null;
|
|
47272
|
-
}
|
|
47273
|
-
if (typeof record3.timestamp !== "string" || Number.isNaN(Date.parse(record3.timestamp))) {
|
|
47274
|
-
return null;
|
|
47275
|
-
}
|
|
47276
|
-
return {
|
|
47277
|
-
timestamp: record3.timestamp,
|
|
47278
|
-
taskId: record3.taskId,
|
|
47279
|
-
testFile: record3.testFile,
|
|
47280
|
-
testName: record3.testName,
|
|
47281
|
-
result: record3.result,
|
|
47282
|
-
durationMs: Math.max(0, record3.durationMs),
|
|
47283
|
-
errorMessage: typeof record3.errorMessage === "string" ? sanitizeErrorMessage(record3.errorMessage) : undefined,
|
|
47284
|
-
stackPrefix: typeof record3.stackPrefix === "string" ? sanitizeStackPrefix(record3.stackPrefix) : undefined,
|
|
47285
|
-
changedFiles: sanitizeChangedFiles(Array.isArray(record3.changedFiles) ? record3.changedFiles : [])
|
|
47286
|
-
};
|
|
47287
|
-
}
|
|
47288
47314
|
function appendTestRun(record3, workingDir) {
|
|
47289
47315
|
if (typeof record3.testFile !== "string" || record3.testFile.length === 0) {
|
|
47290
47316
|
throw new TypeError("testFile must be a non-empty string");
|
|
@@ -47322,16 +47348,16 @@ function appendTestRun(record3, workingDir) {
|
|
|
47322
47348
|
}
|
|
47323
47349
|
const existingRecords = readAllRecords(historyPath);
|
|
47324
47350
|
existingRecords.push(sanitizedRecord);
|
|
47325
|
-
const
|
|
47351
|
+
const recordsByFile = new Map;
|
|
47326
47352
|
for (const rec of existingRecords) {
|
|
47327
|
-
const
|
|
47328
|
-
if (!
|
|
47329
|
-
|
|
47353
|
+
const normalizedFile = rec.testFile.toLowerCase();
|
|
47354
|
+
if (!recordsByFile.has(normalizedFile)) {
|
|
47355
|
+
recordsByFile.set(normalizedFile, []);
|
|
47330
47356
|
}
|
|
47331
|
-
|
|
47357
|
+
recordsByFile.get(normalizedFile).push(rec);
|
|
47332
47358
|
}
|
|
47333
47359
|
const prunedRecords = [];
|
|
47334
|
-
for (const [, records] of
|
|
47360
|
+
for (const [, records] of recordsByFile) {
|
|
47335
47361
|
records.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
47336
47362
|
const toKeep = records.slice(-MAX_HISTORY_PER_TEST);
|
|
47337
47363
|
prunedRecords.push(...toKeep);
|
|
@@ -47371,9 +47397,8 @@ function readAllRecords(historyPath) {
|
|
|
47371
47397
|
}
|
|
47372
47398
|
try {
|
|
47373
47399
|
const parsed = JSON.parse(trimmed);
|
|
47374
|
-
|
|
47375
|
-
|
|
47376
|
-
records.push(record3);
|
|
47400
|
+
if (typeof parsed === "object" && parsed !== null && "testFile" in parsed && "testName" in parsed && "result" in parsed) {
|
|
47401
|
+
records.push(parsed);
|
|
47377
47402
|
}
|
|
47378
47403
|
} catch {}
|
|
47379
47404
|
}
|
|
@@ -48412,14 +48437,7 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
|
|
|
48412
48437
|
return args;
|
|
48413
48438
|
}
|
|
48414
48439
|
case "vitest": {
|
|
48415
|
-
const args = [
|
|
48416
|
-
"npx",
|
|
48417
|
-
"vitest",
|
|
48418
|
-
"run",
|
|
48419
|
-
"--reporter=json",
|
|
48420
|
-
"--outputFile",
|
|
48421
|
-
VITEST_JSON_OUTPUT_RELATIVE_PATH
|
|
48422
|
-
];
|
|
48440
|
+
const args = ["npx", "vitest", "run"];
|
|
48423
48441
|
if (coverage)
|
|
48424
48442
|
args.push("--coverage");
|
|
48425
48443
|
if (scope !== "all" && files.length > 0) {
|
|
@@ -48428,7 +48446,7 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
|
|
|
48428
48446
|
return args;
|
|
48429
48447
|
}
|
|
48430
48448
|
case "jest": {
|
|
48431
|
-
const args = ["npx", "jest"
|
|
48449
|
+
const args = ["npx", "jest"];
|
|
48432
48450
|
if (coverage)
|
|
48433
48451
|
args.push("--coverage");
|
|
48434
48452
|
if (scope !== "all" && files.length > 0) {
|
|
@@ -48524,122 +48542,6 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
|
|
|
48524
48542
|
return null;
|
|
48525
48543
|
}
|
|
48526
48544
|
}
|
|
48527
|
-
function mapFrameworkStatusToResult(status) {
|
|
48528
|
-
if (typeof status !== "string")
|
|
48529
|
-
return null;
|
|
48530
|
-
const normalized = status.toLowerCase();
|
|
48531
|
-
if (normalized === "pass" || normalized === "passed")
|
|
48532
|
-
return "pass";
|
|
48533
|
-
if (normalized === "fail" || normalized === "failed")
|
|
48534
|
-
return "fail";
|
|
48535
|
-
if (normalized === "skip" || normalized === "skipped" || normalized === "pending" || normalized === "todo") {
|
|
48536
|
-
return "skip";
|
|
48537
|
-
}
|
|
48538
|
-
return null;
|
|
48539
|
-
}
|
|
48540
|
-
function firstLine(value) {
|
|
48541
|
-
if (typeof value !== "string")
|
|
48542
|
-
return;
|
|
48543
|
-
const line = value.split(`
|
|
48544
|
-
`).find((part) => part.trim().length > 0)?.trim();
|
|
48545
|
-
return line && line.length > 0 ? line : undefined;
|
|
48546
|
-
}
|
|
48547
|
-
function parseJestLikeJsonTestResults(payload) {
|
|
48548
|
-
if (typeof payload !== "object" || payload === null)
|
|
48549
|
-
return [];
|
|
48550
|
-
const rawSuites = payload.testResults;
|
|
48551
|
-
if (!Array.isArray(rawSuites))
|
|
48552
|
-
return [];
|
|
48553
|
-
const parsed = [];
|
|
48554
|
-
for (const suite of rawSuites) {
|
|
48555
|
-
if (typeof suite !== "object" || suite === null)
|
|
48556
|
-
continue;
|
|
48557
|
-
const suiteObj = suite;
|
|
48558
|
-
const rawFile = typeof suiteObj.name === "string" ? suiteObj.name : typeof suiteObj.testFilePath === "string" ? suiteObj.testFilePath : undefined;
|
|
48559
|
-
if (!rawFile)
|
|
48560
|
-
continue;
|
|
48561
|
-
const testFile = rawFile.replace(/\\/g, "/");
|
|
48562
|
-
const assertionResults = suiteObj.assertionResults;
|
|
48563
|
-
if (!Array.isArray(assertionResults))
|
|
48564
|
-
continue;
|
|
48565
|
-
for (const assertion of assertionResults) {
|
|
48566
|
-
if (typeof assertion !== "object" || assertion === null)
|
|
48567
|
-
continue;
|
|
48568
|
-
const assertionObj = assertion;
|
|
48569
|
-
const result = mapFrameworkStatusToResult(assertionObj.status);
|
|
48570
|
-
const testName = typeof assertionObj.fullName === "string" ? assertionObj.fullName : typeof assertionObj.title === "string" ? assertionObj.title : undefined;
|
|
48571
|
-
if (!result || !testName || testName.length === 0)
|
|
48572
|
-
continue;
|
|
48573
|
-
const failureMessages = Array.isArray(assertionObj.failureMessages) ? assertionObj.failureMessages : [];
|
|
48574
|
-
const firstFailure = failureMessages.find((entry) => typeof entry === "string" && entry.length > 0);
|
|
48575
|
-
const durationMs = typeof assertionObj.duration === "number" && Number.isFinite(assertionObj.duration) ? Math.max(assertionObj.duration, 0) : 0;
|
|
48576
|
-
parsed.push({
|
|
48577
|
-
testFile,
|
|
48578
|
-
testName,
|
|
48579
|
-
result,
|
|
48580
|
-
durationMs,
|
|
48581
|
-
errorMessage: firstLine(firstFailure),
|
|
48582
|
-
stackPrefix: firstLine(firstFailure)
|
|
48583
|
-
});
|
|
48584
|
-
}
|
|
48585
|
-
}
|
|
48586
|
-
return parsed;
|
|
48587
|
-
}
|
|
48588
|
-
function parseBunJsonLines(output) {
|
|
48589
|
-
const parsed = [];
|
|
48590
|
-
for (const line of output.split(`
|
|
48591
|
-
`)) {
|
|
48592
|
-
const trimmed = line.trim();
|
|
48593
|
-
if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
|
|
48594
|
-
continue;
|
|
48595
|
-
try {
|
|
48596
|
-
const obj = JSON.parse(trimmed);
|
|
48597
|
-
const rawFile = typeof obj.file === "string" ? obj.file : typeof obj.testFile === "string" ? obj.testFile : typeof obj.path === "string" ? obj.path : undefined;
|
|
48598
|
-
const rawName = typeof obj.testName === "string" ? obj.testName : typeof obj.fullName === "string" ? obj.fullName : typeof obj.name === "string" ? obj.name : undefined;
|
|
48599
|
-
const result = mapFrameworkStatusToResult(typeof obj.status === "string" ? obj.status : obj.result);
|
|
48600
|
-
if (!rawFile || !rawName || !result)
|
|
48601
|
-
continue;
|
|
48602
|
-
const errorObj = typeof obj.error === "object" && obj.error !== null ? obj.error : undefined;
|
|
48603
|
-
const durationMs = typeof obj.durationMs === "number" && Number.isFinite(obj.durationMs) ? Math.max(obj.durationMs, 0) : typeof obj.duration === "number" && Number.isFinite(obj.duration) ? Math.max(obj.duration, 0) : 0;
|
|
48604
|
-
parsed.push({
|
|
48605
|
-
testFile: rawFile.replace(/\\/g, "/"),
|
|
48606
|
-
testName: rawName,
|
|
48607
|
-
result,
|
|
48608
|
-
durationMs,
|
|
48609
|
-
errorMessage: firstLine(errorObj?.message ?? obj.errorMessage),
|
|
48610
|
-
stackPrefix: firstLine(errorObj?.stack)
|
|
48611
|
-
});
|
|
48612
|
-
} catch {}
|
|
48613
|
-
}
|
|
48614
|
-
return parsed;
|
|
48615
|
-
}
|
|
48616
|
-
function parseFrameworkJsonTestResults(framework, output) {
|
|
48617
|
-
const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
|
|
48618
|
-
if (jsonMatch) {
|
|
48619
|
-
try {
|
|
48620
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
48621
|
-
const testResults = parseJestLikeJsonTestResults(parsed);
|
|
48622
|
-
if (testResults.length > 0)
|
|
48623
|
-
return testResults;
|
|
48624
|
-
} catch {}
|
|
48625
|
-
}
|
|
48626
|
-
for (const line of output.split(`
|
|
48627
|
-
`)) {
|
|
48628
|
-
const trimmed = line.trim();
|
|
48629
|
-
if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
|
|
48630
|
-
continue;
|
|
48631
|
-
try {
|
|
48632
|
-
const parsed = JSON.parse(trimmed);
|
|
48633
|
-
const testResults = parseJestLikeJsonTestResults(parsed);
|
|
48634
|
-
if (testResults.length > 0)
|
|
48635
|
-
return testResults;
|
|
48636
|
-
} catch {}
|
|
48637
|
-
}
|
|
48638
|
-
if (framework === "bun") {
|
|
48639
|
-
return parseBunJsonLines(output);
|
|
48640
|
-
}
|
|
48641
|
-
return [];
|
|
48642
|
-
}
|
|
48643
48545
|
function parseTestOutput2(framework, output) {
|
|
48644
48546
|
const totals = {
|
|
48645
48547
|
passed: 0,
|
|
@@ -48927,16 +48829,7 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
|
|
|
48927
48829
|
};
|
|
48928
48830
|
}
|
|
48929
48831
|
const startTime = Date.now();
|
|
48930
|
-
const vitestJsonOutputPath = framework === "vitest" ? path39.join(cwd, ".swarm", "cache", "test-runner-vitest.json") : undefined;
|
|
48931
48832
|
try {
|
|
48932
|
-
if (vitestJsonOutputPath) {
|
|
48933
|
-
try {
|
|
48934
|
-
fs22.mkdirSync(path39.dirname(vitestJsonOutputPath), { recursive: true });
|
|
48935
|
-
if (fs22.existsSync(vitestJsonOutputPath)) {
|
|
48936
|
-
fs22.unlinkSync(vitestJsonOutputPath);
|
|
48937
|
-
}
|
|
48938
|
-
} catch {}
|
|
48939
|
-
}
|
|
48940
48833
|
const proc = bunSpawn(command, {
|
|
48941
48834
|
stdout: "pipe",
|
|
48942
48835
|
stderr: "pipe",
|
|
@@ -48957,37 +48850,13 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
|
|
|
48957
48850
|
output += (output ? `
|
|
48958
48851
|
` : "") + stderrResult.text;
|
|
48959
48852
|
}
|
|
48960
|
-
if (vitestJsonOutputPath) {
|
|
48961
|
-
try {
|
|
48962
|
-
if (fs22.existsSync(vitestJsonOutputPath)) {
|
|
48963
|
-
const vitestJsonOutput = fs22.readFileSync(vitestJsonOutputPath, "utf-8");
|
|
48964
|
-
if (vitestJsonOutput.trim().length > 0) {
|
|
48965
|
-
output += (output ? `
|
|
48966
|
-
` : "") + vitestJsonOutput;
|
|
48967
|
-
}
|
|
48968
|
-
}
|
|
48969
|
-
} catch {}
|
|
48970
|
-
}
|
|
48971
48853
|
if (stdoutResult.truncated || stderrResult.truncated) {
|
|
48972
48854
|
output += `
|
|
48973
48855
|
... (output truncated at stream read limit)`;
|
|
48974
48856
|
}
|
|
48975
48857
|
const useDispatchParse = process.env.SWARM_LANG_BACKEND !== "legacy";
|
|
48976
48858
|
const parsed = useDispatchParse ? await parseTestOutputViaDispatch(framework, output, cwd) ?? parseTestOutput2(framework, output) : parseTestOutput2(framework, output);
|
|
48977
|
-
const
|
|
48978
|
-
const totals = { ...parsed.totals };
|
|
48979
|
-
const { coveragePercent } = parsed;
|
|
48980
|
-
if (totals.total === 0 && parsedTestCases.length > 0) {
|
|
48981
|
-
for (const entry of parsedTestCases) {
|
|
48982
|
-
if (entry.result === "pass")
|
|
48983
|
-
totals.passed++;
|
|
48984
|
-
else if (entry.result === "fail")
|
|
48985
|
-
totals.failed++;
|
|
48986
|
-
else
|
|
48987
|
-
totals.skipped++;
|
|
48988
|
-
}
|
|
48989
|
-
totals.total = parsedTestCases.length;
|
|
48990
|
-
}
|
|
48859
|
+
const { totals, coveragePercent } = parsed;
|
|
48991
48860
|
const isTimeout = exitCode === -1;
|
|
48992
48861
|
const testPassed = exitCode === 0 && totals.failed === 0;
|
|
48993
48862
|
if (testPassed) {
|
|
@@ -49000,8 +48869,7 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
|
|
|
49000
48869
|
duration_ms,
|
|
49001
48870
|
totals,
|
|
49002
48871
|
rawOutput: output,
|
|
49003
|
-
outcome: "pass"
|
|
49004
|
-
testCases: parsedTestCases
|
|
48872
|
+
outcome: "pass"
|
|
49005
48873
|
};
|
|
49006
48874
|
if (coveragePercent !== undefined) {
|
|
49007
48875
|
result.coveragePercent = coveragePercent;
|
|
@@ -49023,8 +48891,7 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
|
|
|
49023
48891
|
rawOutput: output,
|
|
49024
48892
|
error: isTimeout ? `Tests timed out after ${timeout_ms}ms` : `Tests failed with ${totals.failed} failures`,
|
|
49025
48893
|
message: isTimeout ? `${framework} tests timed out after ${timeout_ms}ms` : `${framework} tests failed (${totals.failed}/${totals.total} failed)`,
|
|
49026
|
-
outcome: isTimeout ? "error" : "regression"
|
|
49027
|
-
testCases: parsedTestCases
|
|
48894
|
+
outcome: isTimeout ? "error" : "regression"
|
|
49028
48895
|
};
|
|
49029
48896
|
if (coveragePercent !== undefined) {
|
|
49030
48897
|
result.coveragePercent = coveragePercent;
|
|
@@ -49045,78 +48912,25 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
|
|
|
49045
48912
|
};
|
|
49046
48913
|
}
|
|
49047
48914
|
}
|
|
49048
|
-
function
|
|
49049
|
-
const normalized = testFile.replace(/\\/g, "/");
|
|
49050
|
-
if (!path39.isAbsolute(testFile))
|
|
49051
|
-
return normalized;
|
|
49052
|
-
const relative8 = path39.relative(workingDir, testFile);
|
|
49053
|
-
if (relative8.startsWith("..") || path39.isAbsolute(relative8)) {
|
|
49054
|
-
return normalized;
|
|
49055
|
-
}
|
|
49056
|
-
return relative8.replace(/\\/g, "/");
|
|
49057
|
-
}
|
|
49058
|
-
function combineAggregateResult(current, next) {
|
|
49059
|
-
if (current === "fail" || next === "fail")
|
|
49060
|
-
return "fail";
|
|
49061
|
-
if (current === "pass" || next === "pass")
|
|
49062
|
-
return "pass";
|
|
49063
|
-
return "skip";
|
|
49064
|
-
}
|
|
49065
|
-
function recordAndAnalyzeResults(result, testFiles, workingDir, sourceFiles, parsedTestCases) {
|
|
48915
|
+
function recordAndAnalyzeResults(result, testFiles, workingDir, sourceFiles) {
|
|
49066
48916
|
if (!result.totals || result.totals.total === 0)
|
|
49067
48917
|
return;
|
|
49068
48918
|
const now = new Date().toISOString();
|
|
49069
48919
|
const changedFiles = (sourceFiles && sourceFiles.length > 0 ? sourceFiles : testFiles).map((f) => f.replace(/\\/g, "/"));
|
|
49070
|
-
const
|
|
49071
|
-
const validParsedCases = parsedTestCases?.filter((parsedCase) => parsedCase.testFile.length > 0 && parsedCase.testName.length > 0) ?? [];
|
|
49072
|
-
for (const parsedCase of validParsedCases) {
|
|
49073
|
-
const normalizedTestFile = normalizeHistoryTestFile(parsedCase.testFile, workingDir);
|
|
49074
|
-
try {
|
|
49075
|
-
appendTestRun({
|
|
49076
|
-
timestamp: now,
|
|
49077
|
-
taskId: "auto",
|
|
49078
|
-
testFile: normalizedTestFile,
|
|
49079
|
-
testName: parsedCase.testName,
|
|
49080
|
-
result: parsedCase.result,
|
|
49081
|
-
durationMs: parsedCase.durationMs,
|
|
49082
|
-
errorMessage: parsedCase.errorMessage,
|
|
49083
|
-
stackPrefix: parsedCase.stackPrefix,
|
|
49084
|
-
changedFiles
|
|
49085
|
-
}, workingDir);
|
|
49086
|
-
} catch {}
|
|
49087
|
-
aggregateResultsByFile.set(normalizedTestFile, combineAggregateResult(aggregateResultsByFile.get(normalizedTestFile), parsedCase.result));
|
|
49088
|
-
}
|
|
49089
|
-
if (aggregateResultsByFile.size === 0) {
|
|
49090
|
-
const aggregateResult = result.success ? "pass" : "fail";
|
|
49091
|
-
for (const testFile of testFiles) {
|
|
49092
|
-
aggregateResultsByFile.set(testFile.replace(/\\/g, "/"), aggregateResult);
|
|
49093
|
-
}
|
|
49094
|
-
}
|
|
49095
|
-
for (const [testFile, aggregateResult] of aggregateResultsByFile) {
|
|
48920
|
+
for (const testFile of testFiles) {
|
|
49096
48921
|
try {
|
|
49097
48922
|
appendTestRun({
|
|
49098
48923
|
timestamp: now,
|
|
49099
48924
|
taskId: "auto",
|
|
49100
|
-
testFile,
|
|
49101
|
-
testName:
|
|
49102
|
-
result:
|
|
48925
|
+
testFile: testFile.replace(/\\/g, "/"),
|
|
48926
|
+
testName: "(aggregate)",
|
|
48927
|
+
result: result.success ? "pass" : "fail",
|
|
49103
48928
|
durationMs: result.duration_ms || 0,
|
|
49104
48929
|
changedFiles
|
|
49105
48930
|
}, workingDir);
|
|
49106
48931
|
} catch {}
|
|
49107
48932
|
}
|
|
49108
48933
|
}
|
|
49109
|
-
function selectHistoryForAnalysis(history) {
|
|
49110
|
-
const filesWithIndividualRecords = new Set;
|
|
49111
|
-
for (const record3 of history) {
|
|
49112
|
-
if (record3.testName !== AGGREGATE_TEST_NAME) {
|
|
49113
|
-
filesWithIndividualRecords.add(record3.testFile.toLowerCase());
|
|
49114
|
-
}
|
|
49115
|
-
}
|
|
49116
|
-
if (filesWithIndividualRecords.size === 0)
|
|
49117
|
-
return history;
|
|
49118
|
-
return history.filter((record3) => record3.testName !== AGGREGATE_TEST_NAME || !filesWithIndividualRecords.has(record3.testFile.toLowerCase()));
|
|
49119
|
-
}
|
|
49120
48934
|
function analyzeFailures(workingDir) {
|
|
49121
48935
|
const report = {
|
|
49122
48936
|
flakyTests: [],
|
|
@@ -49124,7 +48938,7 @@ function analyzeFailures(workingDir) {
|
|
|
49124
48938
|
quarantinedFailures: []
|
|
49125
48939
|
};
|
|
49126
48940
|
try {
|
|
49127
|
-
const history =
|
|
48941
|
+
const history = getAllHistory(workingDir);
|
|
49128
48942
|
if (history.length === 0)
|
|
49129
48943
|
return report;
|
|
49130
48944
|
report.flakyTests = detectFlakyTests(history);
|
|
@@ -49145,7 +48959,7 @@ function analyzeFailures(workingDir) {
|
|
|
49145
48959
|
} catch {}
|
|
49146
48960
|
return report;
|
|
49147
48961
|
}
|
|
49148
|
-
var MAX_OUTPUT_BYTES3 = 512000, MAX_COMMAND_LENGTH2 = 500, DEFAULT_TIMEOUT_MS = 60000, MAX_TIMEOUT_MS = 300000, MAX_SAFE_TEST_FILES = 50, MAX_SAFE_SOURCE_FILES = 1,
|
|
48962
|
+
var MAX_OUTPUT_BYTES3 = 512000, MAX_COMMAND_LENGTH2 = 500, DEFAULT_TIMEOUT_MS = 60000, MAX_TIMEOUT_MS = 300000, MAX_SAFE_TEST_FILES = 50, MAX_SAFE_SOURCE_FILES = 1, POWERSHELL_METACHARACTERS, DISPATCH_FRAMEWORK_MAP, COMPOUND_TEST_EXTENSIONS, TEST_DIRECTORY_NAMES, SOURCE_EXTENSIONS, SKIP_DIRECTORIES, test_runner;
|
|
49149
48963
|
var init_test_runner = __esm(() => {
|
|
49150
48964
|
init_zod();
|
|
49151
48965
|
init_discovery();
|
|
@@ -49591,7 +49405,7 @@ var init_test_runner = __esm(() => {
|
|
|
49591
49405
|
return JSON.stringify(errorResult, null, 2);
|
|
49592
49406
|
}
|
|
49593
49407
|
const result = await runTests(framework, effectiveScope, testFiles, coverage, timeout_ms, workingDir);
|
|
49594
|
-
recordAndAnalyzeResults(result, testFiles, workingDir, _files.length > 0 ? _files : undefined
|
|
49408
|
+
recordAndAnalyzeResults(result, testFiles, workingDir, _files.length > 0 ? _files : undefined);
|
|
49595
49409
|
let historyReport;
|
|
49596
49410
|
if (!result.success && result.totals && result.totals.failed > 0) {
|
|
49597
49411
|
historyReport = analyzeFailures(workingDir);
|