phantom-pr 0.1.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/LICENSE.md +0 -0
- package/README.md +143 -0
- package/dist/adapters/git.d.ts +28 -0
- package/dist/adapters/git.js +112 -0
- package/dist/adapters/git.js.map +1 -0
- package/dist/adapters/github.d.ts +71 -0
- package/dist/adapters/github.js +194 -0
- package/dist/adapters/github.js.map +1 -0
- package/dist/cli.d.ts +47 -0
- package/dist/cli.js +201 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/context.js +275 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/full.d.ts +13 -0
- package/dist/commands/full.js +590 -0
- package/dist/commands/full.js.map +1 -0
- package/dist/commands/gen_test.d.ts +2 -0
- package/dist/commands/gen_test.js +94 -0
- package/dist/commands/gen_test.js.map +1 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +62 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/plan.d.ts +2 -0
- package/dist/commands/plan.js +107 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/pr.d.ts +9 -0
- package/dist/commands/pr.js +400 -0
- package/dist/commands/pr.js.map +1 -0
- package/dist/commands/pr_list.d.ts +2 -0
- package/dist/commands/pr_list.js +158 -0
- package/dist/commands/pr_list.js.map +1 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +132 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/test.d.ts +2 -0
- package/dist/commands/test.js +143 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/testability.d.ts +10 -0
- package/dist/commands/testability.js +406 -0
- package/dist/commands/testability.js.map +1 -0
- package/dist/commands/tests.d.ts +9 -0
- package/dist/commands/tests.js +801 -0
- package/dist/commands/tests.js.map +1 -0
- package/dist/core/code/exports.d.ts +5 -0
- package/dist/core/code/exports.js +68 -0
- package/dist/core/code/exports.js.map +1 -0
- package/dist/core/config/forkPolicy.d.ts +25 -0
- package/dist/core/config/forkPolicy.js +35 -0
- package/dist/core/config/forkPolicy.js.map +1 -0
- package/dist/core/config/load.d.ts +13 -0
- package/dist/core/config/load.js +35 -0
- package/dist/core/config/load.js.map +1 -0
- package/dist/core/config/types.d.ts +87 -0
- package/dist/core/config/types.js +2 -0
- package/dist/core/config/types.js.map +1 -0
- package/dist/core/config/validate.d.ts +4 -0
- package/dist/core/config/validate.js +246 -0
- package/dist/core/config/validate.js.map +1 -0
- package/dist/core/context/packer.d.ts +31 -0
- package/dist/core/context/packer.js +345 -0
- package/dist/core/context/packer.js.map +1 -0
- package/dist/core/context/types.d.ts +41 -0
- package/dist/core/context/types.js +2 -0
- package/dist/core/context/types.js.map +1 -0
- package/dist/core/converge/loop.d.ts +13 -0
- package/dist/core/converge/loop.js +35 -0
- package/dist/core/converge/loop.js.map +1 -0
- package/dist/core/converge/types.d.ts +15 -0
- package/dist/core/converge/types.js +2 -0
- package/dist/core/converge/types.js.map +1 -0
- package/dist/core/generator/llm/diffApply.d.ts +26 -0
- package/dist/core/generator/llm/diffApply.js +276 -0
- package/dist/core/generator/llm/diffApply.js.map +1 -0
- package/dist/core/generator/llm/qualityGate.d.ts +34 -0
- package/dist/core/generator/llm/qualityGate.js +324 -0
- package/dist/core/generator/llm/qualityGate.js.map +1 -0
- package/dist/core/generator/llmGenerator.d.ts +34 -0
- package/dist/core/generator/llmGenerator.js +245 -0
- package/dist/core/generator/llmGenerator.js.map +1 -0
- package/dist/core/generator/quality.d.ts +17 -0
- package/dist/core/generator/quality.js +31 -0
- package/dist/core/generator/quality.js.map +1 -0
- package/dist/core/generator/registry.d.ts +26 -0
- package/dist/core/generator/registry.js +29 -0
- package/dist/core/generator/registry.js.map +1 -0
- package/dist/core/generator/smokeGenerator.d.ts +11 -0
- package/dist/core/generator/smokeGenerator.js +27 -0
- package/dist/core/generator/smokeGenerator.js.map +1 -0
- package/dist/core/generator/types.d.ts +48 -0
- package/dist/core/generator/types.js +2 -0
- package/dist/core/generator/types.js.map +1 -0
- package/dist/core/index/indexer.d.ts +29 -0
- package/dist/core/index/indexer.js +167 -0
- package/dist/core/index/indexer.js.map +1 -0
- package/dist/core/jest/parser.d.ts +17 -0
- package/dist/core/jest/parser.js +90 -0
- package/dist/core/jest/parser.js.map +1 -0
- package/dist/core/llm/provider.d.ts +55 -0
- package/dist/core/llm/provider.js +105 -0
- package/dist/core/llm/provider.js.map +1 -0
- package/dist/core/logger.d.ts +19 -0
- package/dist/core/logger.js +44 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/plan/planner.d.ts +16 -0
- package/dist/core/plan/planner.js +91 -0
- package/dist/core/plan/planner.js.map +1 -0
- package/dist/core/process/exec.d.ts +22 -0
- package/dist/core/process/exec.js +83 -0
- package/dist/core/process/exec.js.map +1 -0
- package/dist/core/redact.d.ts +6 -0
- package/dist/core/redact.js +49 -0
- package/dist/core/redact.js.map +1 -0
- package/dist/core/repo/boundary.d.ts +10 -0
- package/dist/core/repo/boundary.js +58 -0
- package/dist/core/repo/boundary.js.map +1 -0
- package/dist/core/stableJson.d.ts +1 -0
- package/dist/core/stableJson.js +32 -0
- package/dist/core/stableJson.js.map +1 -0
- package/dist/core/state/policy.d.ts +20 -0
- package/dist/core/state/policy.js +51 -0
- package/dist/core/state/policy.js.map +1 -0
- package/dist/core/state/state.d.ts +67 -0
- package/dist/core/state/state.js +142 -0
- package/dist/core/state/state.js.map +1 -0
- package/dist/core/state/storage.d.ts +9 -0
- package/dist/core/state/storage.js +25 -0
- package/dist/core/state/storage.js.map +1 -0
- package/dist/core/targets/resolve.d.ts +28 -0
- package/dist/core/targets/resolve.js +96 -0
- package/dist/core/targets/resolve.js.map +1 -0
- package/dist/core/testGenerator/conventions.d.ts +7 -0
- package/dist/core/testGenerator/conventions.js +29 -0
- package/dist/core/testGenerator/conventions.js.map +1 -0
- package/dist/core/testGenerator/generate.d.ts +35 -0
- package/dist/core/testGenerator/generate.js +127 -0
- package/dist/core/testGenerator/generate.js.map +1 -0
- package/dist/core/testRunner/hints.d.ts +12 -0
- package/dist/core/testRunner/hints.js +133 -0
- package/dist/core/testRunner/hints.js.map +1 -0
- package/dist/core/testRunner/infer.d.ts +24 -0
- package/dist/core/testRunner/infer.js +65 -0
- package/dist/core/testRunner/infer.js.map +1 -0
- package/dist/core/testRunner/resolve.d.ts +12 -0
- package/dist/core/testRunner/resolve.js +31 -0
- package/dist/core/testRunner/resolve.js.map +1 -0
- package/dist/core/testRunner/runner.d.ts +24 -0
- package/dist/core/testRunner/runner.js +145 -0
- package/dist/core/testRunner/runner.js.map +1 -0
- package/dist/core/testability/heuristics.d.ts +7 -0
- package/dist/core/testability/heuristics.js +35 -0
- package/dist/core/testability/heuristics.js.map +1 -0
- package/dist/core/tests/fixers.d.ts +14 -0
- package/dist/core/tests/fixers.js +59 -0
- package/dist/core/tests/fixers.js.map +1 -0
- package/dist/core/tests/types.d.ts +98 -0
- package/dist/core/tests/types.js +2 -0
- package/dist/core/tests/types.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified diff parsing and application for LLM-generated test patches.
|
|
3
|
+
* Deterministic, fail-closed: rejects invalid, unsafe, or out-of-scope patches.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { validateGeneratedTestQuality } from './qualityGate.js';
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Path helpers
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
export function toPosix(p) {
|
|
12
|
+
return p.replace(/\\/g, '/');
|
|
13
|
+
}
|
|
14
|
+
export function isAllowedTestPathForRunner(runner, pathPosix) {
|
|
15
|
+
if (pathPosix.includes('..'))
|
|
16
|
+
return false;
|
|
17
|
+
if (pathPosix.startsWith('/'))
|
|
18
|
+
return false;
|
|
19
|
+
if (pathPosix.split('/').includes('node_modules'))
|
|
20
|
+
return false;
|
|
21
|
+
const r = runner.toLowerCase();
|
|
22
|
+
if (r === 'pytest') {
|
|
23
|
+
// Common pytest conventions: tests/test_*.py or *_test.py under tests/
|
|
24
|
+
if (!pathPosix.endsWith('.py'))
|
|
25
|
+
return false;
|
|
26
|
+
if (pathPosix.startsWith('tests/'))
|
|
27
|
+
return true;
|
|
28
|
+
return /(^|\/)test_[^/]+\.py$/.test(pathPosix) || /(^|\/)[^/]+_test\.py$/.test(pathPosix);
|
|
29
|
+
}
|
|
30
|
+
// Jest/Vitest: allow only TS/TSX/JS test files under common test conventions.
|
|
31
|
+
const ext = path.posix.extname(pathPosix).toLowerCase();
|
|
32
|
+
if (!(ext === '.ts' || ext === '.tsx' || ext === '.js'))
|
|
33
|
+
return false;
|
|
34
|
+
if (pathPosix.includes('__tests__/'))
|
|
35
|
+
return true;
|
|
36
|
+
return /\.(test|spec)\.(ts|tsx|js)$/.test(pathPosix);
|
|
37
|
+
}
|
|
38
|
+
export function isAllowedFixturePath(pathPosix) {
|
|
39
|
+
// Controlled fixture allowlist:
|
|
40
|
+
// - tests/fixtures/** (repo-root convention)
|
|
41
|
+
// - **/__fixtures__/** (Jest/Vitest convention)
|
|
42
|
+
// - **/__tests__/fixtures/** (Jest/Vitest convention)
|
|
43
|
+
if (pathPosix.includes('..'))
|
|
44
|
+
return false;
|
|
45
|
+
if (pathPosix.startsWith('/'))
|
|
46
|
+
return false;
|
|
47
|
+
if (pathPosix.split('/').includes('node_modules'))
|
|
48
|
+
return false;
|
|
49
|
+
const lower = pathPosix.toLowerCase();
|
|
50
|
+
if (lower.startsWith('tests/fixtures/'))
|
|
51
|
+
return true;
|
|
52
|
+
if (lower.startsWith('__fixtures__/'))
|
|
53
|
+
return true;
|
|
54
|
+
if (lower.includes('/__fixtures__/'))
|
|
55
|
+
return true;
|
|
56
|
+
if (lower.startsWith('__tests__/fixtures/'))
|
|
57
|
+
return true;
|
|
58
|
+
if (lower.includes('/__tests__/fixtures/'))
|
|
59
|
+
return true;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function isAllowedTouchedPathForRunner(runner, pathPosix) {
|
|
63
|
+
return isAllowedTestPathForRunner(runner, pathPosix) || isAllowedFixturePath(pathPosix);
|
|
64
|
+
}
|
|
65
|
+
function containsControlChars(s) {
|
|
66
|
+
return /[\u0000-\u001F\u007F]/.test(s);
|
|
67
|
+
}
|
|
68
|
+
function isUnsafePathHeader(raw) {
|
|
69
|
+
// Fail-closed: reject absolute paths, Windows drive paths, backslashes, "..", and control chars.
|
|
70
|
+
if (!raw)
|
|
71
|
+
return true;
|
|
72
|
+
if (containsControlChars(raw))
|
|
73
|
+
return true;
|
|
74
|
+
if (raw.includes('\\'))
|
|
75
|
+
return true;
|
|
76
|
+
if (raw.startsWith('/'))
|
|
77
|
+
return true;
|
|
78
|
+
if (/^[A-Za-z]:/.test(raw))
|
|
79
|
+
return true;
|
|
80
|
+
if (raw.includes('..'))
|
|
81
|
+
return true;
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
function hasDisallowedDiffMarkers(diffText) {
|
|
85
|
+
const t = diffText;
|
|
86
|
+
// Reject binary diffs and git rename/delete machinery.
|
|
87
|
+
if (t.includes('GIT binary patch') || t.includes('Binary files '))
|
|
88
|
+
return 'llm_reject_binary';
|
|
89
|
+
if (t.includes('rename from') || t.includes('rename to') || t.includes('deleted file mode'))
|
|
90
|
+
return 'llm_reject_rename_or_delete';
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
// Diff application
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
export function applyUnifiedDiffToTestFiles(opts) {
|
|
97
|
+
const warnings = [];
|
|
98
|
+
const normalizedDiff = opts.diffText.replace(/\r\n/g, '\n');
|
|
99
|
+
// Hard cap the diff itself (not just the produced file) for safety.
|
|
100
|
+
if (normalizedDiff.length > 25_000) {
|
|
101
|
+
return { ok: false, warnings: ['llm_reject_diff_too_large_chars'], pathsPosix: [], createdPathsPosix: [] };
|
|
102
|
+
}
|
|
103
|
+
const markerRejection = hasDisallowedDiffMarkers(normalizedDiff);
|
|
104
|
+
if (markerRejection)
|
|
105
|
+
return { ok: false, warnings: [markerRejection], pathsPosix: [], createdPathsPosix: [] };
|
|
106
|
+
const lines = normalizedDiff.split('\n');
|
|
107
|
+
const MAX_TOUCHED_FILES = 2;
|
|
108
|
+
const touched = [];
|
|
109
|
+
let cur = null;
|
|
110
|
+
const parseGitPath = (raw) => {
|
|
111
|
+
const r = raw.trim();
|
|
112
|
+
if (r === '/dev/null')
|
|
113
|
+
return '/dev/null';
|
|
114
|
+
return toPosix(r.replace(/^(a\/|b\/)/, ''));
|
|
115
|
+
};
|
|
116
|
+
for (const l of lines) {
|
|
117
|
+
if (l.startsWith('diff --git ')) {
|
|
118
|
+
if (cur)
|
|
119
|
+
touched.push(cur);
|
|
120
|
+
const m = l.match(/^diff --git\s+(a\/\S+)\s+(b\/\S+)/);
|
|
121
|
+
if (!m?.[1] || !m?.[2])
|
|
122
|
+
return { ok: false, warnings: ['llm_reject_invalid_diff'], pathsPosix: [], createdPathsPosix: [] };
|
|
123
|
+
cur = { oldPath: parseGitPath(m[1]), newPath: parseGitPath(m[2]), hunks: [] };
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!cur)
|
|
127
|
+
continue;
|
|
128
|
+
cur.hunks.push(l);
|
|
129
|
+
}
|
|
130
|
+
if (cur)
|
|
131
|
+
touched.push(cur);
|
|
132
|
+
if (touched.length === 0)
|
|
133
|
+
return { ok: false, warnings: ['llm_reject_invalid_diff'], pathsPosix: [], createdPathsPosix: [] };
|
|
134
|
+
if (touched.length > MAX_TOUCHED_FILES)
|
|
135
|
+
return { ok: false, warnings: ['llm_reject_multiple_files'], pathsPosix: [], createdPathsPosix: [] };
|
|
136
|
+
const boundaryRootPosix = opts.boundaryRootPosix ? toPosix(path.posix.normalize(opts.boundaryRootPosix)) : '';
|
|
137
|
+
const touchedPaths = [];
|
|
138
|
+
const createdPaths = [];
|
|
139
|
+
let totalAdded = 0;
|
|
140
|
+
const withinBoundary = (p) => {
|
|
141
|
+
if (!boundaryRootPosix)
|
|
142
|
+
return true;
|
|
143
|
+
const prefix = boundaryRootPosix.endsWith('/') ? boundaryRootPosix : `${boundaryRootPosix}/`;
|
|
144
|
+
return p === boundaryRootPosix || p.startsWith(prefix);
|
|
145
|
+
};
|
|
146
|
+
const readFileLines = (abs) => {
|
|
147
|
+
const raw = fs.readFileSync(abs, 'utf8').replace(/\r\n/g, '\n');
|
|
148
|
+
const s = raw.endsWith('\n') ? raw.slice(0, -1) : raw;
|
|
149
|
+
return s === '' ? [] : s.split('\n');
|
|
150
|
+
};
|
|
151
|
+
for (const file of touched) {
|
|
152
|
+
const headerOld = file.hunks.find((x) => x.startsWith('--- ')) ?? null;
|
|
153
|
+
const headerNew = file.hunks.find((x) => x.startsWith('+++ ')) ?? null;
|
|
154
|
+
if (!headerOld || !headerNew)
|
|
155
|
+
return { ok: false, warnings: ['llm_reject_invalid_diff'], pathsPosix: [], createdPathsPosix: [] };
|
|
156
|
+
const oldPath = parseGitPath(headerOld.slice(4));
|
|
157
|
+
const newPath = parseGitPath(headerNew.slice(4));
|
|
158
|
+
if (newPath === '/dev/null')
|
|
159
|
+
return { ok: false, warnings: ['llm_reject_rename_or_delete'], pathsPosix: [], createdPathsPosix: [] };
|
|
160
|
+
if (oldPath !== '/dev/null' && oldPath !== newPath) {
|
|
161
|
+
return { ok: false, warnings: ['llm_reject_rename_or_delete'], pathsPosix: [], createdPathsPosix: [] };
|
|
162
|
+
}
|
|
163
|
+
if (isUnsafePathHeader(newPath))
|
|
164
|
+
return { ok: false, warnings: ['llm_reject_unsafe_path'], pathsPosix: [], createdPathsPosix: [] };
|
|
165
|
+
const normalized = path.posix.normalize(newPath);
|
|
166
|
+
if (!isAllowedTouchedPathForRunner(opts.runner, normalized)) {
|
|
167
|
+
return { ok: false, warnings: ['llm_reject_invalid_test_path'], pathsPosix: [], createdPathsPosix: [] };
|
|
168
|
+
}
|
|
169
|
+
if (!withinBoundary(normalized)) {
|
|
170
|
+
return { ok: false, warnings: ['llm_reject_path_outside_boundary'], pathsPosix: [], createdPathsPosix: [] };
|
|
171
|
+
}
|
|
172
|
+
// Cap total added lines across all touched files.
|
|
173
|
+
for (const l of file.hunks) {
|
|
174
|
+
if (l.startsWith('+') && !l.startsWith('+++ '))
|
|
175
|
+
totalAdded++;
|
|
176
|
+
if (totalAdded > 250)
|
|
177
|
+
return { ok: false, warnings: ['llm_reject_diff_too_large_lines'], pathsPosix: [], createdPathsPosix: [] };
|
|
178
|
+
}
|
|
179
|
+
const abs = path.resolve(opts.rootAbs, ...normalized.split('/'));
|
|
180
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
181
|
+
const isAdd = oldPath === '/dev/null';
|
|
182
|
+
if (isAdd) {
|
|
183
|
+
if (fs.existsSync(abs))
|
|
184
|
+
return { ok: false, warnings: ['llm_reject_file_exists'], pathsPosix: [], createdPathsPosix: [] };
|
|
185
|
+
const contentLines = [];
|
|
186
|
+
for (const l of file.hunks) {
|
|
187
|
+
if (l.startsWith('+++ ') || l.startsWith('--- ') || l.startsWith('diff --git') || l.startsWith('index '))
|
|
188
|
+
continue;
|
|
189
|
+
if (l.startsWith('@@'))
|
|
190
|
+
continue;
|
|
191
|
+
if (l.startsWith('new file mode'))
|
|
192
|
+
continue;
|
|
193
|
+
if (l.startsWith('\\'))
|
|
194
|
+
continue;
|
|
195
|
+
if (l.startsWith('-'))
|
|
196
|
+
return { ok: false, warnings: ['llm_reject_rename_or_delete'], pathsPosix: [], createdPathsPosix: [] };
|
|
197
|
+
if (l.startsWith('+'))
|
|
198
|
+
contentLines.push(l.slice(1));
|
|
199
|
+
else if (l.startsWith(' '))
|
|
200
|
+
contentLines.push(l.slice(1));
|
|
201
|
+
}
|
|
202
|
+
const content = contentLines.join('\n').replace(/\n+$/, '') + '\n';
|
|
203
|
+
if (Buffer.byteLength(content, 'utf8') > 50_000)
|
|
204
|
+
return { ok: false, warnings: ['llm_reject_file_too_large_bytes'], pathsPosix: [], createdPathsPosix: [] };
|
|
205
|
+
// Only gate test files for reviewability; fixture files may be data blobs.
|
|
206
|
+
if (isAllowedTestPathForRunner(opts.runner, normalized)) {
|
|
207
|
+
const quality = validateGeneratedTestQuality({
|
|
208
|
+
runner: opts.runner,
|
|
209
|
+
content,
|
|
210
|
+
tuning: opts.tuning,
|
|
211
|
+
targetExportNames: opts.targetExportNames,
|
|
212
|
+
targetModuleStem: opts.targetModuleStem,
|
|
213
|
+
targetSource: opts.targetSource ?? null,
|
|
214
|
+
});
|
|
215
|
+
if (!quality.ok)
|
|
216
|
+
return { ok: false, warnings: [quality.code], pathsPosix: [], createdPathsPosix: [] };
|
|
217
|
+
}
|
|
218
|
+
fs.writeFileSync(abs, content, 'utf8');
|
|
219
|
+
createdPaths.push(normalized);
|
|
220
|
+
touchedPaths.push(normalized);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// Edit existing file: apply unified diff hunks with context validation.
|
|
224
|
+
if (!fs.existsSync(abs))
|
|
225
|
+
return { ok: false, warnings: ['llm_reject_invalid_diff'], pathsPosix: [], createdPathsPosix: [] };
|
|
226
|
+
const original = readFileLines(abs);
|
|
227
|
+
const out = [];
|
|
228
|
+
let idx = 0;
|
|
229
|
+
const hunksOnly = file.hunks.filter((l) => l.startsWith('@@') || l.startsWith(' ') || l.startsWith('+') || l.startsWith('-') || l.startsWith('\\'));
|
|
230
|
+
let inHunk = false;
|
|
231
|
+
for (const l of hunksOnly) {
|
|
232
|
+
if (l.startsWith('@@')) {
|
|
233
|
+
inHunk = true;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (!inHunk)
|
|
237
|
+
continue;
|
|
238
|
+
if (l.startsWith('\\'))
|
|
239
|
+
continue;
|
|
240
|
+
const kind = l[0];
|
|
241
|
+
const text = l.slice(1);
|
|
242
|
+
if (kind === ' ') {
|
|
243
|
+
const curLine = original[idx] ?? null;
|
|
244
|
+
if (curLine !== text)
|
|
245
|
+
return { ok: false, warnings: ['llm_reject_invalid_diff'], pathsPosix: [], createdPathsPosix: [] };
|
|
246
|
+
out.push(curLine);
|
|
247
|
+
idx++;
|
|
248
|
+
}
|
|
249
|
+
else if (kind === '-') {
|
|
250
|
+
const curLine = original[idx] ?? null;
|
|
251
|
+
if (curLine !== text)
|
|
252
|
+
return { ok: false, warnings: ['llm_reject_invalid_diff'], pathsPosix: [], createdPathsPosix: [] };
|
|
253
|
+
idx++;
|
|
254
|
+
}
|
|
255
|
+
else if (kind === '+') {
|
|
256
|
+
out.push(text);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Append remaining original (if any).
|
|
260
|
+
for (; idx < original.length; idx++)
|
|
261
|
+
out.push(original[idx] ?? '');
|
|
262
|
+
const nextContent = out.join('\n').replace(/\n+$/, '') + '\n';
|
|
263
|
+
if (Buffer.byteLength(nextContent, 'utf8') > 50_000)
|
|
264
|
+
return { ok: false, warnings: ['llm_reject_file_too_large_bytes'], pathsPosix: [], createdPathsPosix: [] };
|
|
265
|
+
fs.writeFileSync(abs, nextContent, 'utf8');
|
|
266
|
+
touchedPaths.push(normalized);
|
|
267
|
+
}
|
|
268
|
+
const uniqTouched = [...new Set(touchedPaths)].sort((a, b) => a.localeCompare(b));
|
|
269
|
+
const uniqCreated = [...new Set(createdPaths)].sort((a, b) => a.localeCompare(b));
|
|
270
|
+
// Ensure at least one test file is present (fixture-only patches are not allowed)
|
|
271
|
+
const hasTestFile = uniqTouched.some((p) => isAllowedTestPathForRunner(opts.runner, p));
|
|
272
|
+
if (!hasTestFile)
|
|
273
|
+
return { ok: false, warnings: ['llm_reject_no_test_file'], pathsPosix: [], createdPathsPosix: [] };
|
|
274
|
+
return { ok: true, warnings, pathsPosix: uniqTouched, createdPathsPosix: uniqCreated };
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=diffApply.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diffApply.js","sourceRoot":"","sources":["../../../../src/core/generator/llm/diffApply.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality gate for LLM-generated test files.
|
|
3
|
+
* Deterministic, fail-closed validation with no network or logging of file contents.
|
|
4
|
+
*/
|
|
5
|
+
export type LlmRejectCode = 'llm_reject_invalid_diff' | 'llm_reject_multiple_files' | 'llm_reject_binary' | 'llm_reject_rename_or_delete' | 'llm_reject_unsafe_path' | 'llm_reject_invalid_test_path' | 'llm_reject_path_outside_boundary' | 'llm_reject_diff_too_large_chars' | 'llm_reject_diff_too_large_lines' | 'llm_reject_file_too_large_bytes' | 'llm_reject_file_exists' | 'llm_reject_quality_too_few_tests' | 'llm_reject_quality_too_many_tests' | 'llm_reject_quality_too_few_expects' | 'llm_reject_quality_insufficient_matcher_diversity' | 'llm_reject_quality_too_shallow' | 'llm_reject_quality_vitest_style' | 'llm_reject_quality_too_few_assertions' | 'llm_reject_quality_trivial' | 'llm_reject_quality_missing_anchor' | 'llm_reject_quality_missing_edges' | 'llm_reject_no_test_file';
|
|
6
|
+
export declare const MAX_SHALLOW_RATIO_PCT = 40;
|
|
7
|
+
export declare const MIN_DISTINCT_MATCHERS = 3;
|
|
8
|
+
export declare const MIN_EDGE_CASES = 2;
|
|
9
|
+
export declare const ALLOWED_MATCHERS: Set<string>;
|
|
10
|
+
export declare const SHALLOW_MATCHERS: Set<string>;
|
|
11
|
+
export declare function countMatches(re: RegExp, text: string, cap?: number): number;
|
|
12
|
+
export declare function stripJsComments(text: string): string;
|
|
13
|
+
export declare function extractMatchersFromExpects(textRaw: string): {
|
|
14
|
+
matcherExpectCalls: number;
|
|
15
|
+
distinctAllowedMatchers: string[];
|
|
16
|
+
shallowExpectCalls: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function validateGeneratedTestQuality(opts: {
|
|
19
|
+
runner: string;
|
|
20
|
+
content: string;
|
|
21
|
+
tuning: {
|
|
22
|
+
minTestCases: number;
|
|
23
|
+
maxTestCases: number;
|
|
24
|
+
minExpectCalls: number;
|
|
25
|
+
};
|
|
26
|
+
targetExportNames: readonly string[];
|
|
27
|
+
targetModuleStem: string;
|
|
28
|
+
targetSource?: string | null;
|
|
29
|
+
}): {
|
|
30
|
+
ok: true;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
code: LlmRejectCode;
|
|
34
|
+
};
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality gate for LLM-generated test files.
|
|
3
|
+
* Deterministic, fail-closed validation with no network or logging of file contents.
|
|
4
|
+
*/
|
|
5
|
+
export const MAX_SHALLOW_RATIO_PCT = 40;
|
|
6
|
+
export const MIN_DISTINCT_MATCHERS = 3;
|
|
7
|
+
export const MIN_EDGE_CASES = 2;
|
|
8
|
+
// Signals that indicate edge/negative test cases (case-insensitive regex patterns)
|
|
9
|
+
const EDGE_CASE_SIGNALS_JS = [
|
|
10
|
+
/\bthrow/i,
|
|
11
|
+
/\binvalid/i,
|
|
12
|
+
/\bnull\b/i,
|
|
13
|
+
/\bundefined\b/i,
|
|
14
|
+
/\berror/i,
|
|
15
|
+
/\bempty/i,
|
|
16
|
+
/\bnegative/i,
|
|
17
|
+
/\bzero\b/i,
|
|
18
|
+
/\bboundary/i,
|
|
19
|
+
/\bedge/i,
|
|
20
|
+
/\bfail/i,
|
|
21
|
+
/\bexception/i,
|
|
22
|
+
/\breject/i,
|
|
23
|
+
/\.toThrow\s*\(/,
|
|
24
|
+
/\.toThrowError\s*\(/,
|
|
25
|
+
/\.rejects\s*\./,
|
|
26
|
+
];
|
|
27
|
+
const EDGE_CASE_SIGNALS_PY = [
|
|
28
|
+
/\braise/i,
|
|
29
|
+
/\binvalid/i,
|
|
30
|
+
/\bnone\b/i,
|
|
31
|
+
/\berror/i,
|
|
32
|
+
/\bempty/i,
|
|
33
|
+
/\bnegative/i,
|
|
34
|
+
/\bzero\b/i,
|
|
35
|
+
/\bboundary/i,
|
|
36
|
+
/\bedge/i,
|
|
37
|
+
/\bfail/i,
|
|
38
|
+
/\bexception/i,
|
|
39
|
+
/pytest\.raises/,
|
|
40
|
+
];
|
|
41
|
+
// Signals that suggest the target module is stateful
|
|
42
|
+
const STATEFUL_SIGNALS = [/\bset[A-Z]/i, /\bupdate/i, /\bcache/i, /\bMap\b/, /\bStore\b/i, /\bstate/i, /\bpush\b/, /\bdelete\b/, /\badd\b/, /\bremove\b/];
|
|
43
|
+
// Signals that tests exercise stateful behavior (JS/TS)
|
|
44
|
+
const STATEFUL_TEST_SIGNALS_JS = [/\bbefore/i, /\bafter/i, /\bsetup/i, /\bteardown/i, /\bmultiple/i, /\bsequence/i, /\bclear/i, /\breset/i];
|
|
45
|
+
// Signals that tests exercise stateful behavior (pytest)
|
|
46
|
+
const STATEFUL_TEST_SIGNALS_PY = [
|
|
47
|
+
/\bsetup/i,
|
|
48
|
+
/\bteardown/i,
|
|
49
|
+
/\bfixture/i,
|
|
50
|
+
/pytest\.fixture/,
|
|
51
|
+
/pytest\.mark\.parametrize/,
|
|
52
|
+
/\bmultiple/i,
|
|
53
|
+
/\bsequence/i,
|
|
54
|
+
/\bclear/i,
|
|
55
|
+
/\breset/i,
|
|
56
|
+
];
|
|
57
|
+
export const ALLOWED_MATCHERS = new Set([
|
|
58
|
+
'toBe',
|
|
59
|
+
'toEqual',
|
|
60
|
+
'toStrictEqual',
|
|
61
|
+
'toMatch',
|
|
62
|
+
'toContain',
|
|
63
|
+
'toHaveLength',
|
|
64
|
+
'toBeGreaterThan',
|
|
65
|
+
'toBeGreaterThanOrEqual',
|
|
66
|
+
'toBeLessThan',
|
|
67
|
+
'toBeLessThanOrEqual',
|
|
68
|
+
'toBeCloseTo',
|
|
69
|
+
'toThrow',
|
|
70
|
+
'toThrowError',
|
|
71
|
+
'toMatchObject',
|
|
72
|
+
'toHaveProperty',
|
|
73
|
+
]);
|
|
74
|
+
export const SHALLOW_MATCHERS = new Set(['toBeDefined', 'toBeTruthy', 'toBeFalsy', 'toBeNull', 'toBeUndefined']);
|
|
75
|
+
export function countMatches(re, text, cap = 10_000) {
|
|
76
|
+
// Deterministic and safe: linear scan with a hard cap.
|
|
77
|
+
let count = 0;
|
|
78
|
+
re.lastIndex = 0;
|
|
79
|
+
let m;
|
|
80
|
+
while ((m = re.exec(text))) {
|
|
81
|
+
count++;
|
|
82
|
+
if (count >= cap)
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
return count;
|
|
86
|
+
}
|
|
87
|
+
export function stripJsComments(text) {
|
|
88
|
+
// Deterministic, best-effort comment stripping (no string literal awareness).
|
|
89
|
+
// Sufficient for log-free, fail-closed heuristics.
|
|
90
|
+
const withoutBlock = text.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
91
|
+
return withoutBlock.replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
92
|
+
}
|
|
93
|
+
function escapeRegexLiteral(s) {
|
|
94
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
95
|
+
}
|
|
96
|
+
function hasModuleImportJsOrTs(textRaw, moduleStem) {
|
|
97
|
+
const text = stripJsComments(textRaw);
|
|
98
|
+
const stem = moduleStem.trim();
|
|
99
|
+
if (!stem)
|
|
100
|
+
return true; // can't enforce if no stem
|
|
101
|
+
const s = escapeRegexLiteral(stem);
|
|
102
|
+
if (new RegExp(`\\bimport\\b[\\s\\S]*?\\bfrom\\s+['"][^'"]*${s}[^'"]*['"]`, 'm').test(text))
|
|
103
|
+
return true;
|
|
104
|
+
if (new RegExp(`\\brequire\\s*\\(\\s*['"][^'"]*${s}[^'"]*['"]\\s*\\)`, 'm').test(text))
|
|
105
|
+
return true;
|
|
106
|
+
if (new RegExp(`\\bimport\\s*\\(\\s*['"][^'"]*${s}[^'"]*['"]\\s*\\)`, 'm').test(text))
|
|
107
|
+
return true;
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
function hasAnchorUsageJsOrTs(textRaw, anchors) {
|
|
111
|
+
if (anchors.length === 0)
|
|
112
|
+
return true; // no anchors to check = pass
|
|
113
|
+
const text = stripJsComments(textRaw);
|
|
114
|
+
for (const name of anchors) {
|
|
115
|
+
const n = escapeRegexLiteral(name);
|
|
116
|
+
// Strong usage signals: calling, or asserting about the symbol.
|
|
117
|
+
if (new RegExp(`\\b${n}\\s*\\(`, 'm').test(text))
|
|
118
|
+
return true;
|
|
119
|
+
if (new RegExp(`\\bexpect\\s*\\(\\s*${n}\\b`, 'm').test(text))
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
// Combined check for backward compat with trivial gate
|
|
125
|
+
function hasTargetUsageJsOrTs(textRaw, exportNames, moduleStem) {
|
|
126
|
+
if (exportNames.length > 0) {
|
|
127
|
+
return hasAnchorUsageJsOrTs(textRaw, exportNames);
|
|
128
|
+
}
|
|
129
|
+
return hasModuleImportJsOrTs(textRaw, moduleStem);
|
|
130
|
+
}
|
|
131
|
+
function hasModuleImportPy(textRaw, moduleStem) {
|
|
132
|
+
const stem = moduleStem.trim();
|
|
133
|
+
if (!stem)
|
|
134
|
+
return true; // can't enforce if no stem
|
|
135
|
+
const s = escapeRegexLiteral(stem);
|
|
136
|
+
if (new RegExp(`^\\s*from\\s+[^\\n]*\\b${s}\\b\\s+import\\b`, 'm').test(textRaw))
|
|
137
|
+
return true;
|
|
138
|
+
if (new RegExp(`^\\s*import\\s+[^\\n]*\\b${s}\\b`, 'm').test(textRaw))
|
|
139
|
+
return true;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
function hasAnchorUsagePy(textRaw, anchors) {
|
|
143
|
+
if (anchors.length === 0)
|
|
144
|
+
return true; // no anchors to check = pass
|
|
145
|
+
for (const name of anchors) {
|
|
146
|
+
const n = escapeRegexLiteral(name);
|
|
147
|
+
if (new RegExp(`\\b${n}\\s*\\(`, 'm').test(textRaw))
|
|
148
|
+
return true;
|
|
149
|
+
if (new RegExp(`\\bassert\\b[^\\n]*\\b${n}\\b`, 'm').test(textRaw))
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
// Combined check for backward compat with trivial gate
|
|
155
|
+
function hasTargetUsagePy(textRaw, defNames, moduleStem) {
|
|
156
|
+
if (defNames.length > 0) {
|
|
157
|
+
return hasAnchorUsagePy(textRaw, defNames);
|
|
158
|
+
}
|
|
159
|
+
return hasModuleImportPy(textRaw, moduleStem);
|
|
160
|
+
}
|
|
161
|
+
function countEdgeCaseSignals(content, signals) {
|
|
162
|
+
let count = 0;
|
|
163
|
+
for (const sig of signals) {
|
|
164
|
+
if (sig.test(content))
|
|
165
|
+
count++;
|
|
166
|
+
}
|
|
167
|
+
return count;
|
|
168
|
+
}
|
|
169
|
+
function isModuleStateful(targetSource) {
|
|
170
|
+
if (!targetSource)
|
|
171
|
+
return false;
|
|
172
|
+
for (const sig of STATEFUL_SIGNALS) {
|
|
173
|
+
if (sig.test(targetSource))
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
function hasStatefulTestEvidence(testContent, runner) {
|
|
179
|
+
const signals = runner === 'pytest' ? STATEFUL_TEST_SIGNALS_PY : STATEFUL_TEST_SIGNALS_JS;
|
|
180
|
+
for (const sig of signals) {
|
|
181
|
+
if (sig.test(testContent))
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
function isAllTrivialBoolSelfExpects(textRaw) {
|
|
187
|
+
const text = stripJsComments(textRaw);
|
|
188
|
+
const total = countMatches(/\bexpect\s*\(/g, text, 50_000);
|
|
189
|
+
if (total === 0)
|
|
190
|
+
return false;
|
|
191
|
+
const trivial = countMatches(/\bexpect\s*\(\s*(true|false)\s*\)\s*\.(?:toBe|toEqual|toStrictEqual)\s*\(\s*\1\s*\)/g, text, 50_000);
|
|
192
|
+
return trivial === total;
|
|
193
|
+
}
|
|
194
|
+
function isAllTrivialPyAsserts(textRaw) {
|
|
195
|
+
const total = countMatches(/^\s*assert\b/gm, textRaw, 50_000);
|
|
196
|
+
if (total === 0)
|
|
197
|
+
return false;
|
|
198
|
+
const trivial = countMatches(/^\s*assert\s+True\s*$/gm, textRaw, 50_000);
|
|
199
|
+
return trivial === total;
|
|
200
|
+
}
|
|
201
|
+
export function extractMatchersFromExpects(textRaw) {
|
|
202
|
+
const text = stripJsComments(textRaw);
|
|
203
|
+
const distinct = new Set();
|
|
204
|
+
let shallow = 0;
|
|
205
|
+
// Only count patterns of the form: expect(...).<matcher>(...)
|
|
206
|
+
// Keep bounded: file bytes are capped upstream; this regex is deterministic.
|
|
207
|
+
const re = /\bexpect\s*\([\s\S]*?\)\s*\.\s*(?:not\s*\.\s*)?([A-Za-z0-9_]+)\s*\(/g;
|
|
208
|
+
re.lastIndex = 0;
|
|
209
|
+
let matcherCalls = 0;
|
|
210
|
+
let m;
|
|
211
|
+
while ((m = re.exec(text))) {
|
|
212
|
+
matcherCalls++;
|
|
213
|
+
const matcher = (m[1] ?? '').trim();
|
|
214
|
+
if (!matcher)
|
|
215
|
+
continue;
|
|
216
|
+
if (SHALLOW_MATCHERS.has(matcher))
|
|
217
|
+
shallow++;
|
|
218
|
+
if (ALLOWED_MATCHERS.has(matcher))
|
|
219
|
+
distinct.add(matcher);
|
|
220
|
+
if (matcherCalls >= 50_000)
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
matcherExpectCalls: matcherCalls,
|
|
225
|
+
distinctAllowedMatchers: [...distinct].sort((a, b) => a.localeCompare(b)),
|
|
226
|
+
shallowExpectCalls: shallow,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
export function validateGeneratedTestQuality(opts) {
|
|
230
|
+
const r = opts.runner.toLowerCase();
|
|
231
|
+
const content = opts.content;
|
|
232
|
+
const tuning = opts.tuning;
|
|
233
|
+
const anchors = opts.targetExportNames;
|
|
234
|
+
const moduleStem = opts.targetModuleStem;
|
|
235
|
+
const targetSource = opts.targetSource ?? null;
|
|
236
|
+
if (r === 'pytest') {
|
|
237
|
+
const testCases = countMatches(/^\s*def\s+test_[A-Za-z0-9_]+\s*\(/gm, content, 10_000);
|
|
238
|
+
if (testCases < tuning.minTestCases)
|
|
239
|
+
return { ok: false, code: 'llm_reject_quality_too_few_tests' };
|
|
240
|
+
if (testCases > tuning.maxTestCases)
|
|
241
|
+
return { ok: false, code: 'llm_reject_quality_too_many_tests' };
|
|
242
|
+
const asserts = countMatches(/\bassert\b/g, content, 50_000);
|
|
243
|
+
if (asserts < tuning.minExpectCalls)
|
|
244
|
+
return { ok: false, code: 'llm_reject_quality_too_few_assertions' };
|
|
245
|
+
// Anchor check: require import AND anchor usage when anchors exist
|
|
246
|
+
if (anchors.length > 0) {
|
|
247
|
+
if (!hasModuleImportPy(content, moduleStem) || !hasAnchorUsagePy(content, anchors)) {
|
|
248
|
+
return { ok: false, code: 'llm_reject_quality_missing_anchor' };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!hasTargetUsagePy(content, anchors, moduleStem) || isAllTrivialPyAsserts(content)) {
|
|
252
|
+
return { ok: false, code: 'llm_reject_quality_trivial' };
|
|
253
|
+
}
|
|
254
|
+
// Edge case check for pytest
|
|
255
|
+
const edgeSignals = countEdgeCaseSignals(content, EDGE_CASE_SIGNALS_PY);
|
|
256
|
+
if (edgeSignals < MIN_EDGE_CASES)
|
|
257
|
+
return { ok: false, code: 'llm_reject_quality_missing_edges' };
|
|
258
|
+
// Stateful check for pytest
|
|
259
|
+
if (isModuleStateful(targetSource) && !hasStatefulTestEvidence(content, 'pytest')) {
|
|
260
|
+
return { ok: false, code: 'llm_reject_quality_missing_edges' };
|
|
261
|
+
}
|
|
262
|
+
return { ok: true };
|
|
263
|
+
}
|
|
264
|
+
// Jest/Vitest.
|
|
265
|
+
const testCases = countMatches(/\b(?:test|it)\s*\(/g, content, 10_000);
|
|
266
|
+
if (testCases < tuning.minTestCases)
|
|
267
|
+
return { ok: false, code: 'llm_reject_quality_too_few_tests' };
|
|
268
|
+
if (testCases > tuning.maxTestCases)
|
|
269
|
+
return { ok: false, code: 'llm_reject_quality_too_many_tests' };
|
|
270
|
+
if (r === 'vitest') {
|
|
271
|
+
// Enforce style: explicit vitest imports + ensure used test API is imported.
|
|
272
|
+
const importLine = content.match(/import\s+\{([^}]+)\}\s+from\s+['"]vitest['"]/);
|
|
273
|
+
const imported = (importLine?.[1] ?? '')
|
|
274
|
+
.split(',')
|
|
275
|
+
.map((s) => s.trim())
|
|
276
|
+
.filter(Boolean);
|
|
277
|
+
const importedSet = new Set(imported);
|
|
278
|
+
if (!importedSet.has('expect'))
|
|
279
|
+
return { ok: false, code: 'llm_reject_quality_vitest_style' };
|
|
280
|
+
const usesIt = /\bit\s*\(/.test(content);
|
|
281
|
+
const usesTest = /\btest\s*\(/.test(content);
|
|
282
|
+
if (usesIt && !importedSet.has('it'))
|
|
283
|
+
return { ok: false, code: 'llm_reject_quality_vitest_style' };
|
|
284
|
+
if (usesTest && !importedSet.has('test'))
|
|
285
|
+
return { ok: false, code: 'llm_reject_quality_vitest_style' };
|
|
286
|
+
if (!usesIt && !usesTest)
|
|
287
|
+
return { ok: false, code: 'llm_reject_quality_vitest_style' };
|
|
288
|
+
// Mocking correctness: if vi.fn/vi.spyOn/vi.mock used, must import vi from vitest
|
|
289
|
+
const usesMocking = /\bvi\s*\.\s*(?:fn|spyOn|mock)\s*\(/.test(content);
|
|
290
|
+
if (usesMocking && !importedSet.has('vi'))
|
|
291
|
+
return { ok: false, code: 'llm_reject_quality_vitest_style' };
|
|
292
|
+
}
|
|
293
|
+
// Anchor check: require import AND anchor usage when anchors exist (stricter than trivial check)
|
|
294
|
+
if (anchors.length > 0) {
|
|
295
|
+
if (!hasModuleImportJsOrTs(content, moduleStem) || !hasAnchorUsageJsOrTs(content, anchors)) {
|
|
296
|
+
return { ok: false, code: 'llm_reject_quality_missing_anchor' };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Reject trivial tests: avoid "expect(true).toBe(true)" suites.
|
|
300
|
+
if (!hasTargetUsageJsOrTs(content, anchors, moduleStem) || isAllTrivialBoolSelfExpects(content)) {
|
|
301
|
+
return { ok: false, code: 'llm_reject_quality_trivial' };
|
|
302
|
+
}
|
|
303
|
+
const metrics = extractMatchersFromExpects(content);
|
|
304
|
+
const total = metrics.matcherExpectCalls;
|
|
305
|
+
if (total < tuning.minExpectCalls)
|
|
306
|
+
return { ok: false, code: 'llm_reject_quality_too_few_expects' };
|
|
307
|
+
if (metrics.distinctAllowedMatchers.length < MIN_DISTINCT_MATCHERS) {
|
|
308
|
+
return { ok: false, code: 'llm_reject_quality_insufficient_matcher_diversity' };
|
|
309
|
+
}
|
|
310
|
+
// shallow ratio must be < 40% (strict)
|
|
311
|
+
const shallowPct = Math.floor((metrics.shallowExpectCalls * 100) / (total > 0 ? total : 1));
|
|
312
|
+
if (shallowPct >= MAX_SHALLOW_RATIO_PCT)
|
|
313
|
+
return { ok: false, code: 'llm_reject_quality_too_shallow' };
|
|
314
|
+
// Edge case check for Jest/Vitest
|
|
315
|
+
const edgeSignals = countEdgeCaseSignals(content, EDGE_CASE_SIGNALS_JS);
|
|
316
|
+
if (edgeSignals < MIN_EDGE_CASES)
|
|
317
|
+
return { ok: false, code: 'llm_reject_quality_missing_edges' };
|
|
318
|
+
// Stateful check: if target appears stateful, require stateful test evidence
|
|
319
|
+
if (isModuleStateful(targetSource) && !hasStatefulTestEvidence(content, r)) {
|
|
320
|
+
return { ok: false, code: 'llm_reject_quality_missing_edges' };
|
|
321
|
+
}
|
|
322
|
+
return { ok: true };
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=qualityGate.js.map
|