@wrongstack/plugins 0.281.3 → 0.282.1
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 +30 -4
- package/dist/accessibility-auditor.d.ts +40 -0
- package/dist/accessibility-auditor.js +411 -0
- package/dist/agent-handoff.d.ts +37 -0
- package/dist/agent-handoff.js +298 -0
- package/dist/api-compatibility-gate.d.ts +38 -0
- package/dist/api-compatibility-gate.js +357 -0
- package/dist/auto-escalate.d.ts +1 -1
- package/dist/auto-i18n-extractor.d.ts +36 -0
- package/dist/auto-i18n-extractor.js +335 -0
- package/dist/branch-guard.d.ts +6 -5
- package/dist/branch-guard.js +54 -4
- package/dist/checkpoint.js +18 -0
- package/dist/code-metrics.d.ts +31 -0
- package/dist/code-metrics.js +338 -0
- package/dist/commit-validator.js +67 -12
- package/dist/context-pins.js +4 -4
- package/dist/cost-tracker.d.ts +1 -1
- package/dist/cost-tracker.js +58 -19
- package/dist/cron.js +18 -8
- package/dist/dead-code-detector.d.ts +34 -0
- package/dist/dead-code-detector.js +354 -0
- package/dist/dep-guard.js +47 -6
- package/dist/dependency-vulnerability-gate.d.ts +35 -0
- package/dist/dependency-vulnerability-gate.js +308 -0
- package/dist/diff-summary.js +97 -10
- package/dist/doc-sync-guard.d.ts +33 -0
- package/dist/doc-sync-guard.js +223 -0
- package/dist/duplicate-code-detector.d.ts +33 -0
- package/dist/duplicate-code-detector.js +384 -0
- package/dist/feature-flag-tracker.d.ts +38 -0
- package/dist/feature-flag-tracker.js +316 -0
- package/dist/file-watcher.js +85 -36
- package/dist/format-on-save.js +99 -18
- package/dist/import-organizer.js +73 -14
- package/dist/index.d.ts +27 -0
- package/dist/index.js +11205 -2106
- package/dist/interface-contract-guard.d.ts +37 -0
- package/dist/interface-contract-guard.js +302 -0
- package/dist/knowledge-graph.d.ts +45 -0
- package/dist/knowledge-graph.js +325 -0
- package/dist/license-audit-gate.d.ts +34 -0
- package/dist/license-audit-gate.js +260 -0
- package/dist/llm-cache.js +5 -0
- package/dist/loop-breaker.d.ts +0 -38
- package/dist/loop-breaker.js +209 -8
- package/dist/migration-planner.d.ts +30 -0
- package/dist/migration-planner.js +349 -0
- package/dist/model-router.js +5 -0
- package/dist/performance-regression-gate.d.ts +33 -0
- package/dist/performance-regression-gate.js +315 -0
- package/dist/plugin-stack-observer.d.ts +35 -0
- package/dist/plugin-stack-observer.js +138 -0
- package/dist/pr-drafter.d.ts +35 -0
- package/dist/pr-drafter.js +334 -0
- package/dist/prompt-firewall.js +5 -0
- package/dist/refactor-suggester.d.ts +38 -0
- package/dist/refactor-suggester.js +382 -0
- package/dist/release-notes-generator.d.ts +27 -0
- package/dist/release-notes-generator.js +209 -0
- package/dist/schema-evolution-guard.d.ts +42 -0
- package/dist/schema-evolution-guard.js +319 -0
- package/dist/security-hotspot-scanner.d.ts +30 -0
- package/dist/security-hotspot-scanner.js +402 -0
- package/dist/semantic-search-indexer.d.ts +38 -0
- package/dist/semantic-search-indexer.js +436 -0
- package/dist/shell-check.js +38 -3
- package/dist/smart-rename.d.ts +26 -0
- package/dist/smart-rename.js +170 -0
- package/dist/spec-linker.js +273 -133
- package/dist/test-coverage-gate.d.ts +37 -0
- package/dist/test-coverage-gate.js +263 -0
- package/dist/test-flake-detector.d.ts +27 -0
- package/dist/test-flake-detector.js +274 -0
- package/dist/test-generator.d.ts +32 -0
- package/dist/test-generator.js +243 -0
- package/dist/test-runner-gate.js +154 -22
- package/dist/todo-listener.d.ts +2 -2
- package/dist/todo-listener.js +5 -5
- package/dist/token-throttle.js +5 -0
- package/dist/type-gate.d.ts +37 -0
- package/dist/type-gate.js +311 -0
- package/package.json +112 -4
- package/LICENSE +0 -21
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { isAbsolute, resolve, relative } from 'path';
|
|
3
|
+
|
|
4
|
+
// src/test-coverage-gate/index.ts
|
|
5
|
+
var API_VERSION = "^0.1.10";
|
|
6
|
+
var state = {
|
|
7
|
+
invocationCount: 0,
|
|
8
|
+
runCount: 0,
|
|
9
|
+
passCount: 0,
|
|
10
|
+
failCount: 0,
|
|
11
|
+
errorCount: 0,
|
|
12
|
+
skippedCount: 0,
|
|
13
|
+
lastOverallPct: null,
|
|
14
|
+
lastResult: null,
|
|
15
|
+
hookUnregister: null
|
|
16
|
+
};
|
|
17
|
+
var DEFAULTS = {
|
|
18
|
+
enabled: true,
|
|
19
|
+
coveragePath: "coverage/coverage-summary.json",
|
|
20
|
+
threshold: 80,
|
|
21
|
+
mode: "warn",
|
|
22
|
+
deltaThreshold: 1,
|
|
23
|
+
runOnChange: [".ts", ".tsx", ".js", ".jsx"]
|
|
24
|
+
};
|
|
25
|
+
function readConfig(raw) {
|
|
26
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
27
|
+
const r = raw;
|
|
28
|
+
return {
|
|
29
|
+
enabled: r["enabled"] !== false,
|
|
30
|
+
coveragePath: typeof r["coveragePath"] === "string" ? r["coveragePath"] : DEFAULTS.coveragePath,
|
|
31
|
+
threshold: typeof r["threshold"] === "number" && r["threshold"] >= 0 && r["threshold"] <= 100 ? r["threshold"] : DEFAULTS.threshold,
|
|
32
|
+
mode: r["mode"] === "block" ? "block" : DEFAULTS.mode,
|
|
33
|
+
deltaThreshold: typeof r["deltaThreshold"] === "number" && r["deltaThreshold"] >= 0 ? r["deltaThreshold"] : DEFAULTS.deltaThreshold,
|
|
34
|
+
runOnChange: Array.isArray(r["runOnChange"]) ? r["runOnChange"].filter((x) => typeof x === "string") : DEFAULTS.runOnChange
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function withinProject(p) {
|
|
38
|
+
if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
|
|
39
|
+
const root = process.cwd();
|
|
40
|
+
const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
|
|
41
|
+
const rel = relative(root, resolved);
|
|
42
|
+
if (rel === "" || rel === ".") return true;
|
|
43
|
+
if (rel.startsWith("..")) return false;
|
|
44
|
+
if (isAbsolute(rel)) return false;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function readCoverageSummary(coveragePath) {
|
|
48
|
+
try {
|
|
49
|
+
const raw = readFileSync(coveragePath, "utf-8");
|
|
50
|
+
return JSON.parse(raw);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function overallPercent(summary) {
|
|
56
|
+
const total = summary.total;
|
|
57
|
+
if (!total || typeof total !== "object") return null;
|
|
58
|
+
const t = total;
|
|
59
|
+
const values = ["lines", "statements", "functions", "branches"].map((k) => t[k]?.pct).filter((v) => typeof v === "number");
|
|
60
|
+
if (values.length === 0) return null;
|
|
61
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
62
|
+
}
|
|
63
|
+
var plugin = {
|
|
64
|
+
name: "test-coverage-gate",
|
|
65
|
+
version: "0.1.0",
|
|
66
|
+
description: "PostToolUse hook that detects test coverage regressions after source-file edits",
|
|
67
|
+
apiVersion: API_VERSION,
|
|
68
|
+
capabilities: { tools: true, hooks: true },
|
|
69
|
+
defaultConfig: { ...DEFAULTS },
|
|
70
|
+
configSchema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
enabled: {
|
|
74
|
+
type: "boolean",
|
|
75
|
+
default: true,
|
|
76
|
+
description: "Master switch."
|
|
77
|
+
},
|
|
78
|
+
coveragePath: {
|
|
79
|
+
type: "string",
|
|
80
|
+
default: "coverage/coverage-summary.json",
|
|
81
|
+
description: "Path to the JSON coverage summary file."
|
|
82
|
+
},
|
|
83
|
+
threshold: {
|
|
84
|
+
type: "number",
|
|
85
|
+
minimum: 0,
|
|
86
|
+
maximum: 100,
|
|
87
|
+
default: 80,
|
|
88
|
+
description: "Minimum allowed overall coverage percentage."
|
|
89
|
+
},
|
|
90
|
+
mode: {
|
|
91
|
+
type: "string",
|
|
92
|
+
enum: ["warn", "block"],
|
|
93
|
+
default: "warn",
|
|
94
|
+
description: "warn = inject context; block = refuse the mutating tool."
|
|
95
|
+
},
|
|
96
|
+
deltaThreshold: {
|
|
97
|
+
type: "number",
|
|
98
|
+
minimum: 0,
|
|
99
|
+
default: 1,
|
|
100
|
+
description: "Warn/block when coverage drops by at least this many percentage points."
|
|
101
|
+
},
|
|
102
|
+
runOnChange: {
|
|
103
|
+
type: "array",
|
|
104
|
+
items: { type: "string" },
|
|
105
|
+
default: [".ts", ".tsx", ".js", ".jsx"],
|
|
106
|
+
description: "Only check coverage when the edited file has one of these extensions."
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
setup(api) {
|
|
111
|
+
state.invocationCount = 0;
|
|
112
|
+
state.runCount = 0;
|
|
113
|
+
state.passCount = 0;
|
|
114
|
+
state.failCount = 0;
|
|
115
|
+
state.errorCount = 0;
|
|
116
|
+
state.skippedCount = 0;
|
|
117
|
+
state.lastOverallPct = null;
|
|
118
|
+
state.lastResult = null;
|
|
119
|
+
state.hookUnregister = null;
|
|
120
|
+
const cfg = readConfig(api.config.extensions?.["test-coverage-gate"]);
|
|
121
|
+
const hook = (input) => {
|
|
122
|
+
if (!cfg.enabled) return;
|
|
123
|
+
if (input.toolResult?.isError) return;
|
|
124
|
+
const inp = input.toolInput ?? {};
|
|
125
|
+
const sourcePath = inp["path"];
|
|
126
|
+
if (!sourcePath || typeof sourcePath !== "string") return;
|
|
127
|
+
if (!withinProject(sourcePath)) return;
|
|
128
|
+
const ext = sourcePath.includes(".") ? sourcePath.slice(sourcePath.lastIndexOf(".")).toLowerCase() : "";
|
|
129
|
+
if (!cfg.runOnChange.includes(ext)) {
|
|
130
|
+
state.skippedCount += 1;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
state.invocationCount += 1;
|
|
134
|
+
const summary = readCoverageSummary(cfg.coveragePath);
|
|
135
|
+
if (!summary) {
|
|
136
|
+
state.errorCount += 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const pct = overallPercent(summary);
|
|
140
|
+
if (pct === null) {
|
|
141
|
+
state.errorCount += 1;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
state.runCount += 1;
|
|
145
|
+
const previous = state.lastOverallPct;
|
|
146
|
+
state.lastOverallPct = pct;
|
|
147
|
+
const belowThreshold = pct < cfg.threshold;
|
|
148
|
+
const dropped = previous !== null && previous - pct >= cfg.deltaThreshold;
|
|
149
|
+
const passed = !belowThreshold && !dropped;
|
|
150
|
+
state.lastResult = {
|
|
151
|
+
overallPct: pct,
|
|
152
|
+
previousOverallPct: previous,
|
|
153
|
+
threshold: cfg.threshold,
|
|
154
|
+
passed,
|
|
155
|
+
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
156
|
+
};
|
|
157
|
+
if (passed) {
|
|
158
|
+
state.passCount += 1;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
state.failCount += 1;
|
|
162
|
+
const parts = ["\n\u274C test-coverage-gate:"];
|
|
163
|
+
if (belowThreshold) {
|
|
164
|
+
parts.push(
|
|
165
|
+
` Overall coverage ${pct.toFixed(2)}% is below the ${cfg.threshold}% threshold.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (dropped) {
|
|
169
|
+
parts.push(
|
|
170
|
+
` Coverage dropped from ${previous.toFixed(2)}% to ${pct.toFixed(2)}% (>= ${cfg.deltaThreshold}% delta).`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
parts.push(" Add or update tests to recover coverage before continuing.");
|
|
174
|
+
const message = parts.join("\n");
|
|
175
|
+
if (cfg.mode === "block") {
|
|
176
|
+
return {
|
|
177
|
+
decision: "block",
|
|
178
|
+
reason: message
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return { additionalContext: message };
|
|
182
|
+
};
|
|
183
|
+
state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
|
|
184
|
+
api.tools.register({
|
|
185
|
+
name: "coverage_gate_status",
|
|
186
|
+
description: "Reports test-coverage-gate state: threshold, mode, last coverage percentage, and counters.",
|
|
187
|
+
inputSchema: { type: "object", properties: {} },
|
|
188
|
+
permission: "auto",
|
|
189
|
+
category: "Diagnostics",
|
|
190
|
+
mutating: false,
|
|
191
|
+
async execute() {
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
enabled: cfg.enabled,
|
|
195
|
+
coveragePath: cfg.coveragePath,
|
|
196
|
+
threshold: cfg.threshold,
|
|
197
|
+
mode: cfg.mode,
|
|
198
|
+
deltaThreshold: cfg.deltaThreshold,
|
|
199
|
+
lastOverallPct: state.lastOverallPct,
|
|
200
|
+
counters: {
|
|
201
|
+
invocations: state.invocationCount,
|
|
202
|
+
runs: state.runCount,
|
|
203
|
+
passed: state.passCount,
|
|
204
|
+
failed: state.failCount,
|
|
205
|
+
errors: state.errorCount,
|
|
206
|
+
skipped: state.skippedCount
|
|
207
|
+
},
|
|
208
|
+
lastResult: state.lastResult
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
api.log.info("test-coverage-gate plugin loaded", {
|
|
213
|
+
version: "0.1.0",
|
|
214
|
+
coveragePath: cfg.coveragePath,
|
|
215
|
+
threshold: cfg.threshold,
|
|
216
|
+
mode: cfg.mode
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
teardown(api) {
|
|
220
|
+
if (state.hookUnregister) {
|
|
221
|
+
try {
|
|
222
|
+
state.hookUnregister();
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
state.hookUnregister = null;
|
|
226
|
+
}
|
|
227
|
+
const final = {
|
|
228
|
+
invocations: state.invocationCount,
|
|
229
|
+
runs: state.runCount,
|
|
230
|
+
passed: state.passCount,
|
|
231
|
+
failed: state.failCount,
|
|
232
|
+
errors: state.errorCount,
|
|
233
|
+
skipped: state.skippedCount
|
|
234
|
+
};
|
|
235
|
+
state.invocationCount = 0;
|
|
236
|
+
state.runCount = 0;
|
|
237
|
+
state.passCount = 0;
|
|
238
|
+
state.failCount = 0;
|
|
239
|
+
state.errorCount = 0;
|
|
240
|
+
state.skippedCount = 0;
|
|
241
|
+
state.lastOverallPct = null;
|
|
242
|
+
state.lastResult = null;
|
|
243
|
+
api.log.info("test-coverage-gate: teardown complete", { final });
|
|
244
|
+
},
|
|
245
|
+
async health() {
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
message: state.lastResult ? `test-coverage-gate: ${state.runCount} run(s), last ${state.lastResult.passed ? "PASSED" : "FAILED"} (${state.lastResult.overallPct.toFixed(2)}%)` : `test-coverage-gate: ${state.invocationCount} invocation(s), ${state.runCount} run(s)`,
|
|
249
|
+
counters: {
|
|
250
|
+
invocations: state.invocationCount,
|
|
251
|
+
runs: state.runCount,
|
|
252
|
+
passed: state.passCount,
|
|
253
|
+
failed: state.failCount,
|
|
254
|
+
errors: state.errorCount,
|
|
255
|
+
skipped: state.skippedCount
|
|
256
|
+
},
|
|
257
|
+
lastResult: state.lastResult
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
var test_coverage_gate_default = plugin;
|
|
262
|
+
|
|
263
|
+
export { test_coverage_gate_default as default };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* test-flake-detector plugin — runs a test command multiple times and reports
|
|
5
|
+
* tests that fail non-deterministically (flaky tests).
|
|
6
|
+
*
|
|
7
|
+
* Tools registered:
|
|
8
|
+
* - `flake_detect` : run a test command N times and identify flaky tests
|
|
9
|
+
* - `flake_status` : show config + per-session counters
|
|
10
|
+
*
|
|
11
|
+
* Config (`config.extensions['test-flake-detector']`):
|
|
12
|
+
*
|
|
13
|
+
* ```jsonc
|
|
14
|
+
* {
|
|
15
|
+
* "enabled": true,
|
|
16
|
+
* "defaultCommand": "npx vitest run", // used when flake_detect.command is omitted
|
|
17
|
+
* "maxRuns": 20, // hard upper bound for runs param
|
|
18
|
+
* "timeoutMs": 120000 // per-run timeout
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
declare const plugin: Plugin;
|
|
26
|
+
|
|
27
|
+
export { plugin as default };
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
// src/test-flake-detector/index.ts
|
|
4
|
+
var API_VERSION = "^0.1.10";
|
|
5
|
+
var state = {
|
|
6
|
+
invocationCount: 0,
|
|
7
|
+
runCount: 0,
|
|
8
|
+
errorCount: 0,
|
|
9
|
+
lastResult: null
|
|
10
|
+
};
|
|
11
|
+
var DEFAULTS = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
defaultCommand: "npx vitest run",
|
|
14
|
+
maxRuns: 20,
|
|
15
|
+
timeoutMs: 12e4
|
|
16
|
+
};
|
|
17
|
+
function readConfig(raw) {
|
|
18
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
19
|
+
const r = raw;
|
|
20
|
+
return {
|
|
21
|
+
enabled: r["enabled"] !== false,
|
|
22
|
+
defaultCommand: typeof r["defaultCommand"] === "string" ? r["defaultCommand"] : DEFAULTS.defaultCommand,
|
|
23
|
+
maxRuns: typeof r["maxRuns"] === "number" && r["maxRuns"] >= 1 && r["maxRuns"] <= 100 ? Math.floor(r["maxRuns"]) : DEFAULTS.maxRuns,
|
|
24
|
+
timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] > 0 ? r["timeoutMs"] : DEFAULTS.timeoutMs
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function parseTestOutput(output) {
|
|
28
|
+
const passed = [];
|
|
29
|
+
const failed = [];
|
|
30
|
+
const seen = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
32
|
+
const line = rawLine.trim();
|
|
33
|
+
if (!line) continue;
|
|
34
|
+
const passMatch = line.match(/^[✓✔]\s+(.+)/);
|
|
35
|
+
if (passMatch) {
|
|
36
|
+
const name = passMatch[1].trim();
|
|
37
|
+
if (!seen.has(`p:${name}`)) {
|
|
38
|
+
seen.add(`p:${name}`);
|
|
39
|
+
passed.push(name);
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const failMatch = line.match(/^[✕✗×]\s+(.+)/);
|
|
44
|
+
if (failMatch) {
|
|
45
|
+
const name = failMatch[1].trim();
|
|
46
|
+
if (!seen.has(`f:${name}`)) {
|
|
47
|
+
seen.add(`f:${name}`);
|
|
48
|
+
failed.push(name);
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const jestFailMatch = line.match(/^●\s+(.+)/);
|
|
53
|
+
if (jestFailMatch) {
|
|
54
|
+
const name = jestFailMatch[1].trim();
|
|
55
|
+
if (!seen.has(`f:${name}`)) {
|
|
56
|
+
seen.add(`f:${name}`);
|
|
57
|
+
failed.push(name);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { passed, failed };
|
|
62
|
+
}
|
|
63
|
+
function buildCommand(baseCommand, testPattern) {
|
|
64
|
+
const trimmedBase = baseCommand.trim();
|
|
65
|
+
if (!testPattern) return trimmedBase;
|
|
66
|
+
if (trimmedBase.includes(testPattern)) return trimmedBase;
|
|
67
|
+
return `${trimmedBase} ${testPattern}`;
|
|
68
|
+
}
|
|
69
|
+
function runOnce(command, timeoutMs) {
|
|
70
|
+
try {
|
|
71
|
+
const output = execSync(command, {
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
timeout: timeoutMs,
|
|
74
|
+
cwd: process.cwd(),
|
|
75
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
76
|
+
});
|
|
77
|
+
return { output };
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const e = err;
|
|
80
|
+
if (e.killed) {
|
|
81
|
+
return { output: e.stdout ?? "", error: `timeout after ${timeoutMs}ms` };
|
|
82
|
+
}
|
|
83
|
+
const output = `${e.stdout ?? ""}
|
|
84
|
+
${e.stderr ?? ""}`.trim();
|
|
85
|
+
return { output, error: e.message ?? "test command exited with non-zero code" };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
var plugin = {
|
|
89
|
+
name: "test-flake-detector",
|
|
90
|
+
version: "0.1.0",
|
|
91
|
+
description: "Runs a test command multiple times and reports tests that fail non-deterministically",
|
|
92
|
+
apiVersion: API_VERSION,
|
|
93
|
+
capabilities: { tools: true },
|
|
94
|
+
defaultConfig: { ...DEFAULTS },
|
|
95
|
+
configSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
enabled: {
|
|
99
|
+
type: "boolean",
|
|
100
|
+
default: true,
|
|
101
|
+
description: "Master switch."
|
|
102
|
+
},
|
|
103
|
+
defaultCommand: {
|
|
104
|
+
type: "string",
|
|
105
|
+
default: "npx vitest run",
|
|
106
|
+
description: "Command used when flake_detect.command is omitted."
|
|
107
|
+
},
|
|
108
|
+
maxRuns: {
|
|
109
|
+
type: "number",
|
|
110
|
+
minimum: 1,
|
|
111
|
+
maximum: 100,
|
|
112
|
+
default: 20,
|
|
113
|
+
description: "Hard upper bound for the runs parameter."
|
|
114
|
+
},
|
|
115
|
+
timeoutMs: {
|
|
116
|
+
type: "number",
|
|
117
|
+
minimum: 1e3,
|
|
118
|
+
default: 12e4,
|
|
119
|
+
description: "Per-run timeout in milliseconds."
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
setup(api) {
|
|
124
|
+
state.invocationCount = 0;
|
|
125
|
+
state.runCount = 0;
|
|
126
|
+
state.errorCount = 0;
|
|
127
|
+
state.lastResult = null;
|
|
128
|
+
const cfg = readConfig(api.config.extensions?.["test-flake-detector"]);
|
|
129
|
+
api.tools.register({
|
|
130
|
+
name: "flake_detect",
|
|
131
|
+
description: "Runs a test command multiple times and reports tests that fail non-deterministically. Provide testPattern and optionally runs (default 5) and command.",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {
|
|
135
|
+
testPattern: {
|
|
136
|
+
type: "string",
|
|
137
|
+
description: "Test file pattern or path to pass to the test command."
|
|
138
|
+
},
|
|
139
|
+
runs: {
|
|
140
|
+
type: "number",
|
|
141
|
+
minimum: 1,
|
|
142
|
+
maximum: 100,
|
|
143
|
+
default: 5,
|
|
144
|
+
description: "How many times to run the command."
|
|
145
|
+
},
|
|
146
|
+
command: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "Custom test command. Defaults to the configured defaultCommand."
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
permission: "confirm",
|
|
153
|
+
category: "Diagnostics",
|
|
154
|
+
mutating: false,
|
|
155
|
+
async execute(input) {
|
|
156
|
+
if (!cfg.enabled) {
|
|
157
|
+
return { ok: false, error: "test-flake-detector is disabled" };
|
|
158
|
+
}
|
|
159
|
+
const requestedRuns = typeof input.runs === "number" && input.runs >= 1 ? Math.min(Math.floor(input.runs), cfg.maxRuns) : 5;
|
|
160
|
+
const command = buildCommand(
|
|
161
|
+
typeof input.command === "string" && input.command.trim().length > 0 ? input.command : cfg.defaultCommand,
|
|
162
|
+
input.testPattern
|
|
163
|
+
);
|
|
164
|
+
state.invocationCount += 1;
|
|
165
|
+
const start = Date.now();
|
|
166
|
+
const records = /* @__PURE__ */ new Map();
|
|
167
|
+
let runsCompleted = 0;
|
|
168
|
+
const runErrors = [];
|
|
169
|
+
for (let run = 1; run <= requestedRuns; run += 1) {
|
|
170
|
+
const { output, error } = runOnce(command, cfg.timeoutMs);
|
|
171
|
+
state.runCount += 1;
|
|
172
|
+
if (error) {
|
|
173
|
+
state.errorCount += 1;
|
|
174
|
+
runErrors.push(`run ${run}: ${error}`);
|
|
175
|
+
}
|
|
176
|
+
const parsed = parseTestOutput(output);
|
|
177
|
+
runsCompleted += 1;
|
|
178
|
+
for (const name of parsed.passed) {
|
|
179
|
+
const rec = records.get(name) ?? { test: name, passCount: 0, failCount: 0 };
|
|
180
|
+
rec.passCount += 1;
|
|
181
|
+
records.set(name, rec);
|
|
182
|
+
}
|
|
183
|
+
for (const name of parsed.failed) {
|
|
184
|
+
const rec = records.get(name) ?? { test: name, passCount: 0, failCount: 0 };
|
|
185
|
+
rec.failCount += 1;
|
|
186
|
+
records.set(name, rec);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const all = Array.from(records.values());
|
|
190
|
+
const flakyTests = all.filter((r) => r.passCount > 0 && r.failCount > 0);
|
|
191
|
+
const alwaysFailing = all.filter((r) => r.passCount === 0 && r.failCount > 0);
|
|
192
|
+
const alwaysPassing = all.filter((r) => r.passCount > 0 && r.failCount === 0);
|
|
193
|
+
const durationMs = Date.now() - start;
|
|
194
|
+
state.lastResult = {
|
|
195
|
+
runsRequested: requestedRuns,
|
|
196
|
+
runsCompleted,
|
|
197
|
+
flakyTests,
|
|
198
|
+
alwaysFailing,
|
|
199
|
+
alwaysPassing,
|
|
200
|
+
durationMs,
|
|
201
|
+
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
ok: true,
|
|
205
|
+
command,
|
|
206
|
+
runsRequested: requestedRuns,
|
|
207
|
+
runsCompleted,
|
|
208
|
+
durationMs,
|
|
209
|
+
totalTestsObserved: all.length,
|
|
210
|
+
flakyCount: flakyTests.length,
|
|
211
|
+
flakyTests,
|
|
212
|
+
alwaysFailing,
|
|
213
|
+
alwaysPassing,
|
|
214
|
+
runErrors: runErrors.length > 0 ? runErrors : void 0
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
api.tools.register({
|
|
219
|
+
name: "flake_status",
|
|
220
|
+
description: "Reports test-flake-detector state: config and per-session counters.",
|
|
221
|
+
inputSchema: { type: "object", properties: {} },
|
|
222
|
+
permission: "auto",
|
|
223
|
+
category: "Diagnostics",
|
|
224
|
+
mutating: false,
|
|
225
|
+
async execute() {
|
|
226
|
+
return {
|
|
227
|
+
ok: true,
|
|
228
|
+
enabled: cfg.enabled,
|
|
229
|
+
defaultCommand: cfg.defaultCommand,
|
|
230
|
+
maxRuns: cfg.maxRuns,
|
|
231
|
+
timeoutMs: cfg.timeoutMs,
|
|
232
|
+
counters: {
|
|
233
|
+
invocations: state.invocationCount,
|
|
234
|
+
runs: state.runCount,
|
|
235
|
+
errors: state.errorCount
|
|
236
|
+
},
|
|
237
|
+
lastResult: state.lastResult
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
api.log.info("test-flake-detector plugin loaded", {
|
|
242
|
+
version: "0.1.0",
|
|
243
|
+
defaultCommand: cfg.defaultCommand,
|
|
244
|
+
maxRuns: cfg.maxRuns
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
teardown(api) {
|
|
248
|
+
const final = {
|
|
249
|
+
invocations: state.invocationCount,
|
|
250
|
+
runs: state.runCount,
|
|
251
|
+
errors: state.errorCount
|
|
252
|
+
};
|
|
253
|
+
state.invocationCount = 0;
|
|
254
|
+
state.runCount = 0;
|
|
255
|
+
state.errorCount = 0;
|
|
256
|
+
state.lastResult = null;
|
|
257
|
+
api.log.info("test-flake-detector: teardown complete", { final });
|
|
258
|
+
},
|
|
259
|
+
async health() {
|
|
260
|
+
return {
|
|
261
|
+
ok: true,
|
|
262
|
+
message: state.lastResult ? `test-flake-detector: ${state.runCount} run(s), last detection found ${state.lastResult.flakyTests.length} flaky test(s)` : `test-flake-detector: ${state.invocationCount} invocation(s), ${state.runCount} run(s)`,
|
|
263
|
+
counters: {
|
|
264
|
+
invocations: state.invocationCount,
|
|
265
|
+
runs: state.runCount,
|
|
266
|
+
errors: state.errorCount
|
|
267
|
+
},
|
|
268
|
+
lastResult: state.lastResult
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
var test_flake_detector_default = plugin;
|
|
273
|
+
|
|
274
|
+
export { test_flake_detector_default as default };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* test-generator plugin — generates a Vitest test skeleton from a source file
|
|
5
|
+
* using regex-based export detection.
|
|
6
|
+
*
|
|
7
|
+
* Tool registered:
|
|
8
|
+
* - generate_unit_tests : Read a source file and return a test file skeleton.
|
|
9
|
+
*
|
|
10
|
+
* No hooks are registered.
|
|
11
|
+
*
|
|
12
|
+
* Config (`config.extensions['test-generator']`):
|
|
13
|
+
*
|
|
14
|
+
* ```jsonc
|
|
15
|
+
* {
|
|
16
|
+
* "enabled": true,
|
|
17
|
+
* "framework": "vitest",
|
|
18
|
+
* "testSuffix": ".test",
|
|
19
|
+
* "includeImports": true
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface DetectedExport {
|
|
27
|
+
name: string;
|
|
28
|
+
kind: 'function' | 'arrow' | 'class' | 'named';
|
|
29
|
+
}
|
|
30
|
+
declare const plugin: Plugin;
|
|
31
|
+
|
|
32
|
+
export { type DetectedExport, plugin as default };
|