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.
Files changed (159) hide show
  1. package/LICENSE.md +0 -0
  2. package/README.md +143 -0
  3. package/dist/adapters/git.d.ts +28 -0
  4. package/dist/adapters/git.js +112 -0
  5. package/dist/adapters/git.js.map +1 -0
  6. package/dist/adapters/github.d.ts +71 -0
  7. package/dist/adapters/github.js +194 -0
  8. package/dist/adapters/github.js.map +1 -0
  9. package/dist/cli.d.ts +47 -0
  10. package/dist/cli.js +201 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/commands/context.d.ts +2 -0
  13. package/dist/commands/context.js +275 -0
  14. package/dist/commands/context.js.map +1 -0
  15. package/dist/commands/full.d.ts +13 -0
  16. package/dist/commands/full.js +590 -0
  17. package/dist/commands/full.js.map +1 -0
  18. package/dist/commands/gen_test.d.ts +2 -0
  19. package/dist/commands/gen_test.js +94 -0
  20. package/dist/commands/gen_test.js.map +1 -0
  21. package/dist/commands/index.d.ts +2 -0
  22. package/dist/commands/index.js +62 -0
  23. package/dist/commands/index.js.map +1 -0
  24. package/dist/commands/plan.d.ts +2 -0
  25. package/dist/commands/plan.js +107 -0
  26. package/dist/commands/plan.js.map +1 -0
  27. package/dist/commands/pr.d.ts +9 -0
  28. package/dist/commands/pr.js +400 -0
  29. package/dist/commands/pr.js.map +1 -0
  30. package/dist/commands/pr_list.d.ts +2 -0
  31. package/dist/commands/pr_list.js +158 -0
  32. package/dist/commands/pr_list.js.map +1 -0
  33. package/dist/commands/status.d.ts +6 -0
  34. package/dist/commands/status.js +132 -0
  35. package/dist/commands/status.js.map +1 -0
  36. package/dist/commands/test.d.ts +2 -0
  37. package/dist/commands/test.js +143 -0
  38. package/dist/commands/test.js.map +1 -0
  39. package/dist/commands/testability.d.ts +10 -0
  40. package/dist/commands/testability.js +406 -0
  41. package/dist/commands/testability.js.map +1 -0
  42. package/dist/commands/tests.d.ts +9 -0
  43. package/dist/commands/tests.js +801 -0
  44. package/dist/commands/tests.js.map +1 -0
  45. package/dist/core/code/exports.d.ts +5 -0
  46. package/dist/core/code/exports.js +68 -0
  47. package/dist/core/code/exports.js.map +1 -0
  48. package/dist/core/config/forkPolicy.d.ts +25 -0
  49. package/dist/core/config/forkPolicy.js +35 -0
  50. package/dist/core/config/forkPolicy.js.map +1 -0
  51. package/dist/core/config/load.d.ts +13 -0
  52. package/dist/core/config/load.js +35 -0
  53. package/dist/core/config/load.js.map +1 -0
  54. package/dist/core/config/types.d.ts +87 -0
  55. package/dist/core/config/types.js +2 -0
  56. package/dist/core/config/types.js.map +1 -0
  57. package/dist/core/config/validate.d.ts +4 -0
  58. package/dist/core/config/validate.js +246 -0
  59. package/dist/core/config/validate.js.map +1 -0
  60. package/dist/core/context/packer.d.ts +31 -0
  61. package/dist/core/context/packer.js +345 -0
  62. package/dist/core/context/packer.js.map +1 -0
  63. package/dist/core/context/types.d.ts +41 -0
  64. package/dist/core/context/types.js +2 -0
  65. package/dist/core/context/types.js.map +1 -0
  66. package/dist/core/converge/loop.d.ts +13 -0
  67. package/dist/core/converge/loop.js +35 -0
  68. package/dist/core/converge/loop.js.map +1 -0
  69. package/dist/core/converge/types.d.ts +15 -0
  70. package/dist/core/converge/types.js +2 -0
  71. package/dist/core/converge/types.js.map +1 -0
  72. package/dist/core/generator/llm/diffApply.d.ts +26 -0
  73. package/dist/core/generator/llm/diffApply.js +276 -0
  74. package/dist/core/generator/llm/diffApply.js.map +1 -0
  75. package/dist/core/generator/llm/qualityGate.d.ts +34 -0
  76. package/dist/core/generator/llm/qualityGate.js +324 -0
  77. package/dist/core/generator/llm/qualityGate.js.map +1 -0
  78. package/dist/core/generator/llmGenerator.d.ts +34 -0
  79. package/dist/core/generator/llmGenerator.js +245 -0
  80. package/dist/core/generator/llmGenerator.js.map +1 -0
  81. package/dist/core/generator/quality.d.ts +17 -0
  82. package/dist/core/generator/quality.js +31 -0
  83. package/dist/core/generator/quality.js.map +1 -0
  84. package/dist/core/generator/registry.d.ts +26 -0
  85. package/dist/core/generator/registry.js +29 -0
  86. package/dist/core/generator/registry.js.map +1 -0
  87. package/dist/core/generator/smokeGenerator.d.ts +11 -0
  88. package/dist/core/generator/smokeGenerator.js +27 -0
  89. package/dist/core/generator/smokeGenerator.js.map +1 -0
  90. package/dist/core/generator/types.d.ts +48 -0
  91. package/dist/core/generator/types.js +2 -0
  92. package/dist/core/generator/types.js.map +1 -0
  93. package/dist/core/index/indexer.d.ts +29 -0
  94. package/dist/core/index/indexer.js +167 -0
  95. package/dist/core/index/indexer.js.map +1 -0
  96. package/dist/core/jest/parser.d.ts +17 -0
  97. package/dist/core/jest/parser.js +90 -0
  98. package/dist/core/jest/parser.js.map +1 -0
  99. package/dist/core/llm/provider.d.ts +55 -0
  100. package/dist/core/llm/provider.js +105 -0
  101. package/dist/core/llm/provider.js.map +1 -0
  102. package/dist/core/logger.d.ts +19 -0
  103. package/dist/core/logger.js +44 -0
  104. package/dist/core/logger.js.map +1 -0
  105. package/dist/core/plan/planner.d.ts +16 -0
  106. package/dist/core/plan/planner.js +91 -0
  107. package/dist/core/plan/planner.js.map +1 -0
  108. package/dist/core/process/exec.d.ts +22 -0
  109. package/dist/core/process/exec.js +83 -0
  110. package/dist/core/process/exec.js.map +1 -0
  111. package/dist/core/redact.d.ts +6 -0
  112. package/dist/core/redact.js +49 -0
  113. package/dist/core/redact.js.map +1 -0
  114. package/dist/core/repo/boundary.d.ts +10 -0
  115. package/dist/core/repo/boundary.js +58 -0
  116. package/dist/core/repo/boundary.js.map +1 -0
  117. package/dist/core/stableJson.d.ts +1 -0
  118. package/dist/core/stableJson.js +32 -0
  119. package/dist/core/stableJson.js.map +1 -0
  120. package/dist/core/state/policy.d.ts +20 -0
  121. package/dist/core/state/policy.js +51 -0
  122. package/dist/core/state/policy.js.map +1 -0
  123. package/dist/core/state/state.d.ts +67 -0
  124. package/dist/core/state/state.js +142 -0
  125. package/dist/core/state/state.js.map +1 -0
  126. package/dist/core/state/storage.d.ts +9 -0
  127. package/dist/core/state/storage.js +25 -0
  128. package/dist/core/state/storage.js.map +1 -0
  129. package/dist/core/targets/resolve.d.ts +28 -0
  130. package/dist/core/targets/resolve.js +96 -0
  131. package/dist/core/targets/resolve.js.map +1 -0
  132. package/dist/core/testGenerator/conventions.d.ts +7 -0
  133. package/dist/core/testGenerator/conventions.js +29 -0
  134. package/dist/core/testGenerator/conventions.js.map +1 -0
  135. package/dist/core/testGenerator/generate.d.ts +35 -0
  136. package/dist/core/testGenerator/generate.js +127 -0
  137. package/dist/core/testGenerator/generate.js.map +1 -0
  138. package/dist/core/testRunner/hints.d.ts +12 -0
  139. package/dist/core/testRunner/hints.js +133 -0
  140. package/dist/core/testRunner/hints.js.map +1 -0
  141. package/dist/core/testRunner/infer.d.ts +24 -0
  142. package/dist/core/testRunner/infer.js +65 -0
  143. package/dist/core/testRunner/infer.js.map +1 -0
  144. package/dist/core/testRunner/resolve.d.ts +12 -0
  145. package/dist/core/testRunner/resolve.js +31 -0
  146. package/dist/core/testRunner/resolve.js.map +1 -0
  147. package/dist/core/testRunner/runner.d.ts +24 -0
  148. package/dist/core/testRunner/runner.js +145 -0
  149. package/dist/core/testRunner/runner.js.map +1 -0
  150. package/dist/core/testability/heuristics.d.ts +7 -0
  151. package/dist/core/testability/heuristics.js +35 -0
  152. package/dist/core/testability/heuristics.js.map +1 -0
  153. package/dist/core/tests/fixers.d.ts +14 -0
  154. package/dist/core/tests/fixers.js +59 -0
  155. package/dist/core/tests/fixers.js.map +1 -0
  156. package/dist/core/tests/types.d.ts +98 -0
  157. package/dist/core/tests/types.js +2 -0
  158. package/dist/core/tests/types.js.map +1 -0
  159. package/package.json +55 -0
