@wrongstack/plugins 0.281.1 → 0.282.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 +29 -3
- 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-i18n-extractor.d.ts +36 -0
- package/dist/auto-i18n-extractor.js +335 -0
- 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/cost-tracker.js +58 -16
- 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 +76 -10
- package/dist/import-organizer.js +73 -14
- package/dist/index.d.ts +27 -0
- package/dist/index.js +14067 -5054
- 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 +125 -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,243 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { resolve, isAbsolute, relative } from 'path';
|
|
3
|
+
|
|
4
|
+
// src/test-generator/index.ts
|
|
5
|
+
var API_VERSION = "^0.1.10";
|
|
6
|
+
var state = {
|
|
7
|
+
generateCount: 0,
|
|
8
|
+
exportCount: 0,
|
|
9
|
+
errorCount: 0
|
|
10
|
+
};
|
|
11
|
+
var DEFAULTS = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
framework: "vitest",
|
|
14
|
+
testSuffix: ".test",
|
|
15
|
+
includeImports: true
|
|
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
|
+
framework: typeof r["framework"] === "string" ? r["framework"] : DEFAULTS.framework,
|
|
23
|
+
testSuffix: typeof r["testSuffix"] === "string" ? r["testSuffix"] : DEFAULTS.testSuffix,
|
|
24
|
+
includeImports: r["includeImports"] !== false
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function withinProject(p) {
|
|
28
|
+
if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
|
|
29
|
+
const root = process.cwd();
|
|
30
|
+
const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
|
|
31
|
+
const rel = relative(root, resolved);
|
|
32
|
+
if (rel === "" || rel === ".") return true;
|
|
33
|
+
if (rel.startsWith("..")) return false;
|
|
34
|
+
if (isAbsolute(rel)) return false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
function toPosix(p) {
|
|
38
|
+
return p.replace(/\\/g, "/");
|
|
39
|
+
}
|
|
40
|
+
function relativePath(p) {
|
|
41
|
+
return toPosix(relative(process.cwd(), p));
|
|
42
|
+
}
|
|
43
|
+
function detectExports(content) {
|
|
44
|
+
const exports = [];
|
|
45
|
+
const seen = /* @__PURE__ */ new Set();
|
|
46
|
+
const functionRe = /export\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
47
|
+
const arrowRe = /export\s+(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/g;
|
|
48
|
+
const classRe = /export\s+(?:abstract\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
49
|
+
const namedRe = /export\s*\{([^}]+)\}/g;
|
|
50
|
+
let m;
|
|
51
|
+
functionRe.lastIndex = 0;
|
|
52
|
+
while ((m = functionRe.exec(content)) !== null) {
|
|
53
|
+
if (!seen.has(m[1])) {
|
|
54
|
+
seen.add(m[1]);
|
|
55
|
+
exports.push({ name: m[1], kind: "function" });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
arrowRe.lastIndex = 0;
|
|
59
|
+
while ((m = arrowRe.exec(content)) !== null) {
|
|
60
|
+
if (!seen.has(m[1])) {
|
|
61
|
+
seen.add(m[1]);
|
|
62
|
+
exports.push({ name: m[1], kind: "arrow" });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
classRe.lastIndex = 0;
|
|
66
|
+
while ((m = classRe.exec(content)) !== null) {
|
|
67
|
+
if (!seen.has(m[1])) {
|
|
68
|
+
seen.add(m[1]);
|
|
69
|
+
exports.push({ name: m[1], kind: "class" });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
namedRe.lastIndex = 0;
|
|
73
|
+
while ((m = namedRe.exec(content)) !== null) {
|
|
74
|
+
const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
75
|
+
for (const raw of names) {
|
|
76
|
+
const parts = raw.split(/\s+as\s+/);
|
|
77
|
+
const name = (parts.length > 1 ? parts[parts.length - 1] : raw).trim();
|
|
78
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) continue;
|
|
79
|
+
if (!seen.has(name)) {
|
|
80
|
+
seen.add(name);
|
|
81
|
+
exports.push({ name, kind: "named" });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return exports;
|
|
86
|
+
}
|
|
87
|
+
function generateTestContent(filePath, detected, cfg) {
|
|
88
|
+
const sourcePath = relativePath(filePath);
|
|
89
|
+
const importNames = detected.map((e) => e.name).join(", ");
|
|
90
|
+
const lines = [];
|
|
91
|
+
if (cfg.framework === "vitest") {
|
|
92
|
+
lines.push(`import { describe, it, expect } from 'vitest';`);
|
|
93
|
+
} else {
|
|
94
|
+
lines.push(`const { describe, it, expect } = require('${cfg.framework}');`);
|
|
95
|
+
}
|
|
96
|
+
if (cfg.includeImports && detected.length > 0) {
|
|
97
|
+
lines.push(`import { ${importNames} } from './${sourcePath.replace(/\.[^.]+$/, "")}';`);
|
|
98
|
+
}
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push(`describe('${sourcePath}', () => {`);
|
|
101
|
+
for (const exp of detected) {
|
|
102
|
+
lines.push(` it('${exp.name} behaves as expected', () => {`);
|
|
103
|
+
lines.push(` // TODO: replace with a real assertion for ${exp.name}`);
|
|
104
|
+
if (exp.kind === "class") {
|
|
105
|
+
lines.push(` const instance = new ${exp.name}();`);
|
|
106
|
+
lines.push(` expect(instance).toBeDefined();`);
|
|
107
|
+
} else {
|
|
108
|
+
lines.push(` expect(${exp.name}).toBeDefined();`);
|
|
109
|
+
}
|
|
110
|
+
lines.push(` });`);
|
|
111
|
+
lines.push("");
|
|
112
|
+
}
|
|
113
|
+
if (detected.length === 0) {
|
|
114
|
+
lines.push(` it('has no exported symbols to test', () => {`);
|
|
115
|
+
lines.push(` expect(true).toBe(true);`);
|
|
116
|
+
lines.push(` });`);
|
|
117
|
+
}
|
|
118
|
+
lines.push(`});`);
|
|
119
|
+
return lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
function generateForFile(filePath, cfg) {
|
|
122
|
+
const content = readFileSync(filePath, "utf-8");
|
|
123
|
+
const detected = detectExports(content);
|
|
124
|
+
const sourceName = relativePath(filePath).split("/").pop();
|
|
125
|
+
const baseName = sourceName.replace(/\.[^.]+$/, "");
|
|
126
|
+
const testFile = `${baseName}${cfg.testSuffix}.ts`;
|
|
127
|
+
return {
|
|
128
|
+
sourceFile: relativePath(filePath),
|
|
129
|
+
testFile,
|
|
130
|
+
exports: detected,
|
|
131
|
+
content: generateTestContent(filePath, detected, cfg)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
var plugin = {
|
|
135
|
+
name: "test-generator",
|
|
136
|
+
version: "0.1.0",
|
|
137
|
+
description: "Generates a test file skeleton from exported functions, classes, and arrow functions",
|
|
138
|
+
apiVersion: API_VERSION,
|
|
139
|
+
capabilities: { tools: true },
|
|
140
|
+
defaultConfig: { ...DEFAULTS },
|
|
141
|
+
configSchema: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
145
|
+
framework: {
|
|
146
|
+
type: "string",
|
|
147
|
+
default: "vitest",
|
|
148
|
+
description: "Test framework to target."
|
|
149
|
+
},
|
|
150
|
+
testSuffix: {
|
|
151
|
+
type: "string",
|
|
152
|
+
default: ".test",
|
|
153
|
+
description: "Suffix inserted before the extension of the generated test filename."
|
|
154
|
+
},
|
|
155
|
+
includeImports: {
|
|
156
|
+
type: "boolean",
|
|
157
|
+
default: true,
|
|
158
|
+
description: "Emit import statements for detected exports."
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
setup(api) {
|
|
163
|
+
state.generateCount = 0;
|
|
164
|
+
state.exportCount = 0;
|
|
165
|
+
state.errorCount = 0;
|
|
166
|
+
const cfg = readConfig(api.config.extensions?.["test-generator"]);
|
|
167
|
+
api.tools.register({
|
|
168
|
+
name: "generate_unit_tests",
|
|
169
|
+
description: "Generate a test skeleton for a source file. Detects exported functions, arrow functions, classes, and named exports. Returns the test content as a string; it does not write to disk.",
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
path: {
|
|
174
|
+
type: "string",
|
|
175
|
+
description: "Source file path (relative to project root)."
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
required: ["path"]
|
|
179
|
+
},
|
|
180
|
+
permission: "auto",
|
|
181
|
+
category: "Development",
|
|
182
|
+
mutating: false,
|
|
183
|
+
async execute(input) {
|
|
184
|
+
if (!cfg.enabled) return { ok: false, error: "test-generator is disabled" };
|
|
185
|
+
const rawPath = input.path;
|
|
186
|
+
if (!rawPath || typeof rawPath !== "string") {
|
|
187
|
+
return { ok: false, error: "path is required" };
|
|
188
|
+
}
|
|
189
|
+
if (!withinProject(rawPath)) {
|
|
190
|
+
return { ok: false, error: "path is outside the project root" };
|
|
191
|
+
}
|
|
192
|
+
const resolved = resolve(process.cwd(), rawPath);
|
|
193
|
+
state.generateCount += 1;
|
|
194
|
+
let result;
|
|
195
|
+
try {
|
|
196
|
+
result = generateForFile(resolved, cfg);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
state.errorCount += 1;
|
|
199
|
+
return { ok: false, error: String(err) };
|
|
200
|
+
}
|
|
201
|
+
state.exportCount += result.exports.length;
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
sourceFile: result.sourceFile,
|
|
205
|
+
testFile: result.testFile,
|
|
206
|
+
framework: cfg.framework,
|
|
207
|
+
exports: result.exports,
|
|
208
|
+
content: result.content
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
api.log.info("test-generator plugin loaded", {
|
|
213
|
+
version: "0.1.0",
|
|
214
|
+
framework: cfg.framework,
|
|
215
|
+
testSuffix: cfg.testSuffix
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
teardown(api) {
|
|
219
|
+
const final = {
|
|
220
|
+
generated: state.generateCount,
|
|
221
|
+
exports: state.exportCount,
|
|
222
|
+
errors: state.errorCount
|
|
223
|
+
};
|
|
224
|
+
state.generateCount = 0;
|
|
225
|
+
state.exportCount = 0;
|
|
226
|
+
state.errorCount = 0;
|
|
227
|
+
api.log.info("test-generator: teardown complete", { final });
|
|
228
|
+
},
|
|
229
|
+
async health() {
|
|
230
|
+
return {
|
|
231
|
+
ok: state.errorCount === 0,
|
|
232
|
+
message: state.errorCount ? `test-generator: ${state.errorCount} error(s)` : `test-generator: ${state.generateCount} generation(s), ${state.exportCount} export(s)`,
|
|
233
|
+
counters: {
|
|
234
|
+
generated: state.generateCount,
|
|
235
|
+
exports: state.exportCount,
|
|
236
|
+
errors: state.errorCount
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var test_generator_default = plugin;
|
|
242
|
+
|
|
243
|
+
export { test_generator_default as default };
|
package/dist/test-runner-gate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
1
|
+
import { execSync, execFileSync } from 'child_process';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
|
-
import { basename, dirname, join } from 'path';
|
|
3
|
+
import { isAbsolute, resolve, relative, basename, dirname, join } from 'path';
|
|
4
4
|
|
|
5
5
|
// src/test-runner-gate/index.ts
|
|
6
6
|
var API_VERSION = "^0.1.10";
|
|
@@ -16,36 +16,82 @@ var state = {
|
|
|
16
16
|
noTestCount: 0,
|
|
17
17
|
/** Times the test runner itself failed (timeout, crash). */
|
|
18
18
|
errorCount: 0,
|
|
19
|
+
/** Times the extension filter short-circuited the hook (.json, .md, etc.). */
|
|
20
|
+
extensionSkippedCount: 0,
|
|
21
|
+
/** Times a re-run was skipped because the source hash matched a previous PASS. */
|
|
22
|
+
cachedSkipCount: 0,
|
|
19
23
|
/** Hook handle for teardown. */
|
|
20
24
|
hookUnregister: null,
|
|
21
25
|
/** Last test result — surfaced by health() + status tool. */
|
|
22
26
|
lastResult: null
|
|
23
27
|
};
|
|
24
28
|
var DEFAULTS = {
|
|
25
|
-
enabled:
|
|
29
|
+
enabled: false,
|
|
26
30
|
runner: "auto",
|
|
27
31
|
command: "",
|
|
28
32
|
timeoutMs: 3e4,
|
|
29
|
-
testFilePatterns: [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
],
|
|
34
|
-
injectOnPass: false
|
|
33
|
+
testFilePatterns: ["src/{name}.test.ts", "tests/{name}.test.ts", "tests/{name}-exec.test.ts"],
|
|
34
|
+
injectOnPass: false,
|
|
35
|
+
enableContentHashCache: true,
|
|
36
|
+
enableExtensionFilter: true
|
|
35
37
|
};
|
|
36
38
|
function readConfig(raw) {
|
|
37
39
|
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
38
40
|
const r = raw;
|
|
39
41
|
const runner = r["runner"] === "vitest" || r["runner"] === "jest" || r["runner"] === "mocha" ? r["runner"] : "auto";
|
|
40
42
|
return {
|
|
41
|
-
enabled: r["enabled"]
|
|
43
|
+
enabled: r["enabled"] === true,
|
|
42
44
|
runner,
|
|
43
45
|
command: typeof r["command"] === "string" ? r["command"] : DEFAULTS.command,
|
|
44
46
|
timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] > 0 ? r["timeoutMs"] : DEFAULTS.timeoutMs,
|
|
45
47
|
testFilePatterns: Array.isArray(r["testFilePatterns"]) && r["testFilePatterns"].length > 0 ? r["testFilePatterns"].filter((x) => typeof x === "string") : DEFAULTS.testFilePatterns,
|
|
46
|
-
injectOnPass: r["injectOnPass"] === true
|
|
48
|
+
injectOnPass: r["injectOnPass"] === true,
|
|
49
|
+
enableContentHashCache: r["enableContentHashCache"] !== false,
|
|
50
|
+
enableExtensionFilter: r["enableExtensionFilter"] !== false
|
|
47
51
|
};
|
|
48
52
|
}
|
|
53
|
+
var TESTABLE_DEFAULT_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts", ".cts"]);
|
|
54
|
+
function getResolvableExtensions(patterns) {
|
|
55
|
+
const exts = /* @__PURE__ */ new Set();
|
|
56
|
+
for (const p of patterns) {
|
|
57
|
+
const m = /\.([a-z0-9]+)$/i.exec(p);
|
|
58
|
+
if (m?.[1]) exts.add(`.${m[1].toLowerCase()}`);
|
|
59
|
+
}
|
|
60
|
+
return exts;
|
|
61
|
+
}
|
|
62
|
+
function pathContentHash(content) {
|
|
63
|
+
const cap = Math.min(content.length, 65536);
|
|
64
|
+
let h = 5381;
|
|
65
|
+
for (let i = 0; i < cap; i++) {
|
|
66
|
+
h = (h << 5) + h + content.charCodeAt(i) | 0;
|
|
67
|
+
}
|
|
68
|
+
return h >>> 0;
|
|
69
|
+
}
|
|
70
|
+
var lastPassedHash = /* @__PURE__ */ new Map();
|
|
71
|
+
function withinProject(p) {
|
|
72
|
+
if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
|
|
73
|
+
const root = process.cwd();
|
|
74
|
+
const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
|
|
75
|
+
const rel = relative(root, resolved);
|
|
76
|
+
if (rel === "" || rel === ".") return true;
|
|
77
|
+
if (rel.startsWith("..")) return false;
|
|
78
|
+
if (isAbsolute(rel)) return false;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
var ALLOWED_COMMAND_TOKENS = /* @__PURE__ */ new Set([
|
|
82
|
+
"npx",
|
|
83
|
+
"pnpm",
|
|
84
|
+
"pnpm",
|
|
85
|
+
"npm",
|
|
86
|
+
"yarn",
|
|
87
|
+
"vitest",
|
|
88
|
+
"jest",
|
|
89
|
+
"mocha",
|
|
90
|
+
// Useful for `command: "node ./scripts/run-tests.js"` style configs.
|
|
91
|
+
"node"
|
|
92
|
+
// Direct binary paths under the project's node_modules — resolved
|
|
93
|
+
// by basename in resolveAllowedCommand().
|
|
94
|
+
]);
|
|
49
95
|
function resolveTestFiles(sourcePath, patterns) {
|
|
50
96
|
const name = basename(sourcePath).replace(/\.[^.]+$/, "");
|
|
51
97
|
const pathNoExt = sourcePath.replace(/\.[^.]+$/, "");
|
|
@@ -106,16 +152,49 @@ function detectRunner(requested) {
|
|
|
106
152
|
}
|
|
107
153
|
return null;
|
|
108
154
|
}
|
|
155
|
+
function resolveAllowedCommand(customCommand) {
|
|
156
|
+
const tokens = customCommand.split(/\s+/).filter(Boolean);
|
|
157
|
+
if (tokens.length === 0) return null;
|
|
158
|
+
const head = tokens[0];
|
|
159
|
+
if (ALLOWED_COMMAND_TOKENS.has(head)) {
|
|
160
|
+
return { cmd: head, args: tokens.slice(1) };
|
|
161
|
+
}
|
|
162
|
+
if (isAbsolute(head)) {
|
|
163
|
+
if (!withinProject(head)) return null;
|
|
164
|
+
const base = basename(head);
|
|
165
|
+
if (ALLOWED_COMMAND_TOKENS.has(base)) {
|
|
166
|
+
return { cmd: head, args: tokens.slice(1) };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
109
171
|
function runTests(testFile, runner, customCommand, timeoutMs) {
|
|
110
|
-
|
|
111
|
-
|
|
172
|
+
if (!withinProject(testFile)) return null;
|
|
173
|
+
let cmd;
|
|
174
|
+
let cmdArgs;
|
|
175
|
+
let trailingFlag;
|
|
176
|
+
if (customCommand) {
|
|
177
|
+
const resolved = resolveAllowedCommand(customCommand);
|
|
178
|
+
if (!resolved) return null;
|
|
179
|
+
cmd = resolved.cmd;
|
|
180
|
+
cmdArgs = [...resolved.args, testFile];
|
|
181
|
+
trailingFlag = runner.jsonFlags;
|
|
182
|
+
} else {
|
|
183
|
+
const tokens = runner.command.split(/\s+/).filter(Boolean);
|
|
184
|
+
cmd = tokens[0];
|
|
185
|
+
cmdArgs = [...tokens.slice(1), testFile];
|
|
186
|
+
trailingFlag = runner.jsonFlags;
|
|
187
|
+
}
|
|
188
|
+
const trailing = trailingFlag.split(/\s+/).filter(Boolean);
|
|
189
|
+
const fullArgs = [...cmdArgs, ...trailing];
|
|
112
190
|
let stdout = "";
|
|
113
191
|
try {
|
|
114
|
-
stdout =
|
|
192
|
+
stdout = execFileSync(cmd, fullArgs, {
|
|
115
193
|
encoding: "utf-8",
|
|
116
194
|
timeout: timeoutMs,
|
|
117
195
|
cwd: process.cwd(),
|
|
118
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
196
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
197
|
+
shell: false
|
|
119
198
|
});
|
|
120
199
|
} catch (err) {
|
|
121
200
|
const e = err;
|
|
@@ -176,7 +255,7 @@ var plugin = {
|
|
|
176
255
|
properties: {
|
|
177
256
|
enabled: {
|
|
178
257
|
type: "boolean",
|
|
179
|
-
default:
|
|
258
|
+
default: false,
|
|
180
259
|
description: "Master switch."
|
|
181
260
|
},
|
|
182
261
|
runner: {
|
|
@@ -206,6 +285,16 @@ var plugin = {
|
|
|
206
285
|
type: "boolean",
|
|
207
286
|
default: false,
|
|
208
287
|
description: "Inject additionalContext when tests pass too (default: only on failure)."
|
|
288
|
+
},
|
|
289
|
+
enableContentHashCache: {
|
|
290
|
+
type: "boolean",
|
|
291
|
+
default: true,
|
|
292
|
+
description: "Skip re-running tests when the source path is touched again with the same content hash as a previous PASS in this session."
|
|
293
|
+
},
|
|
294
|
+
enableExtensionFilter: {
|
|
295
|
+
type: "boolean",
|
|
296
|
+
default: true,
|
|
297
|
+
description: "Fast-path skip for non-TS/JS files (.json, .md, .lock, .txt, ...) before the test-file resolve walk."
|
|
209
298
|
}
|
|
210
299
|
}
|
|
211
300
|
},
|
|
@@ -216,14 +305,20 @@ var plugin = {
|
|
|
216
305
|
state.failCount = 0;
|
|
217
306
|
state.noTestCount = 0;
|
|
218
307
|
state.errorCount = 0;
|
|
308
|
+
state.extensionSkippedCount = 0;
|
|
309
|
+
state.cachedSkipCount = 0;
|
|
219
310
|
state.hookUnregister = null;
|
|
220
311
|
state.lastResult = null;
|
|
312
|
+
lastPassedHash.clear();
|
|
221
313
|
const cfg = readConfig(api.config.extensions?.["test-runner-gate"]);
|
|
222
314
|
const runner = detectRunner(cfg.runner);
|
|
223
315
|
if (!runner) {
|
|
224
|
-
api.log.warn(
|
|
225
|
-
|
|
226
|
-
|
|
316
|
+
api.log.warn(
|
|
317
|
+
"test-runner-gate: no test runner found (vitest, jest, mocha) \u2014 hook will be a no-op",
|
|
318
|
+
{
|
|
319
|
+
requested: cfg.runner
|
|
320
|
+
}
|
|
321
|
+
);
|
|
227
322
|
} else {
|
|
228
323
|
api.log.info("test-runner-gate: detected runner", { name: runner.name });
|
|
229
324
|
}
|
|
@@ -233,8 +328,30 @@ var plugin = {
|
|
|
233
328
|
const inp = input.toolInput ?? {};
|
|
234
329
|
const sourcePath = inp["path"];
|
|
235
330
|
if (!sourcePath || typeof sourcePath !== "string") return;
|
|
331
|
+
if (!withinProject(sourcePath)) return;
|
|
236
332
|
if (sourcePath.includes(".test.") || sourcePath.includes(".spec.")) return;
|
|
333
|
+
if (cfg.enableExtensionFilter) {
|
|
334
|
+
const ext = sourcePath.includes(".") ? sourcePath.slice(sourcePath.lastIndexOf(".")).toLowerCase() : "";
|
|
335
|
+
const resolvable = getResolvableExtensions(cfg.testFilePatterns);
|
|
336
|
+
if (ext && !resolvable.has(ext) && !TESTABLE_DEFAULT_EXTS.has(ext)) {
|
|
337
|
+
state.extensionSkippedCount += 1;
|
|
338
|
+
api.metrics.counter("extension_skipped");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
237
342
|
state.invocationCount += 1;
|
|
343
|
+
if (cfg.enableContentHashCache) {
|
|
344
|
+
const content = input.toolName === "write" ? inp["content"] ?? "" : input.toolName === "edit" ? inp["new_string"] ?? "" : "";
|
|
345
|
+
if (typeof content === "string" && content.length > 0) {
|
|
346
|
+
const hash = pathContentHash(content);
|
|
347
|
+
const last = lastPassedHash.get(sourcePath);
|
|
348
|
+
if (last !== void 0 && last === hash) {
|
|
349
|
+
state.cachedSkipCount += 1;
|
|
350
|
+
api.metrics.counter("cached_skip");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
238
355
|
const testFile = findTestFile(sourcePath, cfg.testFilePatterns);
|
|
239
356
|
if (!testFile) {
|
|
240
357
|
state.noTestCount += 1;
|
|
@@ -256,6 +373,12 @@ var plugin = {
|
|
|
256
373
|
};
|
|
257
374
|
if (result.passed) {
|
|
258
375
|
state.passCount += 1;
|
|
376
|
+
if (cfg.enableContentHashCache) {
|
|
377
|
+
const content = input.toolName === "write" ? inp["content"] ?? "" : input.toolName === "edit" ? inp["new_string"] ?? "" : "";
|
|
378
|
+
if (typeof content === "string" && content.length > 0) {
|
|
379
|
+
lastPassedHash.set(sourcePath, pathContentHash(content));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
259
382
|
if (!cfg.injectOnPass) return;
|
|
260
383
|
return {
|
|
261
384
|
additionalContext: `
|
|
@@ -298,7 +421,9 @@ Fix the failing tests or revert the change if it broke something.`
|
|
|
298
421
|
passed: state.passCount,
|
|
299
422
|
failed: state.failCount,
|
|
300
423
|
noTest: state.noTestCount,
|
|
301
|
-
errors: state.errorCount
|
|
424
|
+
errors: state.errorCount,
|
|
425
|
+
extensionSkipped: state.extensionSkippedCount,
|
|
426
|
+
cachedSkips: state.cachedSkipCount
|
|
302
427
|
},
|
|
303
428
|
lastResult: state.lastResult
|
|
304
429
|
};
|
|
@@ -324,7 +449,9 @@ Fix the failing tests or revert the change if it broke something.`
|
|
|
324
449
|
passed: state.passCount,
|
|
325
450
|
failed: state.failCount,
|
|
326
451
|
noTest: state.noTestCount,
|
|
327
|
-
errors: state.errorCount
|
|
452
|
+
errors: state.errorCount,
|
|
453
|
+
extensionSkipped: state.extensionSkippedCount,
|
|
454
|
+
cachedSkips: state.cachedSkipCount
|
|
328
455
|
};
|
|
329
456
|
state.invocationCount = 0;
|
|
330
457
|
state.runCount = 0;
|
|
@@ -332,7 +459,10 @@ Fix the failing tests or revert the change if it broke something.`
|
|
|
332
459
|
state.failCount = 0;
|
|
333
460
|
state.noTestCount = 0;
|
|
334
461
|
state.errorCount = 0;
|
|
462
|
+
state.extensionSkippedCount = 0;
|
|
463
|
+
state.cachedSkipCount = 0;
|
|
335
464
|
state.lastResult = null;
|
|
465
|
+
lastPassedHash.clear();
|
|
336
466
|
api.log.info("test-runner-gate: teardown complete", { final });
|
|
337
467
|
},
|
|
338
468
|
async health() {
|
|
@@ -345,7 +475,9 @@ Fix the failing tests or revert the change if it broke something.`
|
|
|
345
475
|
passed: state.passCount,
|
|
346
476
|
failed: state.failCount,
|
|
347
477
|
noTest: state.noTestCount,
|
|
348
|
-
errors: state.errorCount
|
|
478
|
+
errors: state.errorCount,
|
|
479
|
+
extensionSkipped: state.extensionSkippedCount,
|
|
480
|
+
cachedSkips: state.cachedSkipCount
|
|
349
481
|
},
|
|
350
482
|
lastResult: state.lastResult
|
|
351
483
|
};
|
package/dist/todo-listener.d.ts
CHANGED
package/dist/todo-listener.js
CHANGED
|
@@ -10,16 +10,16 @@ var state = {
|
|
|
10
10
|
hookUnregister: null
|
|
11
11
|
};
|
|
12
12
|
var DEFAULTS = {
|
|
13
|
-
enabled:
|
|
13
|
+
enabled: false,
|
|
14
14
|
subjectPrefix: "todo: ",
|
|
15
15
|
broadcastOnChange: true,
|
|
16
|
-
cooldownMs:
|
|
16
|
+
cooldownMs: 3e4
|
|
17
17
|
};
|
|
18
18
|
function readConfig(raw) {
|
|
19
19
|
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
20
20
|
const r = raw;
|
|
21
21
|
return {
|
|
22
|
-
enabled: r["enabled"]
|
|
22
|
+
enabled: r["enabled"] === true,
|
|
23
23
|
subjectPrefix: typeof r["subjectPrefix"] === "string" ? r["subjectPrefix"] : DEFAULTS.subjectPrefix,
|
|
24
24
|
broadcastOnChange: r["broadcastOnChange"] !== false,
|
|
25
25
|
cooldownMs: typeof r["cooldownMs"] === "number" && r["cooldownMs"] >= 0 ? r["cooldownMs"] : DEFAULTS.cooldownMs
|
|
@@ -44,7 +44,7 @@ var plugin = {
|
|
|
44
44
|
configSchema: {
|
|
45
45
|
type: "object",
|
|
46
46
|
properties: {
|
|
47
|
-
enabled: { type: "boolean", default:
|
|
47
|
+
enabled: { type: "boolean", default: false, description: "Master switch." },
|
|
48
48
|
subjectPrefix: {
|
|
49
49
|
type: "string",
|
|
50
50
|
default: DEFAULTS.subjectPrefix,
|
|
@@ -58,7 +58,7 @@ var plugin = {
|
|
|
58
58
|
cooldownMs: {
|
|
59
59
|
type: "number",
|
|
60
60
|
minimum: 0,
|
|
61
|
-
default:
|
|
61
|
+
default: DEFAULTS.cooldownMs,
|
|
62
62
|
description: "Minimum interval between consecutive broadcasts (ms)."
|
|
63
63
|
}
|
|
64
64
|
}
|
package/dist/token-throttle.js
CHANGED
|
@@ -110,6 +110,11 @@ var plugin = {
|
|
|
110
110
|
}
|
|
111
111
|
const cfg = readConfig(api.config.extensions?.["token-throttle"]);
|
|
112
112
|
if (cfg.enabled) {
|
|
113
|
+
api.emitCustom?.("provider.wrap:loaded", {
|
|
114
|
+
plugin: "token-throttle",
|
|
115
|
+
kind: "throttle",
|
|
116
|
+
wraps: ["request"]
|
|
117
|
+
});
|
|
113
118
|
state.extensionUnregister = api.extensions.register({
|
|
114
119
|
name: "token-throttle",
|
|
115
120
|
owner: "token-throttle",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* type-gate plugin — PostToolUse hook that runs TypeScript type-checking
|
|
5
|
+
* after every `write` or `edit` to a source file.
|
|
6
|
+
*
|
|
7
|
+
* `lint-gate` validates style; this plugin validates types. It runs
|
|
8
|
+
* `tsc --noEmit` (or a user-supplied command) and injects the first few
|
|
9
|
+
* errors as `additionalContext` so the model can fix type regressions in
|
|
10
|
+
* the same turn.
|
|
11
|
+
*
|
|
12
|
+
* Tools registered:
|
|
13
|
+
* - type_gate_status : Show config + per-session counters.
|
|
14
|
+
*
|
|
15
|
+
* Hooks registered:
|
|
16
|
+
* - PostToolUse with matcher `write|edit`.
|
|
17
|
+
*
|
|
18
|
+
* Config (`config.extensions['type-gate']`):
|
|
19
|
+
*
|
|
20
|
+
* ```jsonc
|
|
21
|
+
* {
|
|
22
|
+
* "enabled": true,
|
|
23
|
+
* "command": "npx tsc --noEmit", // base type-check command
|
|
24
|
+
* "tsConfigPath": "tsconfig.json",
|
|
25
|
+
* "timeoutMs": 60000,
|
|
26
|
+
* "failSeverity": "warn", // "warn" | "block"
|
|
27
|
+
* "maxErrors": 5, // errors shown in context
|
|
28
|
+
* "runOnChange": [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"]
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
declare const plugin: Plugin;
|
|
36
|
+
|
|
37
|
+
export { plugin as default };
|