swarmdo 1.18.0 → 1.19.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/.claude/helpers/statusline.cjs +1 -1
- package/README.md +2 -1
- package/package.json +1 -1
- package/v3/@swarmdo/cli/dist/src/commands/index.js +6 -3
- package/v3/@swarmdo/cli/dist/src/commands/testreport.d.ts +17 -0
- package/v3/@swarmdo/cli/dist/src/commands/testreport.js +119 -0
- package/v3/@swarmdo/cli/dist/src/mcp-client.js +2 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.d.ts +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.js +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/profiles.js +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/testreport-tools.d.ts +11 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/testreport-tools.js +33 -0
- package/v3/@swarmdo/cli/dist/src/testreport/testreport.d.ts +51 -0
- package/v3/@swarmdo/cli/dist/src/testreport/testreport.js +182 -0
- package/v3/@swarmdo/cli/package.json +1 -1
|
@@ -515,7 +515,7 @@ function getCostFromStdin() {
|
|
|
515
515
|
// misses, the displayed version is meaningful (matches what the user
|
|
516
516
|
// installed), not a stale hard-coded string.
|
|
517
517
|
function getPkgVersion() {
|
|
518
|
-
let ver = '1.
|
|
518
|
+
let ver = '1.19.0';
|
|
519
519
|
try {
|
|
520
520
|
const home = os.homedir();
|
|
521
521
|
const pkgPaths = [
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://swarmdo.com)
|
|
4
4
|
|
|
5
|
-
[](https://img.shields.io/badge/npx%20swarmdo-v1.19.0-cb3837?style=for-the-badge&logo=npm&logoColor=white)](https://github.com/SwarmDo/swarmdo/releases)
|
|
6
6
|
[](https://github.com/SwarmDo/swarmdo/blob/main/LICENSE)
|
|
7
7
|
[](https://swarmdo.com)
|
|
8
8
|
[](https://github.com/SwarmDo/swarmdo)
|
|
@@ -287,6 +287,7 @@ The recent release train added a full day-to-day operations layer around the swa
|
|
|
287
287
|
| `swarmdo hotspots` | **Change-risk hotspots from git history** — rank files by churn × recency × author-spread to surface the technical debt worth refactoring or testing, answered from data instead of a guess. `--since 90d`, `--by risk\|churn\|commits\|authors`, `--top N`, `--format json`. Pairs with `codegraph`; also an MCP tool (`hotspots`). Deterministic |
|
|
288
288
|
| `swarmdo affected` | **Run only the tests your change touches** — from a git diff, walk `codegraph`'s import graph to list every file (and test file) a change could break (reverse-dependency closure, nx/turbo/`jest --findRelatedTests` style). `--base main`, `--tests` (pipeable list), `--format json`. Also an MCP tool (`affected`). Deterministic |
|
|
289
289
|
| `swarmdo cycles` | **Find circular import dependencies** — an SCC scan over `codegraph`'s import graph surfaces the mutually-importing file groups (and self-imports) that cause temporal-dead-zone and `undefined`-export bugs, `madge --circular` style. `--ci` exits 1 on any cycle to gate a build; `--format json`. Also an MCP tool (`cycles`). Deterministic |
|
|
290
|
+
| `swarmdo testreport` | **JUnit/TAP → failure digest** — turn raw test-result files into the exact failing test names + `file:line` + assertion messages, instead of scanning hundreds of log lines. The front-half of the test→fix loop: feed the failures straight into `repair`. Reads a file, a directory, or stdin; `--ci` exits 1 on any failure, `--format json`. Also an MCP tool (`testreport`). Deterministic |
|
|
290
291
|
| `swarmdo integrations` (alias `integrate`) | **Use swarmdo from Codex CLI, GitHub Copilot CLI, and pi** — one command wires AGENTS.md + each CLI's MCP config (idempotent, dry-run first, never touches your Claude Code setup) |
|
|
291
292
|
| OpenRouter model pool | **Let swarms pick from any models you configure** — declare tier-mapped OpenRouter models in `swarmdo.config.json`; the router Thompson-samples among them per task and the execution layer dispatches the winner |
|
|
292
293
|
| `swarmdo changelog` (alias `notes`) | **Release notes from conventional commits** — `--out NOTES.md` feeds `gh release create --notes-file` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swarmdo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "Swarmdo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -50,6 +50,9 @@ const commandLoaders = {
|
|
|
50
50
|
// Circular-import detector (madge --circular demand) — SCC scan over
|
|
51
51
|
// codegraph's import graph; catches TDZ/undefined-export bugs.
|
|
52
52
|
cycles: () => import('./cycles.js'),
|
|
53
|
+
// JUnit/TAP test-result parser (dorny/test-reporter demand) — raw results →
|
|
54
|
+
// failing test + file:line + message; the front-half of the test→fix loop.
|
|
55
|
+
testreport: () => import('./testreport.js'),
|
|
53
56
|
// Queryable exported-symbol index (codegraph demand) — where things are
|
|
54
57
|
// defined without grep+read round-trips.
|
|
55
58
|
codegraph: () => import('./codegraph.js'),
|
|
@@ -283,7 +286,7 @@ export async function getCommandsByCategory() {
|
|
|
283
286
|
// three slots from 'statusline' onward (completionsCmd received the
|
|
284
287
|
// statusline module, analyzeCmd received completions, …), scrambling the
|
|
285
288
|
// categorized help. Every load now has a named slot.
|
|
286
|
-
const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, swarmvectorCmd, hiveMindCmd, configCmd, statuslineCmd, compressCmd, efficiencyCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd, demoCmd, usageCmd, repairCmd, hudCmd, compactCmd, codegraphCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, affectedCmd, cyclesCmd,] = await Promise.all([
|
|
289
|
+
const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, swarmvectorCmd, hiveMindCmd, configCmd, statuslineCmd, compressCmd, efficiencyCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd, demoCmd, usageCmd, repairCmd, hudCmd, compactCmd, codegraphCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, affectedCmd, cyclesCmd, testreportCmd,] = await Promise.all([
|
|
287
290
|
loadCommand('daemon'), loadCommand('doctor'), loadCommand('embeddings'), loadCommand('neural'),
|
|
288
291
|
loadCommand('performance'), loadCommand('security'), loadCommand('swarmvector'), loadCommand('hive-mind'),
|
|
289
292
|
loadCommand('config'), loadCommand('statusline'), loadCommand('compress'), loadCommand('efficiency'),
|
|
@@ -291,7 +294,7 @@ export async function getCommandsByCategory() {
|
|
|
291
294
|
loadCommand('analyze'), loadCommand('route'), loadCommand('progress'), loadCommand('providers'),
|
|
292
295
|
loadCommand('plugins'), loadCommand('deployment'), loadCommand('claims'), loadCommand('issues'),
|
|
293
296
|
loadCommand('update'), loadCommand('process'), loadCommand('guidance'), loadCommand('appliance'),
|
|
294
|
-
loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('demo'), loadCommand('usage'), loadCommand('repair'), loadCommand('hud'), loadCommand('compact'), loadCommand('codegraph'), loadCommand('redact'), loadCommand('pack'), loadCommand('env'), loadCommand('license'), loadCommand('sbom'), loadCommand('apply'), loadCommand('hotspots'), loadCommand('affected'), loadCommand('cycles'),
|
|
297
|
+
loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('demo'), loadCommand('usage'), loadCommand('repair'), loadCommand('hud'), loadCommand('compact'), loadCommand('codegraph'), loadCommand('redact'), loadCommand('pack'), loadCommand('env'), loadCommand('license'), loadCommand('sbom'), loadCommand('apply'), loadCommand('hotspots'), loadCommand('affected'), loadCommand('cycles'), loadCommand('testreport'),
|
|
295
298
|
]);
|
|
296
299
|
return {
|
|
297
300
|
primary: [
|
|
@@ -307,7 +310,7 @@ export async function getCommandsByCategory() {
|
|
|
307
310
|
utility: [
|
|
308
311
|
configCmd, doctorCmd, daemonCmd, completionsCmd,
|
|
309
312
|
migrateCmd, workflowCmd, demoCmd,
|
|
310
|
-
statuslineCmd, compressCmd, compactCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, affectedCmd, cyclesCmd, efficiencyCmd,
|
|
313
|
+
statuslineCmd, compressCmd, compactCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, affectedCmd, cyclesCmd, testreportCmd, efficiencyCmd,
|
|
311
314
|
].filter(Boolean),
|
|
312
315
|
analysis: [
|
|
313
316
|
analyzeCmd, routeCmd, progressCmd, usageCmd, hudCmd, codegraphCmd,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo testreport` — parse JUnit-XML / TAP results into a failure digest.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo testreport junit.xml # human digest
|
|
5
|
+
* swarmdo testreport ./reports # scan a dir for *.xml/*.tap
|
|
6
|
+
* vitest --reporter=junit | swarmdo testreport # from stdin
|
|
7
|
+
* swarmdo testreport junit.xml --format json # machine output
|
|
8
|
+
* swarmdo testreport junit.xml --ci # exit 1 if any failed
|
|
9
|
+
*
|
|
10
|
+
* The front-half of the test→fix loop: turn raw results into exact failing
|
|
11
|
+
* test + file:line + message, then hand it to `repair`. Engine
|
|
12
|
+
* (../testreport/testreport.ts) is pure + tested; this reads files.
|
|
13
|
+
*/
|
|
14
|
+
import type { Command } from '../types.js';
|
|
15
|
+
export declare const testreportCommand: Command;
|
|
16
|
+
export default testreportCommand;
|
|
17
|
+
//# sourceMappingURL=testreport.d.ts.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo testreport` — parse JUnit-XML / TAP results into a failure digest.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo testreport junit.xml # human digest
|
|
5
|
+
* swarmdo testreport ./reports # scan a dir for *.xml/*.tap
|
|
6
|
+
* vitest --reporter=junit | swarmdo testreport # from stdin
|
|
7
|
+
* swarmdo testreport junit.xml --format json # machine output
|
|
8
|
+
* swarmdo testreport junit.xml --ci # exit 1 if any failed
|
|
9
|
+
*
|
|
10
|
+
* The front-half of the test→fix loop: turn raw results into exact failing
|
|
11
|
+
* test + file:line + message, then hand it to `repair`. Engine
|
|
12
|
+
* (../testreport/testreport.ts) is pure + tested; this reads files.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import { output } from '../output.js';
|
|
17
|
+
import { parseTestReport, detectFormat, mergeSummaries, formatSummary, } from '../testreport/testreport.js';
|
|
18
|
+
function readStdin() {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
process.stdin.on('data', (c) => chunks.push(Buffer.from(c)));
|
|
22
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
23
|
+
process.stdin.on('error', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Expand path args into a list of result files (dirs → their *.xml/*.tap). */
|
|
27
|
+
function collectFiles(root, args) {
|
|
28
|
+
const files = [];
|
|
29
|
+
for (const a of args) {
|
|
30
|
+
const abs = path.resolve(root, a);
|
|
31
|
+
let st;
|
|
32
|
+
try {
|
|
33
|
+
st = fs.statSync(abs);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (st.isDirectory()) {
|
|
39
|
+
for (const entry of fs.readdirSync(abs)) {
|
|
40
|
+
if (/\.(xml|tap)$/i.test(entry))
|
|
41
|
+
files.push(path.join(abs, entry));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
files.push(abs);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
function numFlag(v, def) {
|
|
51
|
+
const n = typeof v === 'number' ? v : typeof v === 'string' ? parseInt(v, 10) : NaN;
|
|
52
|
+
return Number.isFinite(n) ? n : def;
|
|
53
|
+
}
|
|
54
|
+
async function run(ctx) {
|
|
55
|
+
const root = ctx.cwd || process.cwd();
|
|
56
|
+
const asJson = ctx.flags.format === 'json';
|
|
57
|
+
const ci = ctx.flags.ci === true;
|
|
58
|
+
const top = numFlag(ctx.flags.top, 0);
|
|
59
|
+
// NB: --type, not --format (global --format is choices-validated to text|json|table).
|
|
60
|
+
const typeOverride = typeof ctx.flags.type === 'string' ? ctx.flags.type : undefined;
|
|
61
|
+
if (typeOverride && typeOverride !== 'junit' && typeOverride !== 'tap') {
|
|
62
|
+
output.printError(`unknown --type '${typeOverride}' (use junit|tap)`);
|
|
63
|
+
return { success: false, exitCode: 1 };
|
|
64
|
+
}
|
|
65
|
+
let summary;
|
|
66
|
+
if (ctx.args.length === 0) {
|
|
67
|
+
if (process.stdin.isTTY) {
|
|
68
|
+
output.writeln(output.error('Usage: swarmdo testreport <file|dir> OR <runner> | swarmdo testreport'));
|
|
69
|
+
return { success: false, exitCode: 1 };
|
|
70
|
+
}
|
|
71
|
+
const content = await readStdin();
|
|
72
|
+
const fmt = typeOverride ?? detectFormat(content);
|
|
73
|
+
summary = parseTestReport(content, fmt);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const files = collectFiles(root, ctx.args);
|
|
77
|
+
if (files.length === 0) {
|
|
78
|
+
output.printError('no test-result files found (looked for *.xml / *.tap)');
|
|
79
|
+
return { success: false, exitCode: 1 };
|
|
80
|
+
}
|
|
81
|
+
const summaries = [];
|
|
82
|
+
for (const f of files) {
|
|
83
|
+
let content;
|
|
84
|
+
try {
|
|
85
|
+
content = fs.readFileSync(f, 'utf8');
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const fmt = typeOverride ?? detectFormat(content, f);
|
|
91
|
+
summaries.push(parseTestReport(content, fmt));
|
|
92
|
+
}
|
|
93
|
+
summary = mergeSummaries(summaries);
|
|
94
|
+
}
|
|
95
|
+
if (asJson) {
|
|
96
|
+
process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
output.writeln(formatSummary(summary, { top: top > 0 ? top : undefined }));
|
|
100
|
+
}
|
|
101
|
+
const code = ci && summary.failed > 0 ? 1 : 0;
|
|
102
|
+
return { success: code === 0, exitCode: code };
|
|
103
|
+
}
|
|
104
|
+
export const testreportCommand = {
|
|
105
|
+
name: 'testreport',
|
|
106
|
+
description: 'Parse JUnit-XML / TAP test results into a compact failure digest (test name + file:line + message) — the front-half of the test→fix loop',
|
|
107
|
+
options: [
|
|
108
|
+
{ name: 'type', description: 'force input format: junit|tap (default: auto-detect)', type: 'string' },
|
|
109
|
+
{ name: 'top', description: 'show only the first N failures', type: 'string' },
|
|
110
|
+
{ name: 'ci', description: 'exit 1 if any test failed', type: 'boolean' },
|
|
111
|
+
],
|
|
112
|
+
examples: [
|
|
113
|
+
{ command: 'swarmdo testreport junit.xml', description: 'Digest a JUnit report' },
|
|
114
|
+
{ command: 'vitest --reporter=junit | swarmdo testreport --ci', description: 'Parse from stdin, fail on any failure' },
|
|
115
|
+
],
|
|
116
|
+
action: run,
|
|
117
|
+
};
|
|
118
|
+
export default testreportCommand;
|
|
119
|
+
//# sourceMappingURL=testreport.js.map
|
|
@@ -27,6 +27,7 @@ import { applyTools } from './mcp-tools/apply-tools.js';
|
|
|
27
27
|
import { hotspotsTools } from './mcp-tools/hotspots-tools.js';
|
|
28
28
|
import { affectedTools } from './mcp-tools/affected-tools.js';
|
|
29
29
|
import { cyclesTools } from './mcp-tools/cycles-tools.js';
|
|
30
|
+
import { testreportTools } from './mcp-tools/testreport-tools.js';
|
|
30
31
|
import { progressTools } from './mcp-tools/progress-tools.js';
|
|
31
32
|
import { embeddingsTools } from './mcp-tools/embeddings-tools.js';
|
|
32
33
|
import { claimsTools } from './mcp-tools/claims-tools.js';
|
|
@@ -118,6 +119,7 @@ const TOOL_GROUPS = {
|
|
|
118
119
|
hotspots: () => hotspotsTools,
|
|
119
120
|
affected: () => affectedTools,
|
|
120
121
|
cycles: () => cyclesTools,
|
|
122
|
+
testreport: () => testreportTools,
|
|
121
123
|
progress: () => progressTools,
|
|
122
124
|
embeddings: () => embeddingsTools,
|
|
123
125
|
claims: () => claimsTools,
|
|
@@ -23,6 +23,7 @@ export { applyTools } from './apply-tools.js';
|
|
|
23
23
|
export { hotspotsTools } from './hotspots-tools.js';
|
|
24
24
|
export { affectedTools } from './affected-tools.js';
|
|
25
25
|
export { cyclesTools } from './cycles-tools.js';
|
|
26
|
+
export { testreportTools } from './testreport-tools.js';
|
|
26
27
|
export { progressTools } from './progress-tools.js';
|
|
27
28
|
export { transferTools } from './transfer-tools.js';
|
|
28
29
|
export { securityTools } from './security-tools.js';
|
|
@@ -22,6 +22,7 @@ export { applyTools } from './apply-tools.js';
|
|
|
22
22
|
export { hotspotsTools } from './hotspots-tools.js';
|
|
23
23
|
export { affectedTools } from './affected-tools.js';
|
|
24
24
|
export { cyclesTools } from './cycles-tools.js';
|
|
25
|
+
export { testreportTools } from './testreport-tools.js';
|
|
25
26
|
export { progressTools } from './progress-tools.js';
|
|
26
27
|
export { transferTools } from './transfer-tools.js';
|
|
27
28
|
export { securityTools } from './security-tools.js';
|
|
@@ -29,6 +29,7 @@ const LEAN_GROUPS = [
|
|
|
29
29
|
'hotspots', // hotspots — git-history change-risk ranking ("where's the debt?")
|
|
30
30
|
'affected', // affected — impacted files/tests from a change via the import graph
|
|
31
31
|
'cycles', // cycles — circular-import detection via the import graph
|
|
32
|
+
'testreport', // testreport — JUnit/TAP → structured failure digest (pairs w/ repair)
|
|
32
33
|
];
|
|
33
34
|
/** Balanced = lean + orchestration/session/system the average user reaches for. */
|
|
34
35
|
const BALANCED_EXTRA = [
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testreport MCP Tool
|
|
3
|
+
*
|
|
4
|
+
* Let an agent turn raw JUnit/TAP test output into a structured failure digest —
|
|
5
|
+
* exact failing test + file:line + message — instead of re-reading hundreds of
|
|
6
|
+
* log lines. The front-half of the test→fix loop: pair with `repair`. Shares the
|
|
7
|
+
* pure parser in ../testreport/testreport.ts with the CLI; string-in/JSON-out.
|
|
8
|
+
*/
|
|
9
|
+
import type { MCPTool } from './types.js';
|
|
10
|
+
export declare const testreportTools: MCPTool[];
|
|
11
|
+
//# sourceMappingURL=testreport-tools.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testreport MCP Tool
|
|
3
|
+
*
|
|
4
|
+
* Let an agent turn raw JUnit/TAP test output into a structured failure digest —
|
|
5
|
+
* exact failing test + file:line + message — instead of re-reading hundreds of
|
|
6
|
+
* log lines. The front-half of the test→fix loop: pair with `repair`. Shares the
|
|
7
|
+
* pure parser in ../testreport/testreport.ts with the CLI; string-in/JSON-out.
|
|
8
|
+
*/
|
|
9
|
+
import { parseTestReport, detectFormat } from '../testreport/testreport.js';
|
|
10
|
+
const testreportTool = {
|
|
11
|
+
name: 'testreport',
|
|
12
|
+
description: 'Parse JUnit-XML or TAP test output into a structured summary: {passed, failed, skipped, total, durationMs, failures:[{suite,name,file,line,message}]}. Call this on a results file/string to get the exact failing tests + file:line instead of scanning logs; feed the failures into `repair`. Deterministic; auto-detects the format.',
|
|
13
|
+
category: 'testreport',
|
|
14
|
+
tags: ['tests', 'junit', 'tap', 'ci', 'failures'],
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
content: { type: 'string', description: 'the raw JUnit XML or TAP text' },
|
|
19
|
+
format: { type: 'string', enum: ['junit', 'tap'], description: 'force the input format (default: auto-detect)' },
|
|
20
|
+
},
|
|
21
|
+
required: ['content'],
|
|
22
|
+
},
|
|
23
|
+
handler: async (params) => {
|
|
24
|
+
if (typeof params.content !== 'string')
|
|
25
|
+
return { error: true, message: 'content (string) is required' };
|
|
26
|
+
const fmt = params.format === 'junit' || params.format === 'tap'
|
|
27
|
+
? params.format
|
|
28
|
+
: detectFormat(params.content);
|
|
29
|
+
return { format: fmt, ...parseTestReport(params.content, fmt) };
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export const testreportTools = [testreportTool];
|
|
33
|
+
//# sourceMappingURL=testreport-tools.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testreport.ts — parse JUnit-XML / TAP test results into a compact failure
|
|
3
|
+
* digest. The missing front-half of the test→fix loop: an agent runs the suite,
|
|
4
|
+
* points this at the results, and gets exactly the failing test names + file:line
|
|
5
|
+
* + assertion message — instead of re-reading hundreds of log lines — then feeds
|
|
6
|
+
* that into `repair`. Inspired by dorny/test-reporter, action-junit-report,
|
|
7
|
+
* gotestsum.
|
|
8
|
+
*
|
|
9
|
+
* Pure + deterministic: a results string in, a normalized summary out. The file
|
|
10
|
+
* read / glob lives in ../commands/testreport.ts, so this is fixture-testable.
|
|
11
|
+
* No XML dependency — well-formed JUnit escapes <>& in attribute values, so a
|
|
12
|
+
* focused tokenizer over <testcase>/<failure> is safe.
|
|
13
|
+
*/
|
|
14
|
+
export type TestStatus = 'passed' | 'failed' | 'skipped';
|
|
15
|
+
export type TestFormat = 'junit' | 'tap';
|
|
16
|
+
export interface TestFailure {
|
|
17
|
+
suite: string;
|
|
18
|
+
name: string;
|
|
19
|
+
file?: string;
|
|
20
|
+
line?: number;
|
|
21
|
+
message?: string;
|
|
22
|
+
type?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface TestSummary {
|
|
25
|
+
passed: number;
|
|
26
|
+
failed: number;
|
|
27
|
+
skipped: number;
|
|
28
|
+
total: number;
|
|
29
|
+
durationMs: number;
|
|
30
|
+
failures: TestFailure[];
|
|
31
|
+
}
|
|
32
|
+
/** Pull a `file:line` out of a stack-trace / assertion message. Pure. */
|
|
33
|
+
export declare function extractFileLine(text: string): {
|
|
34
|
+
file?: string;
|
|
35
|
+
line?: number;
|
|
36
|
+
};
|
|
37
|
+
/** Parse JUnit XML (testsuites/testsuite/testcase). Pure. */
|
|
38
|
+
export declare function parseJUnit(xml: string): TestSummary;
|
|
39
|
+
/** Parse TAP (Test Anything Protocol) output. Pure. */
|
|
40
|
+
export declare function parseTAP(text: string): TestSummary;
|
|
41
|
+
/** Sniff the format from a path extension, then content. Pure. */
|
|
42
|
+
export declare function detectFormat(content: string, path?: string): TestFormat;
|
|
43
|
+
/** Parse by explicit or detected format. Pure. */
|
|
44
|
+
export declare function parseTestReport(content: string, format: TestFormat): TestSummary;
|
|
45
|
+
/** Merge several summaries (multi-file globs). Pure. */
|
|
46
|
+
export declare function mergeSummaries(list: TestSummary[]): TestSummary;
|
|
47
|
+
/** Human-readable digest. Pure. */
|
|
48
|
+
export declare function formatSummary(s: TestSummary, opts?: {
|
|
49
|
+
top?: number;
|
|
50
|
+
}): string;
|
|
51
|
+
//# sourceMappingURL=testreport.d.ts.map
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testreport.ts — parse JUnit-XML / TAP test results into a compact failure
|
|
3
|
+
* digest. The missing front-half of the test→fix loop: an agent runs the suite,
|
|
4
|
+
* points this at the results, and gets exactly the failing test names + file:line
|
|
5
|
+
* + assertion message — instead of re-reading hundreds of log lines — then feeds
|
|
6
|
+
* that into `repair`. Inspired by dorny/test-reporter, action-junit-report,
|
|
7
|
+
* gotestsum.
|
|
8
|
+
*
|
|
9
|
+
* Pure + deterministic: a results string in, a normalized summary out. The file
|
|
10
|
+
* read / glob lives in ../commands/testreport.ts, so this is fixture-testable.
|
|
11
|
+
* No XML dependency — well-formed JUnit escapes <>& in attribute values, so a
|
|
12
|
+
* focused tokenizer over <testcase>/<failure> is safe.
|
|
13
|
+
*/
|
|
14
|
+
/** Decode the five predefined XML entities. Pure. */
|
|
15
|
+
function decodeXml(s) {
|
|
16
|
+
return s
|
|
17
|
+
.replace(/</g, '<')
|
|
18
|
+
.replace(/>/g, '>')
|
|
19
|
+
.replace(/"/g, '"')
|
|
20
|
+
.replace(/'/g, "'")
|
|
21
|
+
.replace(/&#(\d+);/g, (_, d) => String.fromCharCode(parseInt(d, 10)))
|
|
22
|
+
.replace(/&/g, '&'); // last, so &lt; → < not <
|
|
23
|
+
}
|
|
24
|
+
/** Extract key="value" / key='value' attributes from a tag's attribute text. Pure. */
|
|
25
|
+
function parseAttrs(attrText) {
|
|
26
|
+
const out = {};
|
|
27
|
+
const re = /([\w:.-]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
28
|
+
let m;
|
|
29
|
+
while ((m = re.exec(attrText)) !== null) {
|
|
30
|
+
out[m[1]] = decodeXml(m[3] ?? m[4] ?? '');
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/** Pull a `file:line` out of a stack-trace / assertion message. Pure. */
|
|
35
|
+
export function extractFileLine(text) {
|
|
36
|
+
if (!text)
|
|
37
|
+
return {};
|
|
38
|
+
// Prefer a parenthesised frame `(path:line:col)`, else a bare `path:line`.
|
|
39
|
+
const paren = text.match(/\(([^()\s]+?):(\d+):(?:\d+)\)/);
|
|
40
|
+
if (paren)
|
|
41
|
+
return { file: paren[1], line: parseInt(paren[2], 10) };
|
|
42
|
+
const bare = text.match(/([\w./\\-]+\.[a-zA-Z]{1,5}):(\d+)(?::\d+)?/);
|
|
43
|
+
if (bare)
|
|
44
|
+
return { file: bare[1], line: parseInt(bare[2], 10) };
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
/** Parse JUnit XML (testsuites/testsuite/testcase). Pure. */
|
|
48
|
+
export function parseJUnit(xml) {
|
|
49
|
+
const failures = [];
|
|
50
|
+
let passed = 0, failed = 0, skipped = 0, durationMs = 0;
|
|
51
|
+
const caseRe = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g;
|
|
52
|
+
let c;
|
|
53
|
+
while ((c = caseRe.exec(xml)) !== null) {
|
|
54
|
+
const attrs = parseAttrs(c[1]);
|
|
55
|
+
const body = c[2] ?? '';
|
|
56
|
+
const name = attrs.name ?? '(unnamed)';
|
|
57
|
+
const suite = attrs.classname ?? attrs.suite ?? '';
|
|
58
|
+
if (attrs.time)
|
|
59
|
+
durationMs += Math.round(parseFloat(attrs.time) * 1000) || 0;
|
|
60
|
+
const fail = body.match(/<(failure|error)\b([^>]*?)(?:\/>|>([\s\S]*?)<\/\1>)/);
|
|
61
|
+
const isSkipped = /<skipped\b[^>]*\/?>/.test(body);
|
|
62
|
+
if (fail) {
|
|
63
|
+
failed++;
|
|
64
|
+
const fAttrs = parseAttrs(fail[2]);
|
|
65
|
+
const inner = decodeXml((fail[3] ?? '').trim());
|
|
66
|
+
const message = fAttrs.message || inner.split('\n')[0] || undefined;
|
|
67
|
+
// Prefer explicit file/line attrs (pytest, some emitters), else sniff the trace.
|
|
68
|
+
const loc = attrs.file
|
|
69
|
+
? { file: attrs.file, line: attrs.line ? parseInt(attrs.line, 10) : undefined }
|
|
70
|
+
: extractFileLine(inner || fAttrs.message || '');
|
|
71
|
+
failures.push({ suite, name, message, type: fAttrs.type, ...loc });
|
|
72
|
+
}
|
|
73
|
+
else if (isSkipped) {
|
|
74
|
+
skipped++;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
passed++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { passed, failed, skipped, total: passed + failed + skipped, durationMs, failures };
|
|
81
|
+
}
|
|
82
|
+
/** Parse TAP (Test Anything Protocol) output. Pure. */
|
|
83
|
+
export function parseTAP(text) {
|
|
84
|
+
const failures = [];
|
|
85
|
+
let passed = 0, failed = 0, skipped = 0;
|
|
86
|
+
const lines = text.split('\n');
|
|
87
|
+
for (let i = 0; i < lines.length; i++) {
|
|
88
|
+
const line = lines[i];
|
|
89
|
+
const m = line.match(/^(ok|not ok)\b\s*(\d+)?\s*-?\s*(.*)$/);
|
|
90
|
+
if (!m)
|
|
91
|
+
continue;
|
|
92
|
+
const ok = m[1] === 'ok';
|
|
93
|
+
let desc = m[3].trim();
|
|
94
|
+
const directive = desc.match(/#\s*(SKIP|TODO)\b(.*)$/i);
|
|
95
|
+
if (directive)
|
|
96
|
+
desc = desc.slice(0, directive.index).trim();
|
|
97
|
+
if (directive && /skip/i.test(directive[1])) {
|
|
98
|
+
skipped++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (ok) {
|
|
102
|
+
passed++;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
failed++;
|
|
106
|
+
// Look ahead for an indented YAML diagnostic block for message/file/line.
|
|
107
|
+
let message;
|
|
108
|
+
let file;
|
|
109
|
+
let lineNo;
|
|
110
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
111
|
+
const d = lines[j];
|
|
112
|
+
if (!/^\s/.test(d) && d.trim() !== '')
|
|
113
|
+
break; // dedented → block ended
|
|
114
|
+
const mm = d.match(/^\s*message:\s*(.+)$/);
|
|
115
|
+
if (mm)
|
|
116
|
+
message = mm[1].replace(/^["']|["']$/g, '').trim();
|
|
117
|
+
const fm = d.match(/^\s*(?:file|at):\s*(.+)$/);
|
|
118
|
+
if (fm) {
|
|
119
|
+
const fl = extractFileLine(fm[1]);
|
|
120
|
+
if (fl.file) {
|
|
121
|
+
file = fl.file;
|
|
122
|
+
lineNo = fl.line;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const lm = d.match(/^\s*line:\s*(\d+)/);
|
|
126
|
+
if (lm)
|
|
127
|
+
lineNo = parseInt(lm[1], 10);
|
|
128
|
+
if (/^\s*\.\.\.\s*$/.test(d))
|
|
129
|
+
break; // end of YAML block
|
|
130
|
+
}
|
|
131
|
+
failures.push({ suite: '', name: desc || '(unnamed)', message, file, line: lineNo });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { passed, failed, skipped, total: passed + failed + skipped, durationMs: 0, failures };
|
|
135
|
+
}
|
|
136
|
+
/** Sniff the format from a path extension, then content. Pure. */
|
|
137
|
+
export function detectFormat(content, path) {
|
|
138
|
+
if (path) {
|
|
139
|
+
if (/\.tap$/i.test(path))
|
|
140
|
+
return 'tap';
|
|
141
|
+
if (/\.xml$/i.test(path))
|
|
142
|
+
return 'junit';
|
|
143
|
+
}
|
|
144
|
+
if (/<testsuite|<testcase/i.test(content))
|
|
145
|
+
return 'junit';
|
|
146
|
+
if (/^\s*(TAP version|1\.\.\d|ok\b|not ok\b)/m.test(content))
|
|
147
|
+
return 'tap';
|
|
148
|
+
return 'junit';
|
|
149
|
+
}
|
|
150
|
+
/** Parse by explicit or detected format. Pure. */
|
|
151
|
+
export function parseTestReport(content, format) {
|
|
152
|
+
return format === 'tap' ? parseTAP(content) : parseJUnit(content);
|
|
153
|
+
}
|
|
154
|
+
/** Merge several summaries (multi-file globs). Pure. */
|
|
155
|
+
export function mergeSummaries(list) {
|
|
156
|
+
return list.reduce((acc, s) => ({
|
|
157
|
+
passed: acc.passed + s.passed,
|
|
158
|
+
failed: acc.failed + s.failed,
|
|
159
|
+
skipped: acc.skipped + s.skipped,
|
|
160
|
+
total: acc.total + s.total,
|
|
161
|
+
durationMs: acc.durationMs + s.durationMs,
|
|
162
|
+
failures: acc.failures.concat(s.failures),
|
|
163
|
+
}), { passed: 0, failed: 0, skipped: 0, total: 0, durationMs: 0, failures: [] });
|
|
164
|
+
}
|
|
165
|
+
/** Human-readable digest. Pure. */
|
|
166
|
+
export function formatSummary(s, opts = {}) {
|
|
167
|
+
const head = `${s.passed} passed · ${s.failed} failed · ${s.skipped} skipped (${s.total} total, ${s.durationMs}ms)`;
|
|
168
|
+
if (s.failures.length === 0)
|
|
169
|
+
return head + (s.failed === 0 ? ' ✓' : '');
|
|
170
|
+
const shown = opts.top && opts.top > 0 ? s.failures.slice(0, opts.top) : s.failures;
|
|
171
|
+
const lines = [head, ''];
|
|
172
|
+
for (const f of shown) {
|
|
173
|
+
const where = f.file ? `${f.file}${f.line ? `:${f.line}` : ''}` : '';
|
|
174
|
+
lines.push(`✗ ${f.suite ? f.suite + ' › ' : ''}${f.name}${where ? ` (${where})` : ''}`);
|
|
175
|
+
if (f.message)
|
|
176
|
+
lines.push(` ${f.message.split('\n')[0]}`);
|
|
177
|
+
}
|
|
178
|
+
if (shown.length < s.failures.length)
|
|
179
|
+
lines.push(`… and ${s.failures.length - shown.length} more`);
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=testreport.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmdo/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Swarmdo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|