@@ -0,0 +1,801 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { stableStringify } from '../core/stableJson.js';
5
+ import { writeStableJson } from '../core/state/storage.js';
6
+ import { resolveTargetIdOrPath } from '../core/targets/resolve.js';
7
+ import { loadOrInitPolicy } from '../core/state/policy.js';
8
+ import { loadConfigFile } from '../core/config/load.js';
9
+ import { validateConfig } from '../core/config/validate.js';
10
+ import { runTestCommand } from '../core/testRunner/runner.js';
11
+ import { inferTestCommandAndRunner } from '../core/testRunner/infer.js';
12
+ import { extractRunnerHints } from '../core/testRunner/hints.js';
13
+ import { createGenerator, pickGenerator } from '../core/generator/registry.js';
14
+ import { previewGeneratedTestFiles } from '../core/testGenerator/generate.js';
15
+ import { parseJestOutput } from '../core/jest/parser.js';
16
+ import { applyFixers } from '../core/tests/fixers.js';
17
+ import { createGitAdapter } from '../adapters/git.js';
18
+ import { createGithubAdapter } from '../adapters/github.js';
19
+ import { findNearestRepoBoundary } from '../core/repo/boundary.js';
20
+ import { HttpProvider } from '../core/llm/provider.js';
21
+ /**
22
+ * Fork safety note: The tests command does not independently check fork safety
23
+ * because it has no PR context. Fork safety is enforced by the calling orchestrator
24
+ * (full, pr) which must check fork context before invoking tests.
25
+ *
26
+ * When invoked directly (invoker=tests), the user is responsible for ensuring
27
+ * they are not operating on a fork-derived ref in a way that violates policy.
28
+ */
29
+ function toPosix(p) {
30
+ return p.replace(/\\/g, '/');
31
+ }
32
+ function parseArgs(argv) {
33
+ const errors = [];
34
+ let root;
35
+ let target;
36
+ let configPath;
37
+ let reportPath;
38
+ let invoker;
39
+ let baseRef;
40
+ let branch;
41
+ let dryRun = false;
42
+ let noPr = false;
43
+ let inferOnly = false;
44
+ let remote;
45
+ let maxIterations;
46
+ let mustPassConsecutiveRuns;
47
+ let timeoutMs;
48
+ for (let i = 0; i < argv.length; i++) {
49
+ const a = argv[i] ?? '';
50
+ if (a === '--root') {
51
+ const v = argv[i + 1];
52
+ if (typeof v !== 'string' || v.trim() === '')
53
+ errors.push('tests: --root requires a path');
54
+ else {
55
+ root = v;
56
+ i++;
57
+ }
58
+ continue;
59
+ }
60
+ if (a.startsWith('--root=')) {
61
+ const v = a.slice('--root='.length);
62
+ if (v.trim() === '')
63
+ errors.push('tests: --root requires a path');
64
+ else
65
+ root = v;
66
+ continue;
67
+ }
68
+ if (a === '--target') {
69
+ const v = argv[i + 1];
70
+ if (typeof v !== 'string' || v.trim() === '')
71
+ errors.push('tests: --target requires a value');
72
+ else {
73
+ target = v;
74
+ i++;
75
+ }
76
+ continue;
77
+ }
78
+ if (a.startsWith('--target=')) {
79
+ const v = a.slice('--target='.length);
80
+ if (v.trim() === '')
81
+ errors.push('tests: --target requires a value');
82
+ else
83
+ target = v;
84
+ continue;
85
+ }
86
+ if (a === '--config' || a === '-c') {
87
+ const v = argv[i + 1];
88
+ if (typeof v !== 'string' || v.trim() === '')
89
+ errors.push('tests: --config requires a path');
90
+ else {
91
+ configPath = v;
92
+ i++;
93
+ }
94
+ continue;
95
+ }
96
+ if (a.startsWith('--config=')) {
97
+ const v = a.slice('--config='.length);
98
+ if (v.trim() === '')
99
+ errors.push('tests: --config requires a path');
100
+ else
101
+ configPath = v;
102
+ continue;
103
+ }
104
+ if (a === '--report') {
105
+ const v = argv[i + 1];
106
+ if (typeof v !== 'string' || v.trim() === '')
107
+ errors.push('tests: --report requires a path');
108
+ else {
109
+ reportPath = v;
110
+ i++;
111
+ }
112
+ continue;
113
+ }
114
+ if (a.startsWith('--report=')) {
115
+ const v = a.slice('--report='.length);
116
+ if (v.trim() === '')
117
+ errors.push('tests: --report requires a path');
118
+ else
119
+ reportPath = v;
120
+ continue;
121
+ }
122
+ if (a === '--invoker') {
123
+ const v = argv[i + 1];
124
+ if (typeof v !== 'string' || v.trim() === '')
125
+ errors.push('tests: --invoker requires a value');
126
+ else {
127
+ invoker = v.trim();
128
+ i++;
129
+ }
130
+ continue;
131
+ }
132
+ if (a.startsWith('--invoker=')) {
133
+ const v = a.slice('--invoker='.length).trim();
134
+ if (v === '')
135
+ errors.push('tests: --invoker requires a value');
136
+ else
137
+ invoker = v;
138
+ continue;
139
+ }
140
+ if (a === '--baseRef') {
141
+ const v = argv[i + 1];
142
+ if (typeof v !== 'string' || v.trim() === '')
143
+ errors.push('tests: --baseRef requires a value');
144
+ else {
145
+ baseRef = v.trim();
146
+ i++;
147
+ }
148
+ continue;
149
+ }
150
+ if (a.startsWith('--baseRef=')) {
151
+ const v = a.slice('--baseRef='.length).trim();
152
+ if (v === '')
153
+ errors.push('tests: --baseRef requires a value');
154
+ else
155
+ baseRef = v;
156
+ continue;
157
+ }
158
+ if (a === '--branch') {
159
+ const v = argv[i + 1];
160
+ if (typeof v !== 'string' || v.trim() === '')
161
+ errors.push('tests: --branch requires a value');
162
+ else {
163
+ branch = v.trim();
164
+ i++;
165
+ }
166
+ continue;
167
+ }
168
+ if (a.startsWith('--branch=')) {
169
+ const v = a.slice('--branch='.length).trim();
170
+ if (v === '')
171
+ errors.push('tests: --branch requires a value');
172
+ else
173
+ branch = v;
174
+ continue;
175
+ }
176
+ if (a === '--help' || a === '-h')
177
+ continue;
178
+ if (a === '--dryRun' || a === '--dry-run') {
179
+ dryRun = true;
180
+ continue;
181
+ }
182
+ if (a === '--noPr' || a === '--no-pr') {
183
+ noPr = true;
184
+ continue;
185
+ }
186
+ if (a === '--inferOnly' || a === '--infer-only' || a === '--noExec' || a === '--no-exec') {
187
+ inferOnly = true;
188
+ continue;
189
+ }
190
+ if (a === '--remote') {
191
+ const v = argv[i + 1];
192
+ if (typeof v !== 'string' || v.trim() === '')
193
+ errors.push('tests: --remote requires a value');
194
+ else {
195
+ remote = v;
196
+ i++;
197
+ }
198
+ continue;
199
+ }
200
+ if (a.startsWith('--remote=')) {
201
+ const v = a.slice('--remote='.length);
202
+ if (v.trim() === '')
203
+ errors.push('tests: --remote requires a value');
204
+ else
205
+ remote = v;
206
+ continue;
207
+ }
208
+ if (a === '--max-iterations') {
209
+ const v = argv[i + 1];
210
+ const n = v ? Number(v) : NaN;
211
+ if (!Number.isInteger(n) || n <= 0)
212
+ errors.push('tests: --max-iterations must be a positive integer');
213
+ else {
214
+ maxIterations = n;
215
+ i++;
216
+ }
217
+ continue;
218
+ }
219
+ if (a.startsWith('--max-iterations=')) {
220
+ const v = a.slice('--max-iterations='.length);
221
+ const n = Number(v);
222
+ if (!Number.isInteger(n) || n <= 0)
223
+ errors.push('tests: --max-iterations must be a positive integer');
224
+ else
225
+ maxIterations = n;
226
+ continue;
227
+ }
228
+ if (a === '--must-pass-consecutive-runs') {
229
+ const v = argv[i + 1];
230
+ const n = v ? Number(v) : NaN;
231
+ if (!Number.isInteger(n) || n <= 0)
232
+ errors.push('tests: --must-pass-consecutive-runs must be a positive integer');
233
+ else {
234
+ mustPassConsecutiveRuns = n;
235
+ i++;
236
+ }
237
+ continue;
238
+ }
239
+ if (a.startsWith('--must-pass-consecutive-runs=')) {
240
+ const v = a.slice('--must-pass-consecutive-runs='.length);
241
+ const n = Number(v);
242
+ if (!Number.isInteger(n) || n <= 0)
243
+ errors.push('tests: --must-pass-consecutive-runs must be a positive integer');
244
+ else
245
+ mustPassConsecutiveRuns = n;
246
+ continue;
247
+ }
248
+ if (a === '--timeout-ms') {
249
+ const v = argv[i + 1];
250
+ const n = v ? Number(v) : NaN;
251
+ if (!Number.isInteger(n) || n <= 0)
252
+ errors.push('tests: --timeout-ms must be a positive integer');
253
+ else {
254
+ timeoutMs = n;
255
+ i++;
256
+ }
257
+ continue;
258
+ }
259
+ if (a.startsWith('--timeout-ms=')) {
260
+ const v = a.slice('--timeout-ms='.length);
261
+ const n = Number(v);
262
+ if (!Number.isInteger(n) || n <= 0)
263
+ errors.push('tests: --timeout-ms must be a positive integer');
264
+ else
265
+ timeoutMs = n;
266
+ continue;
267
+ }
268
+ errors.push(`tests: unknown argument "${a}"`);
269
+ }
270
+ errors.sort((x, y) => x.localeCompare(y));
271
+ const args = {};
272
+ if (root !== undefined)
273
+ args.root = root;
274
+ if (target !== undefined)
275
+ args.target = target;
276
+ if (configPath !== undefined)
277
+ args.configPath = configPath;
278
+ if (reportPath !== undefined)
279
+ args.reportPath = reportPath;
280
+ if (invoker !== undefined)
281
+ args.invoker = invoker;
282
+ if (baseRef !== undefined)
283
+ args.baseRef = baseRef;
284
+ if (branch !== undefined)
285
+ args.branch = branch;
286
+ args.dryRun = dryRun;
287
+ args.noPr = noPr;
288
+ args.inferOnly = inferOnly;
289
+ if (remote !== undefined)
290
+ args.remote = remote;
291
+ if (maxIterations !== undefined)
292
+ args.maxIterations = maxIterations;
293
+ if (mustPassConsecutiveRuns !== undefined)
294
+ args.mustPassConsecutiveRuns = mustPassConsecutiveRuns;
295
+ if (timeoutMs !== undefined)
296
+ args.timeoutMs = timeoutMs;
297
+ return { args, errors };
298
+ }
299
+ export async function runTestsWithLogger(argv, logger, deps) {
300
+ const parsed = parseArgs(argv);
301
+ if (parsed.errors.length > 0) {
302
+ for (const e of parsed.errors)
303
+ logger.error(e);
304
+ return 2;
305
+ }
306
+ const targetInput = parsed.args.target?.trim() ?? '';
307
+ const rootAbs = path.resolve(process.cwd(), parsed.args.root ?? '.');
308
+ const planPathAbs = path.join(rootAbs, '.test-agent', 'plan.json');
309
+ const indexPathAbs = path.join(rootAbs, '.test-agent', 'index.json');
310
+ const reportArg = parsed.args.reportPath?.trim();
311
+ const reportRel = reportArg ? reportArg.replace(/\\/g, '/') : '.test-agent/report.json';
312
+ const reportAbs = reportArg && path.isAbsolute(reportArg) ? reportArg : path.resolve(rootAbs, reportRel);
313
+ const remote = parsed.args.remote?.trim() || 'origin';
314
+ const maxIterations = Math.max(1, Math.trunc(parsed.args.maxIterations ?? 5));
315
+ const baseReport = {
316
+ schemaVersion: 1,
317
+ command: 'tests',
318
+ dryRun: parsed.args.dryRun ?? false,
319
+ noPr: parsed.args.noPr ?? false,
320
+ inferOnly: parsed.args.inferOnly ?? false,
321
+ remote,
322
+ decision: {
323
+ lane: 'tests',
324
+ skipReason: null,
325
+ caps: { maxIterations: { limit: maxIterations, used: 0 }, maxPRsPerRun: { limit: null, used: null } },
326
+ llm: { enabled: false, reason: 'pending', generatorWarnings: [] },
327
+ },
328
+ generatorKind: 'smoke',
329
+ generatorSummary: '',
330
+ generatorWarnings: [],
331
+ llmAccess: { invoker: 'unknown', allowCommands: [], allowed: false, denylistHits: [] },
332
+ llmContext: null,
333
+ runner: 'unknown',
334
+ runnerInferred: 'unknown',
335
+ runnerObserved: 'unknown',
336
+ boundary: { kind: 'unknown', boundaryFileRel: null, packageRootAbs: rootAbs, packageRootRel: '' },
337
+ reportPath: reportRel.replace(/\\/g, '/'),
338
+ planPath: '.test-agent/plan.json',
339
+ indexPath: '.test-agent/index.json',
340
+ targetInput,
341
+ targetId: null,
342
+ targetPath: null,
343
+ generatedFiles: [],
344
+ testCommandUsed: null,
345
+ testCommandSource: 'none',
346
+ testCommandReason: null,
347
+ mustPassConsecutiveRuns: Math.max(1, Math.trunc(parsed.args.mustPassConsecutiveRuns ?? 1)),
348
+ maxIterations,
349
+ finalOutcome: null,
350
+ git: { branch: null, pushed: false },
351
+ pr: { created: false, url: null, skippedReason: 'not_attempted' },
352
+ suggestions: [],
353
+ warnings: [],
354
+ errors: [],
355
+ attempts: [],
356
+ };
357
+ if (!targetInput) {
358
+ const report = { ...baseReport, errors: ['Missing --target <id|path>'] };
359
+ writeStableJson(reportAbs, report);
360
+ logger.output(stableStringify(report));
361
+ return 2;
362
+ }
363
+ if (!fs.existsSync(planPathAbs)) {
364
+ const report = {
365
+ ...baseReport,
366
+ errors: [`Missing plan at .test-agent/plan.json. Run "test-agent plan" first.`],
367
+ };
368
+ writeStableJson(reportAbs, report);
369
+ logger.output(stableStringify(report));
370
+ return 2;
371
+ }
372
+ if (!fs.existsSync(indexPathAbs)) {
373
+ const report = { ...baseReport, errors: ['Missing index at .test-agent/index.json. Run "test-agent index" first.'] };
374
+ writeStableJson(reportAbs, report);
375
+ logger.output(stableStringify(report));
376
+ return 2;
377
+ }
378
+ let planData;
379
+ try {
380
+ planData = JSON.parse(fs.readFileSync(planPathAbs, 'utf8'));
381
+ }
382
+ catch (e) {
383
+ const report = { ...baseReport, errors: [`Failed to parse plan.json: ${String(e)}`] };
384
+ writeStableJson(reportAbs, report);
385
+ logger.output(stableStringify(report));
386
+ return 2;
387
+ }
388
+ const resolved = resolveTargetIdOrPath({ root: rootAbs, plan: planData, targetInput });
389
+ if (!resolved.found) {
390
+ const report = { ...baseReport, suggestions: resolved.suggestions };
391
+ writeStableJson(reportAbs, report);
392
+ logger.output(stableStringify(report));
393
+ return 2;
394
+ }
395
+ const target = resolved.target;
396
+ const targetPaths = [...new Set(target.paths)].sort((a, b) => a.localeCompare(b));
397
+ const boundaryStartAbs = path.resolve(rootAbs, targetPaths[0] ?? '.');
398
+ const boundary = findNearestRepoBoundary({ rootAbs, targetPathOrDirAbs: boundaryStartAbs });
399
+ baseReport.boundary = {
400
+ kind: boundary.kind,
401
+ boundaryFileRel: boundary.boundaryFileRel,
402
+ packageRootAbs: boundary.packageRootAbs,
403
+ packageRootRel: toPosix(path.relative(rootAbs, boundary.packageRootAbs)),
404
+ };
405
+ // Load index files for convention detection and idempotency.
406
+ const indexData = JSON.parse(fs.readFileSync(indexPathAbs, 'utf8'));
407
+ const indexFiles = Array.isArray(indexData.files) ? indexData.files : [];
408
+ // Determine which test files we would create (for deterministic branch naming).
409
+ const predictedGeneratedFiles = targetPaths
410
+ .filter((p) => {
411
+ const abs = path.resolve(rootAbs, p);
412
+ return fs.existsSync(abs) && !fs.statSync(abs).isDirectory();
413
+ })
414
+ .map((p) => previewGeneratedTestFiles({ targetPath: p, indexFiles }))
415
+ .filter((x) => x.wouldCreate)
416
+ .map((x) => x.testPath)
417
+ .sort((a, b) => a.localeCompare(b));
418
+ // Resolve test command (config -> policy -> package.json scripts).
419
+ const cfgLoaded = loadConfigFile(parsed.args.configPath, {
420
+ cwd: () => rootAbs,
421
+ resolve: (...parts) => path.resolve(...parts),
422
+ existsSync: (p) => fs.existsSync(p),
423
+ readFileSync: (p, enc) => fs.readFileSync(p, enc),
424
+ });
425
+ const cfgValidated = validateConfig(cfgLoaded.data);
426
+ const policy = loadOrInitPolicy({ root: rootAbs }).policy;
427
+ // Read package.json from the boundary root (if present) for script-based inference.
428
+ const pkgPath = path.join(boundary.packageRootAbs, 'package.json');
429
+ const pkg = boundary.kind === 'js' && fs.existsSync(pkgPath)
430
+ ? JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
431
+ : null;
432
+ const decision = inferTestCommandAndRunner({
433
+ rootAbs: boundary.packageRootAbs,
434
+ packageJson: pkg,
435
+ pythonBoundary: boundary.kind === 'py' ? { boundaryFileRel: boundary.boundaryFileRel } : null,
436
+ config: { testCommand: cfgValidated.resolved.test.command },
437
+ policy: { workingTestCommand: policy.workingTestCommand },
438
+ });
439
+ baseReport.runnerInferred = decision.runner;
440
+ baseReport.testCommandSource = decision.source;
441
+ baseReport.testCommandReason = decision.reason;
442
+ baseReport.testCommandUsed = decision.command;
443
+ const invokerRaw = parsed.args.invoker ?? 'tests';
444
+ const invoker = invokerRaw === 'tests' || invokerRaw === 'full' ? invokerRaw : 'unknown';
445
+ const allowCommands = cfgValidated.resolved.llm.allowCommands ?? [];
446
+ const denylistHits = (() => {
447
+ if (!cfgValidated.resolved.llm.enabled)
448
+ return ['llm_deny_disabled'];
449
+ if (allowCommands.length === 0)
450
+ return ['llm_deny_no_allowlist'];
451
+ if (invoker === 'unknown')
452
+ return ['llm_deny_unknown_invoker'];
453
+ if (!allowCommands.includes(invoker))
454
+ return ['llm_deny_invoker_not_allowed'];
455
+ return [];
456
+ })();
457
+ const llmAllowed = cfgValidated.resolved.llm.enabled && denylistHits.length === 0;
458
+ baseReport.llmAccess = {
459
+ invoker,
460
+ allowCommands: [...new Set(allowCommands)].sort((a, b) => a.localeCompare(b)),
461
+ allowed: llmAllowed,
462
+ denylistHits: [...new Set(denylistHits)].sort((a, b) => a.localeCompare(b)),
463
+ };
464
+ // Update decision.llm for audit summary
465
+ const llmReason = llmAllowed ? 'allowed' : denylistHits[0] ?? 'disabled';
466
+ baseReport.decision.llm = { enabled: llmAllowed, reason: llmReason, generatorWarnings: [] };
467
+ let generatorKind = pickGenerator({ generator: cfgValidated.resolved.generator }, policy);
468
+ if (generatorKind === 'llm' && !llmAllowed) {
469
+ baseReport.generatorWarnings = [...new Set([...baseReport.generatorWarnings, ...baseReport.llmAccess.denylistHits])].sort((a, b) => a.localeCompare(b));
470
+ generatorKind = 'smoke';
471
+ }
472
+ const llmProvider = generatorKind === 'llm'
473
+ ? deps?.llmProvider ??
474
+ new HttpProvider({
475
+ baseUrl: cfgValidated.resolved.llm.providerBaseUrl,
476
+ endpointPath: cfgValidated.resolved.llm.endpointPath,
477
+ apiKeyEnvVar: cfgValidated.resolved.llm.apiKeyEnvVar,
478
+ env: process.env,
479
+ timeoutMs: 20_000,
480
+ outputCharCap: 20_000,
481
+ dryRun: baseReport.dryRun,
482
+ logger: { debug: (s) => logger.debug(s) },
483
+ })
484
+ : undefined;
485
+ const generator = createGenerator(generatorKind, {
486
+ indexFiles,
487
+ llm: cfgValidated.resolved.llm,
488
+ dryRun: baseReport.dryRun,
489
+ env: process.env,
490
+ nowMs: () => Date.now(),
491
+ ...(llmProvider ? { llmProvider } : {}),
492
+ });
493
+ baseReport.generatorKind = generatorKind;
494
+ if (!decision.command) {
495
+ const report = {
496
+ ...baseReport,
497
+ targetId: target.id,
498
+ targetPath: targetPaths[0] ?? null,
499
+ generatedFiles: predictedGeneratedFiles,
500
+ warnings: [],
501
+ errors: [
502
+ ...baseReport.errors,
503
+ 'No test command could be resolved (config.test.command, policy.workingTestCommand, or package.json scripts test/test:unit).',
504
+ ],
505
+ };
506
+ writeStableJson(reportAbs, report);
507
+ logger.output(stableStringify(report));
508
+ return 2;
509
+ }
510
+ if (baseReport.inferOnly) {
511
+ const report = {
512
+ ...baseReport,
513
+ decision: { ...baseReport.decision, lane: 'skipped', skipReason: 'infer_only' },
514
+ targetId: target.id,
515
+ targetPath: targetPaths[0] ?? null,
516
+ generatedFiles: predictedGeneratedFiles,
517
+ generatorSummary: 'infer_only',
518
+ generatorWarnings: ['infer_only'],
519
+ attempts: [],
520
+ warnings: ['infer_only'],
521
+ errors: [],
522
+ finalOutcome: null,
523
+ };
524
+ writeStableJson(reportAbs, report);
525
+ logger.output(stableStringify(report));
526
+ return 0;
527
+ }
528
+ const timeoutMs = Math.max(1, Math.trunc(parsed.args.timeoutMs ?? 10 * 60 * 1000));
529
+ const mustPass = baseReport.mustPassConsecutiveRuns;
530
+ let consecutivePasses = 0;
531
+ const attempts = [];
532
+ const warnings = [];
533
+ // Real mode: license gating + create deterministic branch before generating tests.
534
+ let branchToUse = null;
535
+ if (!baseReport.dryRun) {
536
+ const license = process.env.TEST_AGENT_LICENSE ?? cfgValidated.resolved.licenseKey;
537
+ if (!license || license.trim() === '') {
538
+ const gated = {
539
+ ...baseReport,
540
+ targetId: target.id,
541
+ targetPath: targetPaths[0] ?? null,
542
+ generatedFiles: predictedGeneratedFiles,
543
+ testCommandUsed: baseReport.testCommandUsed,
544
+ errors: [...baseReport.errors, 'Missing license: set TEST_AGENT_LICENSE or config licenseKey (required for real mode).'],
545
+ };
546
+ writeStableJson(reportAbs, gated);
547
+ logger.output(stableStringify(gated));
548
+ return 2;
549
+ }
550
+ const branchHash = crypto
551
+ .createHash('sha1')
552
+ .update(predictedGeneratedFiles.join('\n'))
553
+ .digest('hex')
554
+ .slice(0, 8);
555
+ const baseBranch = cfgValidated.resolved.baseBranch;
556
+ const baseRef = parsed.args.baseRef?.trim() || baseBranch;
557
+ const requestedBranch = parsed.args.branch?.trim() || null;
558
+ const baseBranchName = requestedBranch ?? `test-agent/${target.id}/${branchHash}`;
559
+ const git = createGitAdapter({ cwd: rootAbs });
560
+ await git.ensureCleanWorkingTree({ ignoreUntrackedPrefixes: ['.test-agent/'] });
561
+ const branch = baseBranchName;
562
+ await git.checkoutOrCreateBranch({ remote, baseRef, branchName: branch, ignoreUntrackedPrefixes: ['.test-agent/'] });
563
+ branchToUse = branch;
564
+ baseReport.git.branch = branch;
565
+ }
566
+ function isSafeRepoRelPosix(p) {
567
+ const raw = (p ?? '').trim();
568
+ if (!raw)
569
+ return false;
570
+ if (raw.includes('\\'))
571
+ return false;
572
+ if (raw.startsWith('/'))
573
+ return false;
574
+ if (/^[A-Za-z]:/.test(raw))
575
+ return false;
576
+ if (raw.split('/').includes('..'))
577
+ return false;
578
+ return !/[\u0000-\u001F\u007F]/.test(raw);
579
+ }
580
+ function safeAbsFromRepoRelPosix(root, p) {
581
+ if (!isSafeRepoRelPosix(p))
582
+ return null;
583
+ const abs = path.resolve(root, ...p.split('/'));
584
+ const rel = path.relative(root, abs).replace(/\\/g, '/');
585
+ if (rel.startsWith('../') || rel === '..')
586
+ return null;
587
+ return abs;
588
+ }
589
+ function deleteRepoRelFiles(root, pathsPosix) {
590
+ const uniq = [...new Set(pathsPosix)].sort((a, b) => a.localeCompare(b));
591
+ for (const p of uniq) {
592
+ const abs = safeAbsFromRepoRelPosix(root, p);
593
+ if (!abs)
594
+ continue;
595
+ try {
596
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile())
597
+ fs.unlinkSync(abs);
598
+ }
599
+ catch {
600
+ // best-effort cleanup; keep deterministic behavior by ignoring
601
+ }
602
+ }
603
+ }
604
+ async function generatePass(opts) {
605
+ const generated = [];
606
+ const created = [];
607
+ const genWarnings = [];
608
+ const genSummaries = [];
609
+ for (const p of targetPaths) {
610
+ const abs = path.resolve(rootAbs, p);
611
+ if (!fs.existsSync(abs) || fs.statSync(abs).isDirectory())
612
+ continue;
613
+ const out = await generator.generate({
614
+ rootAbs,
615
+ targetId: target.id,
616
+ targetPathPosix: toPosix(p),
617
+ boundary: { kind: boundary.kind, rootPathPosix: toPosix(path.relative(rootAbs, boundary.packageRootAbs)) || null },
618
+ runner: baseReport.runnerInferred,
619
+ testCommand: baseReport.testCommandUsed,
620
+ policy: {
621
+ allowTestabilityEdits: policy.allowTestabilityEdits,
622
+ notes: policy.notes,
623
+ workingTestCommand: policy.workingTestCommand,
624
+ },
625
+ budgets: { maxFiles: cfgValidated.resolved.limits.maxFilesChanged, maxChars: cfgValidated.resolved.limits.maxLinesChanged },
626
+ ...(opts.hints ? { hints: opts.hints } : {}),
627
+ ...(opts.feedback ? { feedback: opts.feedback } : {}),
628
+ });
629
+ genWarnings.push(...out.warnings);
630
+ genSummaries.push(out.summary);
631
+ generated.push(...out.generatedFiles);
632
+ if (out.created)
633
+ created.push(...out.generatedFiles);
634
+ if (out.context)
635
+ baseReport.llmContext = out.context;
636
+ }
637
+ return {
638
+ generatedFiles: [...new Set(generated)].sort((a, b) => a.localeCompare(b)),
639
+ createdFiles: [...new Set(created)].sort((a, b) => a.localeCompare(b)),
640
+ warnings: [...new Set(genWarnings)].sort((a, b) => a.localeCompare(b)),
641
+ summaries: [...new Set(genSummaries)].sort((a, b) => a.localeCompare(b)),
642
+ };
643
+ }
644
+ // First generation pass (no feedback).
645
+ const pass1 = await generatePass({ hints: undefined, feedback: null });
646
+ let generatedFiles = pass1.generatedFiles;
647
+ let createdFilesForCleanup = generator.kind() === 'llm' ? pass1.createdFiles : [];
648
+ warnings.push(...pass1.warnings);
649
+ baseReport.generatorWarnings = [...new Set(pass1.warnings)].sort((a, b) => a.localeCompare(b));
650
+ baseReport.generatorSummary = `createdFiles=${generatedFiles.length}; ${pass1.summaries.join('; ')}`;
651
+ const runTest = deps?.runTestCommand ?? runTestCommand;
652
+ let regeneratedForNextAttempt = false;
653
+ for (let attempt = 1; attempt <= maxIterations; attempt++) {
654
+ const wasRegenerated = regeneratedForNextAttempt;
655
+ regeneratedForNextAttempt = false;
656
+ const r = await runTest({
657
+ command: decision.command,
658
+ cwd: boundary.packageRootAbs,
659
+ timeoutMs,
660
+ });
661
+ baseReport.runnerObserved = r.runner;
662
+ baseReport.runner = r.runner;
663
+ const parsedJest = parseJestOutput(r.rawOutput);
664
+ const fix = r.exitCode === 0
665
+ ? { applied: [], warnings: [] }
666
+ : applyFixers({
667
+ failures: parsedJest.failures,
668
+ ctx: { rootAbs: rootAbs, testFiles: generatedFiles, targetPaths },
669
+ });
670
+ const fixersApplied = fix.applied.sort((a, b) => a.localeCompare(b));
671
+ warnings.push(...fix.warnings);
672
+ attempts.push({
673
+ attempt,
674
+ exitCode: r.exitCode,
675
+ timedOut: r.timedOut,
676
+ durationMs: r.durationMs,
677
+ rawOutput: r.rawOutput,
678
+ failures: parsedJest.failures,
679
+ fixersApplied,
680
+ regenerated: wasRegenerated,
681
+ });
682
+ if (r.exitCode === 0 && !r.timedOut) {
683
+ consecutivePasses++;
684
+ if (consecutivePasses >= mustPass)
685
+ break;
686
+ }
687
+ else {
688
+ consecutivePasses = 0;
689
+ if (r.timedOut)
690
+ break;
691
+ // LLM semantic loop v1: on failure, extract runner hints + feed back redacted output, regenerate, and retry.
692
+ if (generator.kind() === 'llm' && attempt < maxIterations) {
693
+ const extracted = extractRunnerHints({ command: decision.command, rawOutput: r.rawOutput });
694
+ const hints = {
695
+ runner: extracted.runner,
696
+ failingFiles: extracted.hints.failingFiles,
697
+ failingTests: extracted.hints.failingTests,
698
+ };
699
+ const feedback = { attempt, exitCode: r.exitCode ?? 1, rawOutput: r.rawOutput };
700
+ // Allow "add file only" diffs on subsequent attempts by removing previous generated test files first.
701
+ deleteRepoRelFiles(rootAbs, createdFilesForCleanup);
702
+ const next = await generatePass({ hints, feedback });
703
+ generatedFiles = next.generatedFiles;
704
+ createdFilesForCleanup = next.createdFiles;
705
+ warnings.push(...next.warnings);
706
+ baseReport.generatorWarnings = [...new Set([...baseReport.generatorWarnings, ...next.warnings])].sort((a, b) => a.localeCompare(b));
707
+ baseReport.generatorSummary = `createdFiles=${generatedFiles.length}; ${next.summaries.join('; ')}`;
708
+ regeneratedForNextAttempt = true;
709
+ }
710
+ // If no fixers applied, we still continue until maxIterations to be deterministic and bounded.
711
+ }
712
+ }
713
+ const finalAttempt = attempts[attempts.length - 1];
714
+ const passedConsecutive = consecutivePasses >= mustPass;
715
+ const finalOutcome = finalAttempt && finalAttempt.timedOut ? 'timeout' : passedConsecutive ? 'pass' : 'fail';
716
+ const exitCode = finalOutcome === 'pass' ? 0 : 1;
717
+ if (baseReport.runnerInferred !== 'unknown' &&
718
+ baseReport.runnerObserved !== 'unknown' &&
719
+ baseReport.runnerInferred !== baseReport.runnerObserved) {
720
+ warnings.push(`runner_mismatch inferred=${baseReport.runnerInferred} observed=${baseReport.runnerObserved}`);
721
+ }
722
+ // Finalize decision summary for audit trail
723
+ const decisionFinal = {
724
+ ...baseReport.decision,
725
+ caps: {
726
+ maxIterations: { limit: maxIterations, used: attempts.length },
727
+ maxPRsPerRun: baseReport.decision.caps.maxPRsPerRun,
728
+ },
729
+ llm: {
730
+ ...baseReport.decision.llm,
731
+ generatorWarnings: [...new Set(baseReport.generatorWarnings)].sort((a, b) => a.localeCompare(b)),
732
+ },
733
+ };
734
+ const report = {
735
+ ...baseReport,
736
+ decision: decisionFinal,
737
+ targetId: target.id,
738
+ targetPath: targetPaths[0] ?? null,
739
+ generatedFiles,
740
+ testCommandUsed: baseReport.testCommandUsed,
741
+ attempts,
742
+ warnings: [...new Set(warnings)].sort((a, b) => a.localeCompare(b)),
743
+ finalOutcome,
744
+ };
745
+ // Real mode: commit + push (+ optional PR) only on success.
746
+ if (!baseReport.dryRun && finalOutcome === 'pass') {
747
+ const git = createGitAdapter({ cwd: rootAbs });
748
+ // Only commit generated test files (avoid committing .test-agent/report.json etc).
749
+ await git.commitFilesIfChanged(generatedFiles, `test-agent: add tests for ${target.id}`, { ignoreUntrackedPrefixes: ['.test-agent/'] });
750
+ await git.pushBranch(remote, report.git.branch ?? branchToUse ?? '');
751
+ report.git.pushed = true;
752
+ if (baseReport.noPr) {
753
+ report.pr = { created: false, url: null, skippedReason: '--noPr' };
754
+ }
755
+ else {
756
+ const owner = cfgValidated.resolved.github.owner;
757
+ const repo = cfgValidated.resolved.github.repo;
758
+ const tokenEnvVar = cfgValidated.resolved.github.tokenEnvVar;
759
+ if (!owner || !repo || !tokenEnvVar) {
760
+ report.pr = { created: false, url: null, skippedReason: 'missing_github_config' };
761
+ }
762
+ else {
763
+ const token = process.env[tokenEnvVar];
764
+ if (!token) {
765
+ report.pr = { created: false, url: null, skippedReason: `missing_env:${tokenEnvVar}` };
766
+ }
767
+ else {
768
+ const gh = createGithubAdapter({ token });
769
+ const headRef = report.git.branch ?? '';
770
+ const existing = await gh.listOpenPullRequests({ owner, repo, branchPrefix: headRef, max: 50 });
771
+ const existingExact = existing.filter((p) => p.headRef === headRef).sort((a, b) => a.number - b.number)[0] ?? null;
772
+ if (existingExact) {
773
+ await gh.commentOnPullRequest({
774
+ owner,
775
+ repo,
776
+ number: existingExact.number,
777
+ body: `Updated run for target ${target.id}.\n\nReport: ${report.reportPath}`,
778
+ });
779
+ report.pr = { created: false, url: existingExact.url, skippedReason: 'existing_pr' };
780
+ }
781
+ else {
782
+ const pr = await gh.openPullRequest({
783
+ owner,
784
+ repo,
785
+ title: `test-agent: add tests for ${target.id}`,
786
+ body: `Automated test generation/run for target ${target.id}.\n\nReport: ${report.reportPath}`,
787
+ base: cfgValidated.resolved.baseBranch,
788
+ head: headRef,
789
+ });
790
+ report.pr = { created: true, url: pr.url, skippedReason: null };
791
+ }
792
+ }
793
+ }
794
+ }
795
+ }
796
+ // Determinism: generatedFiles and attempt.fixersApplied are sorted; stable JSON writer ensures stable keys.
797
+ writeStableJson(reportAbs, report);
798
+ logger.output(stableStringify(report));
799
+ return exitCode;
800
+ }
801
+ //# sourceMappingURL=tests.js.map