viberadar 0.3.46 β 0.3.48
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/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +441 -360
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +898 -395
- package/package.json +43 -43
package/dist/server/index.js
CHANGED
|
@@ -52,21 +52,22 @@ const WIN = process.platform === 'win32';
|
|
|
52
52
|
* --output-format stream-json gives real-time events (tool calls, writes, etc.)
|
|
53
53
|
* File piping avoids TUI mode in Claude Code v2+.
|
|
54
54
|
*/
|
|
55
|
-
function buildAgentShellCmd(agent, taskFile) {
|
|
55
|
+
function buildAgentShellCmd(agent, taskFile, model) {
|
|
56
56
|
const escaped = taskFile.replace(/\\/g, '\\\\');
|
|
57
|
+
const modelFlag = (agent === 'claude' && model) ? ` --model ${model}` : '';
|
|
57
58
|
if (WIN) {
|
|
58
59
|
if (agent === 'claude')
|
|
59
|
-
return `type "${escaped}" | claude.cmd --print --verbose --output-format stream-json`;
|
|
60
|
+
return `type "${escaped}" | claude.cmd --print --verbose --output-format stream-json${modelFlag}`;
|
|
60
61
|
if (agent === 'codex')
|
|
61
|
-
return `type "${escaped}" | codex.cmd`;
|
|
62
|
+
return `type "${escaped}" | codex.cmd exec - --color never --sandbox workspace-write`;
|
|
62
63
|
}
|
|
63
64
|
else {
|
|
64
65
|
if (agent === 'claude')
|
|
65
|
-
return `claude --print --verbose --output-format stream-json < "${escaped}"`;
|
|
66
|
+
return `claude --print --verbose --output-format stream-json${modelFlag} < "${escaped}"`;
|
|
66
67
|
if (agent === 'codex')
|
|
67
|
-
return `codex < "${escaped}"`;
|
|
68
|
+
return `codex exec - --color never --sandbox workspace-write < "${escaped}"`;
|
|
68
69
|
}
|
|
69
|
-
return `claude --print --verbose --output-format stream-json < "${escaped}"`;
|
|
70
|
+
return `claude --print --verbose --output-format stream-json${modelFlag} < "${escaped}"`;
|
|
70
71
|
}
|
|
71
72
|
/** Parse a Claude Code stream-json event into a human-readable line, or null to skip */
|
|
72
73
|
function parseClaudeEvent(raw) {
|
|
@@ -123,6 +124,84 @@ function fmtTool(name, input = {}) {
|
|
|
123
124
|
default: return `π§ ${name}${fp ? ': ' + fp : ''}`;
|
|
124
125
|
}
|
|
125
126
|
}
|
|
127
|
+
// βββ Test runner after agent ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
128
|
+
const TEST_FILE_RE = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
|
|
129
|
+
function runTestFiles(files, projectRoot) {
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
// Write JSON to a temp file β avoids stdout encoding/regex issues on Windows
|
|
132
|
+
const tmpDir = path.join(projectRoot, '.viberadar');
|
|
133
|
+
const tmpFile = path.join(tmpDir, '_test-results.json');
|
|
134
|
+
try {
|
|
135
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
catch { }
|
|
138
|
+
try {
|
|
139
|
+
fs.unlinkSync(tmpFile);
|
|
140
|
+
}
|
|
141
|
+
catch { }
|
|
142
|
+
const proc = (0, child_process_1.spawn)('npx', ['vitest', 'run', '--reporter=json', `--outputFile=${tmpFile}`, ...files], { cwd: projectRoot, shell: true, stdio: 'pipe' });
|
|
143
|
+
let stdout = '';
|
|
144
|
+
proc.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
145
|
+
proc.on('close', () => {
|
|
146
|
+
try {
|
|
147
|
+
// Prefer the output file (reliable); fall back to stdout
|
|
148
|
+
let jsonStr;
|
|
149
|
+
if (fs.existsSync(tmpFile)) {
|
|
150
|
+
jsonStr = fs.readFileSync(tmpFile, 'utf-8').trim();
|
|
151
|
+
process.stdout.write(` β
vitest outputFile: ${tmpFile} (${jsonStr.length} bytes)\n`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
process.stdout.write(` β οΈ vitest outputFile not found, falling back to stdout (${stdout.length} bytes)\n`);
|
|
155
|
+
const match = stdout.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
|
|
156
|
+
jsonStr = match ? match[0] : stdout.trim();
|
|
157
|
+
}
|
|
158
|
+
const json = JSON.parse(jsonStr);
|
|
159
|
+
process.stdout.write(` π vitest JSON: passed=${json.numPassedTests} failed=${json.numFailedTests} testResults=${(json.testResults ?? []).length}\n`);
|
|
160
|
+
// Extract per-file failure details
|
|
161
|
+
const fileDetails = {};
|
|
162
|
+
for (const tr of (json.testResults ?? [])) {
|
|
163
|
+
const fp = tr.testFilePath ?? '';
|
|
164
|
+
if (!fp)
|
|
165
|
+
continue;
|
|
166
|
+
const assertions = tr.assertionResults ?? tr.testResults ?? [];
|
|
167
|
+
const errors = [];
|
|
168
|
+
for (const ar of assertions) {
|
|
169
|
+
if (ar.status === 'failed') {
|
|
170
|
+
errors.push({
|
|
171
|
+
testName: ar.fullName ?? ar.title ?? 'unknown',
|
|
172
|
+
message: (ar.failureMessages?.[0] ?? ar.errors?.[0]?.message ?? '').split('\n')[0].slice(0, 300),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
fileDetails[fp] = {
|
|
177
|
+
passed: assertions.filter((a) => a.status === 'passed').length,
|
|
178
|
+
failed: errors.length,
|
|
179
|
+
errors,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const failedFiles = Object.entries(fileDetails).filter(([, d]) => d.failed > 0);
|
|
183
|
+
process.stdout.write(` π fileDetails: ${Object.keys(fileDetails).length} files, ${failedFiles.length} with failures\n`);
|
|
184
|
+
if (failedFiles.length > 0) {
|
|
185
|
+
for (const [fp, d] of failedFiles) {
|
|
186
|
+
process.stdout.write(` β ${fp} β ${d.failed} failed\n`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
resolve({
|
|
190
|
+
passed: json.numPassedTests ?? 0,
|
|
191
|
+
failed: json.numFailedTests ?? 0,
|
|
192
|
+
files,
|
|
193
|
+
fileDetails,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
process.stdout.write(` β runTestFiles parse error: ${err.message}\n`);
|
|
198
|
+
resolve({ passed: 0, failed: files.length, files, fileDetails: {}, runError: err.message });
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
proc.on('error', (err) => resolve({ passed: 0, failed: files.length, files, fileDetails: {}, runError: err.message }));
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// βββ E2E plan storage βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
126
205
|
function e2ePlanDir(projectRoot) {
|
|
127
206
|
return path.join(projectRoot, '.viberadar', 'e2e-plans');
|
|
128
207
|
}
|
|
@@ -130,9 +209,11 @@ function e2ePlanPath(projectRoot, featureKey) {
|
|
|
130
209
|
return path.join(e2ePlanDir(projectRoot), `${featureKey}.json`);
|
|
131
210
|
}
|
|
132
211
|
function loadE2ePlan(projectRoot, featureKey) {
|
|
133
|
-
const fp = e2ePlanPath(projectRoot, featureKey);
|
|
134
212
|
try {
|
|
135
|
-
|
|
213
|
+
const p = e2ePlanPath(projectRoot, featureKey);
|
|
214
|
+
if (!fs.existsSync(p))
|
|
215
|
+
return null;
|
|
216
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
136
217
|
}
|
|
137
218
|
catch {
|
|
138
219
|
return null;
|
|
@@ -147,24 +228,19 @@ function saveE2ePlan(projectRoot, plan) {
|
|
|
147
228
|
function e2eScreenshotDir(projectRoot, featureKey) {
|
|
148
229
|
return path.join(projectRoot, '.viberadar', 'e2e-screenshots', featureKey);
|
|
149
230
|
}
|
|
150
|
-
function collectScreenshots(projectRoot, featureKey) {
|
|
151
|
-
const
|
|
152
|
-
const result = new Map();
|
|
153
|
-
if (!fs.existsSync(baseDir))
|
|
154
|
-
return result;
|
|
231
|
+
function collectScreenshots(projectRoot, featureKey, testCaseId) {
|
|
232
|
+
const dir = path.join(e2eScreenshotDir(projectRoot, featureKey), testCaseId);
|
|
155
233
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
234
|
+
if (!fs.existsSync(dir))
|
|
235
|
+
return [];
|
|
236
|
+
return fs.readdirSync(dir)
|
|
237
|
+
.filter(f => /\.(png|jpg|jpeg|webp)$/i.test(f))
|
|
238
|
+
.sort()
|
|
239
|
+
.map(f => `${featureKey}/${testCaseId}/${f}`);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return [];
|
|
165
243
|
}
|
|
166
|
-
catch { }
|
|
167
|
-
return result;
|
|
168
244
|
}
|
|
169
245
|
function hasPlaywright(projectRoot) {
|
|
170
246
|
try {
|
|
@@ -176,96 +252,47 @@ function hasPlaywright(projectRoot) {
|
|
|
176
252
|
return false;
|
|
177
253
|
}
|
|
178
254
|
}
|
|
179
|
-
// βββ
|
|
180
|
-
const TEST_FILE_RE = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
|
|
181
|
-
function runTestFiles(files, projectRoot) {
|
|
182
|
-
return new Promise((resolve) => {
|
|
183
|
-
const proc = (0, child_process_1.spawn)('npx', ['vitest', 'run', '--reporter=json', ...files], { cwd: projectRoot, shell: true, stdio: 'pipe' });
|
|
184
|
-
let stdout = '';
|
|
185
|
-
proc.stdout?.on('data', (d) => stdout += d.toString());
|
|
186
|
-
proc.on('close', () => {
|
|
187
|
-
try {
|
|
188
|
-
// vitest --reporter=json wraps output; find the JSON object
|
|
189
|
-
const match = stdout.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
|
|
190
|
-
const json = JSON.parse(match ? match[0] : stdout);
|
|
191
|
-
// Extract per-file failure details
|
|
192
|
-
const fileDetails = {};
|
|
193
|
-
for (const tr of (json.testResults ?? [])) {
|
|
194
|
-
const fp = tr.testFilePath ?? '';
|
|
195
|
-
const errors = [];
|
|
196
|
-
for (const ar of (tr.assertionResults ?? [])) {
|
|
197
|
-
if (ar.status === 'failed') {
|
|
198
|
-
errors.push({
|
|
199
|
-
testName: ar.fullName ?? ar.title ?? 'unknown',
|
|
200
|
-
message: (ar.failureMessages?.[0] ?? '').split('\n')[0].slice(0, 300),
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
fileDetails[fp] = {
|
|
205
|
-
passed: (tr.assertionResults ?? []).filter((a) => a.status === 'passed').length,
|
|
206
|
-
failed: errors.length,
|
|
207
|
-
errors,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
resolve({
|
|
211
|
-
passed: json.numPassedTests ?? 0,
|
|
212
|
-
failed: json.numFailedTests ?? 0,
|
|
213
|
-
files,
|
|
214
|
-
fileDetails,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
resolve({ passed: 0, failed: 0, files, fileDetails: {} });
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
proc.on('error', () => resolve({ passed: 0, failed: 0, files, fileDetails: {} }));
|
|
222
|
-
});
|
|
223
|
-
}
|
|
255
|
+
// βββ Playwright runner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
224
256
|
function runPlaywrightTests(files, projectRoot) {
|
|
225
257
|
return new Promise((resolve) => {
|
|
226
|
-
const proc = (0, child_process_1.spawn)('npx', ['playwright', 'test',
|
|
258
|
+
const proc = (0, child_process_1.spawn)('npx', ['playwright', 'test', '--reporter=json', ...files], { cwd: projectRoot, shell: true, stdio: 'pipe' });
|
|
227
259
|
let stdout = '';
|
|
228
|
-
proc.stdout?.on('data', (d) => stdout += d.toString());
|
|
260
|
+
proc.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
229
261
|
proc.on('close', () => {
|
|
230
262
|
try {
|
|
231
263
|
const match = stdout.match(/\{[\s\S]*"suites"[\s\S]*\}/);
|
|
232
264
|
const json = JSON.parse(match ? match[0] : stdout);
|
|
233
|
-
const
|
|
265
|
+
const results = {};
|
|
266
|
+
const errors = {};
|
|
234
267
|
let passed = 0, failed = 0;
|
|
235
268
|
const walkSuites = (suites) => {
|
|
236
|
-
for (const suite of suites) {
|
|
237
|
-
for (const spec of
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
title: spec.title || '',
|
|
250
|
-
status,
|
|
251
|
-
error: result.error?.message?.slice(0, 500),
|
|
252
|
-
duration: result.duration || 0,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
269
|
+
for (const suite of suites ?? []) {
|
|
270
|
+
for (const spec of suite.specs ?? []) {
|
|
271
|
+
const ok = spec.tests?.every((t) => t.results?.every((r) => r.status === 'passed'));
|
|
272
|
+
const title = spec.title ?? suite.title ?? 'unknown';
|
|
273
|
+
if (ok) {
|
|
274
|
+
results[title] = 'passed';
|
|
275
|
+
passed++;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
results[title] = 'failed';
|
|
279
|
+
failed++;
|
|
280
|
+
const errMsg = spec.tests?.[0]?.results?.[0]?.error?.message ?? 'Test failed';
|
|
281
|
+
errors[title] = errMsg.split('\n')[0].slice(0, 300);
|
|
255
282
|
}
|
|
256
283
|
}
|
|
257
284
|
if (suite.suites)
|
|
258
285
|
walkSuites(suite.suites);
|
|
259
286
|
}
|
|
260
287
|
};
|
|
261
|
-
walkSuites(json.suites
|
|
262
|
-
resolve({ passed, failed,
|
|
288
|
+
walkSuites(json.suites ?? []);
|
|
289
|
+
resolve({ passed, failed, results, errors });
|
|
263
290
|
}
|
|
264
291
|
catch {
|
|
265
|
-
resolve({ passed: 0, failed: 0,
|
|
292
|
+
resolve({ passed: 0, failed: 0, results: {}, errors: {} });
|
|
266
293
|
}
|
|
267
294
|
});
|
|
268
|
-
proc.on('error', () => resolve({ passed: 0, failed: 0,
|
|
295
|
+
proc.on('error', () => resolve({ passed: 0, failed: 0, results: {}, errors: {} }));
|
|
269
296
|
});
|
|
270
297
|
}
|
|
271
298
|
// βββ Prompt builders ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -371,6 +398,28 @@ function buildFixTestsPrompt(filePath, errors) {
|
|
|
371
398
|
`- ΠΠΎΡΠ»Π΅ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ Π·Π°ΠΏΡΡΡΠΈ ΡΠ΅ΡΡ, ΡΡΠΎΠ±Ρ ΡΠ±Π΅Π΄ΠΈΡΡΡΡ ΡΡΠΎ ΠΎΠ½ ΠΏΡΠΎΡ
ΠΎΠ΄ΠΈΡ`,
|
|
372
399
|
].join('\n');
|
|
373
400
|
}
|
|
401
|
+
function buildFixAllTestsPrompt(failedFiles, testType) {
|
|
402
|
+
const totalErrors = failedFiles.reduce((sum, f) => sum + f.errors.length, 0);
|
|
403
|
+
const fileBlocks = failedFiles.map(f => {
|
|
404
|
+
const normalPath = f.filePath.replace(/\\/g, '/');
|
|
405
|
+
return [
|
|
406
|
+
`### \`${normalPath}\` (${f.errors.length} ΡΠΏΠ°Π»ΠΎ):`,
|
|
407
|
+
...f.errors.map(e => `β’ "${e.testName}"\n ${e.message}`),
|
|
408
|
+
].join('\n');
|
|
409
|
+
});
|
|
410
|
+
return [
|
|
411
|
+
`ΠΡΠΏΡΠ°Π²Ρ Π²ΡΠ΅ ΠΏΠ°Π΄Π°ΡΡΠΈΠ΅ ${testType} ΡΠ΅ΡΡΡ (${totalErrors} ΡΠ΅ΡΡΠΎΠ² Π² ${failedFiles.length} ΡΠ°ΠΉΠ»Π°Ρ
).`,
|
|
412
|
+
``,
|
|
413
|
+
...fileBlocks,
|
|
414
|
+
``,
|
|
415
|
+
`Π’ΡΠ΅Π±ΠΎΠ²Π°Π½ΠΈΡ:`,
|
|
416
|
+
`- ΠΡΠΏΡΠ°Π²Ρ ΡΠΎΠ»ΡΠΊΠΎ ΠΏΠ°Π΄Π°ΡΡΠΈΠ΅ ΡΠ΅ΡΡΡ, Π½Π΅ ΡΡΠΎΠ³Π°ΠΉ ΠΏΡΠΎΡ
ΠΎΠ΄ΡΡΠΈΠ΅`,
|
|
417
|
+
`- ΠΠ΅ ΡΠ΄Π°Π»ΡΠΉ ΡΠ΅ΡΡΡ β ΠΈΡΠΏΡΠ°Π²Ρ Π»ΠΎΠ³ΠΈΠΊΡ ΠΈΠ»ΠΈ ΠΌΠΎΠΊΠΈ`,
|
|
418
|
+
`- ΠΡΠ»ΠΈ ΡΠ΅ΡΡ ΠΏΡΠΎΠ²Π΅ΡΡΠ΅Ρ Π½Π΅ΡΡΡΠ΅ΡΡΠ²ΡΡΡΠ΅Π΅ ΠΏΠΎΠ²Π΅Π΄Π΅Π½ΠΈΠ΅ β Π°Π΄Π°ΠΏΡΠΈΡΡΠΉ ΠΏΠΎΠ΄ ΡΠ΅Π°Π»ΡΠ½ΠΎΠ΅ ΠΏΠΎΠ²Π΅Π΄Π΅Π½ΠΈΠ΅ ΠΊΠΎΠ΄Π°`,
|
|
419
|
+
`- ΠΡΠ»ΠΈ ΠΎΡΠΈΠ±ΠΊΠ° Π² ΠΈΡΡ
ΠΎΠ΄Π½ΠΎΠΌ ΠΊΠΎΠ΄Π΅, Π° Π½Π΅ Π² ΡΠ΅ΡΡΠ΅ β ΠΈΡΠΏΡΠ°Π²Ρ ΠΊΠΎΠ΄`,
|
|
420
|
+
`- ΠΠΎΡΠ»Π΅ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΡΠ°ΠΉΠ»Π° Π·Π°ΠΏΡΡΡΠΈ Π΅Π³ΠΎ ΡΠ΅ΡΡΡ, ΡΡΠΎΠ±Ρ ΡΠ±Π΅Π΄ΠΈΡΡΡΡ ΡΡΠΎ ΠΏΡΠΎΡ
ΠΎΠ΄ΡΡ`,
|
|
421
|
+
].join('\n');
|
|
422
|
+
}
|
|
374
423
|
function buildMapUnmappedPrompt(modules, features) {
|
|
375
424
|
const unmapped = modules
|
|
376
425
|
.filter(m => m.type !== 'test' && !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0))
|
|
@@ -392,92 +441,64 @@ function buildMapUnmappedPrompt(modules, features) {
|
|
|
392
441
|
...unmapped,
|
|
393
442
|
].join('\n');
|
|
394
443
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const srcFiles = modules
|
|
444
|
+
function buildE2ePlanPrompt(feat, modules) {
|
|
445
|
+
const files = modules
|
|
398
446
|
.filter(m => m.featureKeys.includes(feat.key) && m.type !== 'test' && !m.isInfra)
|
|
399
447
|
.map(m => '- ' + m.relativePath.replace(/\\/g, '/'));
|
|
400
|
-
const pageFiles = modules
|
|
401
|
-
.filter(m => m.featureKeys.includes(feat.key) &&
|
|
402
|
-
(/page\./i.test(m.name) || /route/i.test(m.name) || /\/(pages|app)\//i.test(m.relativePath)))
|
|
403
|
-
.map(m => '- ' + m.relativePath.replace(/\\/g, '/'));
|
|
404
|
-
const pwConfigExists = fs.existsSync(path.join(projectRoot, 'playwright.config.ts')) ||
|
|
405
|
-
fs.existsSync(path.join(projectRoot, 'playwright.config.js'));
|
|
406
448
|
return [
|
|
407
|
-
|
|
408
|
-
``,
|
|
409
|
-
`ΠΡΠΎΡΠΈΡΠ°ΠΉ ΠΈΡΡ
ΠΎΠ΄Π½ΡΠΉ ΠΊΠΎΠ΄ ΡΠΈΡΠΈ ΡΡΠΎΠ±Ρ ΠΏΠΎΠ½ΡΡΡ UI: ΡΠΎΡΠΌΡ, ΠΊΠ½ΠΎΠΏΠΊΠΈ, Π½Π°Π²ΠΈΠ³Π°ΡΠΈΡ, ΡΠΎΡΡΠΎΡΠ½ΠΈΡ, API-Π²ΡΠ·ΠΎΠ²Ρ.`,
|
|
410
|
-
``,
|
|
411
|
-
`ΠΡΡ
ΠΎΠ΄Π½ΡΠ΅ ΡΠ°ΠΉΠ»Ρ ΡΠΈΡΠΈ (${srcFiles.length}):`,
|
|
412
|
-
...srcFiles,
|
|
449
|
+
`Π’Ρ β QA-ΠΈΠ½ΠΆΠ΅Π½Π΅Ρ. ΠΡΠΎΠ°Π½Π°Π»ΠΈΠ·ΠΈΡΡΠΉ ΠΈΡΡ
ΠΎΠ΄Π½ΡΠΉ ΠΊΠΎΠ΄ ΡΠΈΡΠΈ "${feat.label}" ΠΈ ΡΠΎΡΡΠ°Π²Ρ ΠΏΠΎΠ΄ΡΠΎΠ±Π½ΡΠΉ ΠΏΠ»Π°Π½ E2E-ΡΠ΅ΡΡΠΈΡΠΎΠ²Π°Π½ΠΈΡ.`,
|
|
413
450
|
``,
|
|
414
|
-
|
|
451
|
+
`Π€Π°ΠΉΠ»Ρ ΡΠΈΡΠΈ:`,
|
|
452
|
+
...files,
|
|
415
453
|
``,
|
|
416
|
-
|
|
417
|
-
`- ΠΠ°ΠΆΠ΄ΡΠΉ ΡΠ΅ΡΡ-ΠΊΠ΅ΠΉΡ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΏΡΠΎΠ²Π΅ΡΡΡΡ ΠΎΠ΄ΠΈΠ½ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»ΡΡΠΊΠΈΠΉ ΡΡΠ΅Π½Π°ΡΠΈΠΉ`,
|
|
418
|
-
`- ΠΠΊΠ»ΡΡΠΈ happy path, edge cases, ΠΎΡΠΈΠ±ΠΊΠΈ Π²Π°Π»ΠΈΠ΄Π°ΡΠΈΠΈ`,
|
|
419
|
-
`- Steps Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΊΠΎΠ½ΠΊΡΠ΅ΡΠ½ΡΠΌΠΈ: ΡΠΊΠ°ΠΆΠΈ ΡΠ΅Π»Π΅ΠΊΡΠΎΡΡ, URL, ΡΠ΅ΠΊΡΡ ΠΊΠ½ΠΎΠΏΠΎΠΊ ΠΈΠ· ΡΠ΅Π°Π»ΡΠ½ΠΎΠ³ΠΎ ΠΊΠΎΠ΄Π°`,
|
|
420
|
-
`- id ΡΠ΅ΡΡ-ΠΊΠ΅ΠΉΡΠ°: ${feat.key}-<scenario>-NN (Π½Π°ΠΏΡΠΈΠΌΠ΅Ρ: ${feat.key}-login-01)`,
|
|
454
|
+
`ΠΡΠΎΡΠΈΡΠ°ΠΉ ΡΡΠΈ ΡΠ°ΠΉΠ»Ρ, ΠΈΠ·ΡΡΠΈ UI: ΡΠΎΡΠΌΡ, ΠΊΠ½ΠΎΠΏΠΊΠΈ, Π½Π°Π²ΠΈΠ³Π°ΡΠΈΡ, Π±ΠΈΠ·Π½Π΅Ρ-Π»ΠΎΠ³ΠΈΠΊΡ.`,
|
|
421
455
|
``,
|
|
422
|
-
|
|
456
|
+
`ΠΠ΅ΡΠ½ΠΈ Π’ΠΠΠ¬ΠΠ Π²Π°Π»ΠΈΠ΄Π½ΡΠΉ JSON Π±Π΅Π· markdown-Π±Π»ΠΎΠΊΠΎΠ² Π² ΡΠΎΡΠΌΠ°ΡΠ΅:`,
|
|
423
457
|
`{`,
|
|
424
458
|
` "baseUrl": "http://localhost:3000",`,
|
|
425
459
|
` "testCases": [`,
|
|
426
460
|
` {`,
|
|
427
|
-
` "id": "
|
|
428
|
-
` "name": "
|
|
429
|
-
` "description": "Π§ΡΠΎ ΠΏΡΠΎΠ²Π΅ΡΡΠ΅Ρ
|
|
430
|
-
` "steps": [
|
|
431
|
-
`
|
|
432
|
-
` "Fill [data-testid=email] with test@example.com",`,
|
|
433
|
-
` "Click button:has-text('Submit')"`,
|
|
434
|
-
` ],`,
|
|
435
|
-
` "expectedResults": [`,
|
|
436
|
-
` "URL changes to /dashboard",`,
|
|
437
|
-
` "Text 'Welcome' is visible"`,
|
|
438
|
-
` ]`,
|
|
461
|
+
` "id": "feature-key-01",`,
|
|
462
|
+
` "name": "ΠΡΠ°ΡΠΊΠΎΠ΅ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΡΠ΅ΡΡΠ°",`,
|
|
463
|
+
` "description": "Π§ΡΠΎ ΠΏΡΠΎΠ²Π΅ΡΡΠ΅Ρ ΡΠ΅ΡΡ",`,
|
|
464
|
+
` "steps": ["Π¨Π°Π³ 1", "Π¨Π°Π³ 2", "Π¨Π°Π³ 3"],`,
|
|
465
|
+
` "expectedResults": ["ΠΠΆΠΈΠ΄Π°Π΅ΠΌΡΠΉ ΡΠ΅Π·ΡΠ»ΡΡΠ°Ρ 1", "ΠΠΆΠΈΠ΄Π°Π΅ΠΌΡΠΉ ΡΠ΅Π·ΡΠ»ΡΡΠ°Ρ 2"]`,
|
|
439
466
|
` }`,
|
|
440
467
|
` ]`,
|
|
441
468
|
`}`,
|
|
442
469
|
``,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
470
|
+
`Π’ΡΠ΅Π±ΠΎΠ²Π°Π½ΠΈΡ:`,
|
|
471
|
+
`- 5-15 ΡΠ΅ΡΡ-ΠΊΠ΅ΠΉΡΠΎΠ², ΠΏΠΎΠΊΡΡΠ²Π°ΡΡΠΈΡ
ΠΎΡΠ½ΠΎΠ²Π½ΡΠ΅ ΡΡΠ΅Π½Π°ΡΠΈΠΈ`,
|
|
472
|
+
`- ΠΠ°ΠΆΠ΄ΡΠΉ ΠΊΠ΅ΠΉΡ: happy path, edge cases, ΠΎΠ±ΡΠ°Π±ΠΎΡΠΊΠ° ΠΎΡΠΈΠ±ΠΎΠΊ`,
|
|
473
|
+
`- id: ΡΡΡΠΎΡΠ½ΡΠ΅ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ ΡΠ΅ΡΠ΅Π· Π΄Π΅ΡΠΈΡ, ΡΠ½ΠΈΠΊΠ°Π»ΡΠ½ΡΠΉ`,
|
|
474
|
+
`- Π’ΠΎΠ»ΡΠΊΠΎ JSON, Π±Π΅Π· ΠΎΠ±ΡΡΡΠ½Π΅Π½ΠΈΠΉ`,
|
|
475
|
+
].join('\n');
|
|
446
476
|
}
|
|
447
477
|
function buildWriteE2eTestPrompt(feat, plan, modules) {
|
|
448
478
|
const approvedCases = plan.testCases.filter(tc => tc.status === 'approved');
|
|
449
479
|
const existingE2e = modules
|
|
450
|
-
.filter(m => m.
|
|
480
|
+
.filter(m => m.featureKeys.includes(feat.key) && m.type === 'e2e')
|
|
451
481
|
.map(m => '- ' + m.relativePath.replace(/\\/g, '/'));
|
|
452
482
|
return [
|
|
453
|
-
`ΠΠ°ΠΏΠΈΡΠΈ Playwright E2E
|
|
454
|
-
`Base URL: ${plan.baseUrl || 'http://localhost:3000'}`,
|
|
483
|
+
`ΠΠ°ΠΏΠΈΡΠΈ Playwright E2E ΡΠ΅ΡΡΡ Π΄Π»Ρ ΡΠΈΡΠΈ "${feat.label}".`,
|
|
455
484
|
``,
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
...tc.steps.map((s, j) => ` ${j + 1}. ${s}`),
|
|
463
|
-
`ΠΠΆΠΈΠ΄Π°Π΅ΠΌΡΠΉ ΡΠ΅Π·ΡΠ»ΡΡΠ°Ρ:`,
|
|
464
|
-
...tc.expectedResults.map(r => ` - ${r}`),
|
|
465
|
-
``,
|
|
485
|
+
`Approved ΡΠ΅ΡΡ-ΠΊΠ΅ΠΉΡΡ (${approvedCases.length}):`,
|
|
486
|
+
...approvedCases.map(tc => [
|
|
487
|
+
`### ${tc.name} (id: ${tc.id})`,
|
|
488
|
+
`${tc.description}`,
|
|
489
|
+
`Π¨Π°Π³ΠΈ: ${tc.steps.join(' β ')}`,
|
|
490
|
+
`ΠΠΆΠΈΠ΄Π°Π΅ΡΡΡ: ${tc.expectedResults.join('; ')}`,
|
|
466
491
|
].join('\n')),
|
|
467
492
|
``,
|
|
468
|
-
existingE2e.length > 0
|
|
469
|
-
? `Π‘ΡΡΠ΅ΡΡΠ²ΡΡΡΠΈΠ΅ E2E ΡΠ΅ΡΡΡ (ΡΠ»Π΅Π΄ΡΠΉ ΡΡΠΈΠΌ ΠΏΠ°ΡΡΠ΅ΡΠ½Π°ΠΌ):\n${existingE2e.join('\n')}`
|
|
470
|
-
: 'Π‘ΡΡΠ΅ΡΡΠ²ΡΡΡΠΈΡ
E2E ΡΠ΅ΡΡΠΎΠ² ΠΏΠΎΠΊΠ° Π½Π΅Ρ.',
|
|
493
|
+
existingE2e.length > 0 ? `Π‘ΡΡΠ΅ΡΡΠ²ΡΡΡΠΈΠ΅ E2E ΡΠ΅ΡΡΡ (ΡΠ»Π΅Π΄ΡΠΉ ΠΏΠ°ΡΡΠ΅ΡΠ½Π°ΠΌ):\n${existingE2e.join('\n')}` : '',
|
|
471
494
|
``,
|
|
472
495
|
`Π’ΡΠ΅Π±ΠΎΠ²Π°Π½ΠΈΡ:`,
|
|
473
|
-
`-
|
|
474
|
-
`-
|
|
475
|
-
`-
|
|
476
|
-
`-
|
|
477
|
-
|
|
478
|
-
`-
|
|
479
|
-
`- ΠΠΎΠ±Π°Π²Ρ page.waitForLoadState() ΠΈ Π΄ΡΡΠ³ΠΈΠ΅ ΠΎΠΆΠΈΠ΄Π°Π½ΠΈΡ`,
|
|
480
|
-
`- ΠΠ΅ ΠΈΠ·ΠΌΠ΅Π½ΡΠΉ ΡΡΡΠ΅ΡΡΠ²ΡΡΡΠΈΠ΅ ΡΠ΅ΡΡΡ`,
|
|
496
|
+
`- Π‘ΠΎΠ·Π΄Π°ΠΉ ΡΠ°ΠΉΠ»Ρ Π² Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ e2e/${feat.key}/`,
|
|
497
|
+
`- ΠΡΠΏΠΎΠ»ΡΠ·ΡΠΉ @playwright/test`,
|
|
498
|
+
`- baseUrl: ${plan.baseUrl || 'http://localhost:3000'}`,
|
|
499
|
+
`- ΠΠΎΡΠ»Π΅ ΠΊΠ»ΡΡΠ΅Π²ΡΡ
ΡΠ°Π³ΠΎΠ² Π΄ΠΎΠ±Π°Π²Ρ ΡΠΊΡΠΈΠ½ΡΠΎΡΡ: await page.screenshot({ path: '.viberadar/e2e-screenshots/${feat.key}/<testCaseId>/step-N.png' })`,
|
|
500
|
+
`- ΠΡΡΠΏΠΏΠΈΡΡΠΉ ΠΏΠΎ test case id (ΠΎΠ΄ΠΈΠ½ ΡΠ°ΠΉΠ» Π½Π° ΠΊΠ΅ΠΉΡ ΠΈΠ»ΠΈ Π½Π΅ΡΠΊΠΎΠ»ΡΠΊΠΎ ΠΊΠ΅ΠΉΡΠΎΠ² Π² ΠΎΠ΄Π½ΠΎΠΌ ΡΠ°ΠΉΠ»Π΅)`,
|
|
501
|
+
`- Π‘Π»Π΅Π΄ΡΠΉ ΡΡΡΠ΅ΡΡΠ²ΡΡΡΠΈΠΌ ΠΏΠ°ΡΡΠ΅ΡΠ½Π°ΠΌ ΠΏΡΠΎΠ΅ΠΊΡΠ°`,
|
|
481
502
|
].filter(Boolean).join('\n');
|
|
482
503
|
}
|
|
483
504
|
// βββ Coverage provider auto-install ββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -546,6 +567,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
546
567
|
let coverageError = false;
|
|
547
568
|
let agentRunning = false;
|
|
548
569
|
let testsRunning = false;
|
|
570
|
+
const agentQueue = [];
|
|
549
571
|
// Keyed by absolute file path β per-file failure details from last test run
|
|
550
572
|
const lastTestResults = new Map();
|
|
551
573
|
// ββ SSE clients ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -644,95 +666,88 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
644
666
|
});
|
|
645
667
|
}
|
|
646
668
|
// ββ Agent runner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
669
|
+
/** Execute next queued item, or broadcast agent-done if queue is empty */
|
|
670
|
+
function processNextInQueue() {
|
|
671
|
+
if (agentQueue.length > 0) {
|
|
672
|
+
const next = agentQueue.shift();
|
|
673
|
+
process.stdout.write(` π Starting next from queue: "${next.title}" (remaining: ${agentQueue.length})\n`);
|
|
674
|
+
broadcast('agent-output', { line: `π Π‘Π»Π΅Π΄ΡΡΡΠ°Ρ Π·Π°Π΄Π°ΡΠ° ΠΈΠ· ΠΎΡΠ΅ΡΠ΅Π΄ΠΈ: ${next.title}` });
|
|
675
|
+
broadcast('agent-output', { line: ` Π ΠΎΡΠ΅ΡΠ΅Π΄ΠΈ ΠΎΡΡΠ°Π»ΠΎΡΡ: ${agentQueue.length}` });
|
|
676
|
+
executeAgentItem(next);
|
|
651
677
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
broadcast('agent-error', { message: 'ΠΠ³Π΅Π½Ρ Π½Π΅ Π²ΡΠ±ΡΠ°Π½. Π£ΠΊΠ°ΠΆΠΈ agent Π² viberadar.config.json' });
|
|
655
|
-
return;
|
|
678
|
+
else {
|
|
679
|
+
broadcast('agent-done', { queueLength: 0 });
|
|
656
680
|
}
|
|
657
|
-
|
|
681
|
+
}
|
|
682
|
+
/** Actually spawn the agent process for a queue item */
|
|
683
|
+
function executeAgentItem(item) {
|
|
684
|
+
const { task, featureKey, filePath, title, agent, savedErrors, savedFailedFiles, savedTestType } = item;
|
|
685
|
+
// Build prompt lazily at execution time
|
|
658
686
|
let prompt;
|
|
659
|
-
let title;
|
|
660
|
-
const agentLabel = agent === 'claude' ? 'Claude Code' : 'Codex';
|
|
661
687
|
if (task === 'write-tests') {
|
|
662
688
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
663
689
|
if (!feat) {
|
|
664
690
|
broadcast('agent-error', { message: `Π€ΠΈΡΠ° Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π°: ${featureKey}` });
|
|
691
|
+
agentRunning = false;
|
|
692
|
+
processNextInQueue();
|
|
665
693
|
return;
|
|
666
694
|
}
|
|
667
695
|
prompt = buildWriteTestsPrompt(feat, currentData.modules, currentData.testRunner || 'vitest');
|
|
668
|
-
title = `${agentLabel} β ΡΠ΅ΡΡΡ Π΄Π»Ρ "${feat.label}"`;
|
|
669
696
|
}
|
|
670
697
|
else if (task === 'write-tests-file') {
|
|
671
698
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
672
699
|
if (!feat || !filePath) {
|
|
673
|
-
broadcast('agent-error', { message:
|
|
700
|
+
broadcast('agent-error', { message: 'ΠΠ΅ ΡΠΊΠ°Π·Π°Π½Π° ΡΠΈΡΠ° ΠΈΠ»ΠΈ ΡΠ°ΠΉΠ»' });
|
|
701
|
+
agentRunning = false;
|
|
702
|
+
processNextInQueue();
|
|
674
703
|
return;
|
|
675
704
|
}
|
|
676
705
|
prompt = buildWriteTestsForFilePrompt(filePath, feat, currentData.modules, currentData.testRunner || 'vitest');
|
|
677
|
-
const fileName = filePath.replace(/\\/g, '/').split('/').pop() || filePath;
|
|
678
|
-
title = `${agentLabel} β ΡΠ΅ΡΡ Π΄Π»Ρ "${fileName}"`;
|
|
679
706
|
}
|
|
680
707
|
else if (task === 'fix-tests') {
|
|
681
|
-
if (!filePath) {
|
|
682
|
-
broadcast('agent-error', { message:
|
|
708
|
+
if (!filePath || !savedErrors || savedErrors.length === 0) {
|
|
709
|
+
broadcast('agent-error', { message: `ΠΠ΅Ρ ΡΠΎΡ
ΡΠ°Π½ΡΠ½Π½ΡΡ
ΠΎΡΠΈΠ±ΠΎΠΊ Π΄Π»Ρ ${filePath}` });
|
|
710
|
+
agentRunning = false;
|
|
711
|
+
processNextInQueue();
|
|
683
712
|
return;
|
|
684
713
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
if (!storedErrors || storedErrors.length === 0) {
|
|
695
|
-
broadcast('agent-error', { message: `ΠΠ΅Ρ ΡΠΎΡ
ΡΠ°Π½ΡΠ½Π½ΡΡ
ΠΎΡΠΈΠ±ΠΎΠΊ Π΄Π»Ρ ${filePath}. Π‘Π½Π°ΡΠ°Π»Π° Π·Π°ΠΏΡΡΡΠΈ ΡΠ΅ΡΡΡ.` });
|
|
714
|
+
prompt = buildFixTestsPrompt(filePath, savedErrors);
|
|
715
|
+
}
|
|
716
|
+
else if (task === 'fix-tests-all') {
|
|
717
|
+
if (!savedFailedFiles || savedFailedFiles.length === 0) {
|
|
718
|
+
broadcast('agent-error', { message: 'ΠΠ΅Ρ ΡΠΏΠ°Π²ΡΠΈΡ
ΡΠ΅ΡΡΠΎΠ² Π΄Π»Ρ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ' });
|
|
719
|
+
agentRunning = false;
|
|
720
|
+
processNextInQueue();
|
|
696
721
|
return;
|
|
697
722
|
}
|
|
698
|
-
prompt =
|
|
699
|
-
const fileName = filePath.replace(/\\/g, '/').split('/').pop() || filePath;
|
|
700
|
-
title = `${agentLabel} β ΠΈΡΠΏΡΠ°Π²ΠΈΡΡ ΡΠ΅ΡΡΡ Π² "${fileName}"`;
|
|
723
|
+
prompt = buildFixAllTestsPrompt(savedFailedFiles, savedTestType || 'unit');
|
|
701
724
|
}
|
|
702
725
|
else if (task === 'generate-e2e-plan') {
|
|
703
726
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
704
727
|
if (!feat) {
|
|
705
728
|
broadcast('agent-error', { message: `Π€ΠΈΡΠ° Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π°: ${featureKey}` });
|
|
729
|
+
agentRunning = false;
|
|
730
|
+
processNextInQueue();
|
|
706
731
|
return;
|
|
707
732
|
}
|
|
708
|
-
prompt = buildE2ePlanPrompt(feat, currentData.modules
|
|
709
|
-
title = `${agentLabel} β E2E ΠΏΠ»Π°Π½ Π΄Π»Ρ "${feat.label}"`;
|
|
733
|
+
prompt = buildE2ePlanPrompt(feat, currentData.modules);
|
|
710
734
|
}
|
|
711
735
|
else if (task === 'write-e2e-tests') {
|
|
712
736
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if (!plan) {
|
|
719
|
-
broadcast('agent-error', { message: `E2E ΠΏΠ»Π°Π½ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ Π΄Π»Ρ "${feat.label}". Π‘Π½Π°ΡΠ°Π»Π° ΡΠ³Π΅Π½Π΅ΡΠΈΡΡΠΉ ΠΏΠ»Π°Π½.` });
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
const approved = plan.testCases.filter(tc => tc.status === 'approved');
|
|
723
|
-
if (approved.length === 0) {
|
|
724
|
-
broadcast('agent-error', { message: `ΠΠ΅Ρ ΠΎΠ΄ΠΎΠ±ΡΠ΅Π½Π½ΡΡ
ΡΠ΅ΡΡ-ΠΊΠ΅ΠΉΡΠΎΠ² Π² ΠΏΠ»Π°Π½Π΅ "${feat.label}"` });
|
|
737
|
+
const plan = featureKey ? loadE2ePlan(projectRoot, featureKey) : null;
|
|
738
|
+
if (!feat || !plan) {
|
|
739
|
+
broadcast('agent-error', { message: `Π€ΠΈΡΠ° ΠΈΠ»ΠΈ ΠΏΠ»Π°Π½ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ: ${featureKey}` });
|
|
740
|
+
agentRunning = false;
|
|
741
|
+
processNextInQueue();
|
|
725
742
|
return;
|
|
726
743
|
}
|
|
727
744
|
prompt = buildWriteE2eTestPrompt(feat, plan, currentData.modules);
|
|
728
|
-
title = `${agentLabel} β E2E ΡΠ΅ΡΡΡ Π΄Π»Ρ "${feat.label}" (${approved.length} ΠΊΠ΅ΠΉΡΠΎΠ²)`;
|
|
729
745
|
}
|
|
730
746
|
else {
|
|
731
747
|
prompt = buildMapUnmappedPrompt(currentData.modules, currentData.features || []);
|
|
732
|
-
title = `${agentLabel} β ΡΠ°Π·ΠΎΠ±ΡΠ°ΡΡ unmapped`;
|
|
733
748
|
}
|
|
734
749
|
agentRunning = true;
|
|
735
|
-
broadcast('agent-started', { title, task, featureKey });
|
|
750
|
+
broadcast('agent-started', { title, task, featureKey, filePath: filePath || null, queueLength: agentQueue.length });
|
|
736
751
|
process.stdout.write(` π€ Running agent (${agent}): ${task}\n`);
|
|
737
752
|
// Write prompt to .viberadar/task.md for reference
|
|
738
753
|
const taskDir = path.join(projectRoot, '.viberadar');
|
|
@@ -743,7 +758,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
743
758
|
}
|
|
744
759
|
catch { }
|
|
745
760
|
// Spawn via shell, piping prompt from file (avoids TUI mode, supports stream-json)
|
|
746
|
-
const shellCmd = buildAgentShellCmd(agent, taskFile);
|
|
761
|
+
const shellCmd = buildAgentShellCmd(agent, taskFile, currentData.model);
|
|
747
762
|
process.stdout.write(` π Shell cmd: ${shellCmd}\n`);
|
|
748
763
|
const proc = (0, child_process_1.spawn)(shellCmd, [], {
|
|
749
764
|
cwd: projectRoot,
|
|
@@ -754,7 +769,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
754
769
|
broadcast('agent-output', { line: `π ΠΠ°Π΄Π°ΡΠ° Π·Π°ΠΏΠΈΡΠ°Π½Π° Π² .viberadar/task.md` });
|
|
755
770
|
// Track test files written/edited by agent (for auto-run after)
|
|
756
771
|
const createdTestFiles = [];
|
|
757
|
-
// Accumulate full result text
|
|
772
|
+
// Accumulate full result text for E2E plan parsing
|
|
758
773
|
let agentResultText = '';
|
|
759
774
|
function trackWrittenFiles(raw) {
|
|
760
775
|
try {
|
|
@@ -780,17 +795,14 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
780
795
|
return;
|
|
781
796
|
trackWrittenFiles(raw);
|
|
782
797
|
const parsed = agent === 'claude' ? parseClaudeEvent(raw) : raw;
|
|
783
|
-
// If parsing returned null (noise), still show raw line dimmed so user knows output is coming
|
|
784
798
|
if (!parsed) {
|
|
785
799
|
broadcast('agent-output', { line: raw.slice(0, 120), isDim: true });
|
|
786
800
|
return;
|
|
787
801
|
}
|
|
788
802
|
if (parsed.startsWith('Β§RESULTΒ§')) {
|
|
789
|
-
|
|
790
|
-
agentResultText += resultBody + '\n';
|
|
791
|
-
// Full result summary β split into lines and prefix with indent
|
|
803
|
+
agentResultText = parsed.slice('Β§RESULTΒ§'.length).trim();
|
|
792
804
|
broadcast('agent-output', { line: 'βββββββββββββββββββββββββββββ' });
|
|
793
|
-
for (const l of
|
|
805
|
+
for (const l of agentResultText.split('\n')) {
|
|
794
806
|
if (l.trim())
|
|
795
807
|
broadcast('agent-output', { line: ' ' + l });
|
|
796
808
|
}
|
|
@@ -813,21 +825,32 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
813
825
|
agentRunning = false;
|
|
814
826
|
if (code === 0) {
|
|
815
827
|
// Auto-run created/fixed test files and show results
|
|
816
|
-
|
|
817
|
-
if (
|
|
828
|
+
let testFilesToRun;
|
|
829
|
+
if (task === 'fix-tests-all') {
|
|
830
|
+
testFilesToRun = [...lastTestResults.keys()];
|
|
831
|
+
}
|
|
832
|
+
else if (task === 'fix-tests' && filePath) {
|
|
833
|
+
testFilesToRun = [filePath];
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
testFilesToRun = createdTestFiles;
|
|
837
|
+
}
|
|
838
|
+
if ((task === 'write-tests' || task === 'write-tests-file' || task === 'fix-tests' || task === 'fix-tests-all') && testFilesToRun.length > 0) {
|
|
818
839
|
broadcast('agent-output', { line: 'βββββββββββββββββββββββββββββ' });
|
|
819
840
|
broadcast('agent-output', { line: `π§ͺ ΠΠ°ΠΏΡΡΠΊΠ°Ρ ΡΠ΅ΡΡΡ (${testFilesToRun.length} ΡΠ°ΠΉΠ»ΠΎΠ²)...` });
|
|
820
841
|
const result = await runTestFiles(testFilesToRun, projectRoot);
|
|
821
|
-
// Store per-file results for "fix-tests" feature
|
|
822
|
-
// path.resolve() normalizes slashes on Windows (vitest outputs forward slashes)
|
|
823
842
|
lastTestResults.clear();
|
|
824
843
|
for (const [fp, detail] of Object.entries(result.fileDetails)) {
|
|
825
844
|
if (detail.failed > 0)
|
|
826
845
|
lastTestResults.set(path.resolve(fp), { failed: detail.failed, errors: detail.errors });
|
|
827
846
|
}
|
|
828
|
-
const summary = result.
|
|
829
|
-
?
|
|
830
|
-
:
|
|
847
|
+
const summary = result.runError
|
|
848
|
+
? `β Π’Π΅ΡΡΡ Π½Π΅ Π·Π°ΠΏΡΡΡΠΈΠ»ΠΈΡΡ: ${result.runError}`
|
|
849
|
+
: result.failed === 0 && result.passed > 0
|
|
850
|
+
? `β
ΠΡΠ΅ ΡΠ΅ΡΡΡ ΠΏΡΠΎΡΠ»ΠΈ: ${result.passed} passed`
|
|
851
|
+
: result.failed === 0 && result.passed === 0
|
|
852
|
+
? `β οΈ 0 ΡΠ΅ΡΡΠΎΠ² Π·Π°ΠΏΡΡΡΠΈΠ»ΠΎΡΡ β ΠΏΡΠΎΠ²Π΅ΡΡ ΡΠ°ΠΉΠ» Π½Π° ΠΎΡΠΈΠ±ΠΊΠΈ ΠΈΠΌΠΏΠΎΡΡΠ°`
|
|
853
|
+
: `β οΈ ${result.passed} passed, ${result.failed} failed`;
|
|
831
854
|
broadcast('agent-output', { line: summary });
|
|
832
855
|
if (result.failed > 0) {
|
|
833
856
|
for (const [fp, detail] of Object.entries(result.fileDetails)) {
|
|
@@ -843,61 +866,36 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
843
866
|
}
|
|
844
867
|
broadcast('agent-summary', result);
|
|
845
868
|
}
|
|
846
|
-
//
|
|
869
|
+
// E2E plan post-processing
|
|
847
870
|
if (task === 'generate-e2e-plan' && featureKey) {
|
|
848
|
-
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
849
871
|
try {
|
|
850
|
-
// Extract JSON from agent result text
|
|
851
872
|
const jsonMatch = agentResultText.match(/\{[\s\S]*"testCases"[\s\S]*\}/);
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
steps: tc.steps || [],
|
|
865
|
-
expectedResults: tc.expectedResults || [],
|
|
866
|
-
status: 'pending',
|
|
867
|
-
})),
|
|
868
|
-
};
|
|
869
|
-
saveE2ePlan(projectRoot, plan);
|
|
870
|
-
broadcast('e2e-plan-ready', { featureKey, plan: plan });
|
|
871
|
-
broadcast('agent-output', { line: `β
E2E ΠΏΠ»Π°Π½ ΡΠΎΡ
ΡΠ°Π½ΡΠ½: ${plan.testCases.length} ΡΠ΅ΡΡ-ΠΊΠ΅ΠΉΡΠΎΠ²` });
|
|
872
|
-
}
|
|
873
|
-
else {
|
|
874
|
-
broadcast('e2e-plan-error', { featureKey, message: 'ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΠΈΠ·Π²Π»Π΅ΡΡ JSON ΠΈΠ· ΠΎΡΠ²Π΅ΡΠ° Π°Π³Π΅Π½ΡΠ°' });
|
|
875
|
-
broadcast('agent-output', { line: 'β ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΡΠ°ΡΠΏΠ°ΡΡΠΈΡΡ JSON ΠΏΠ»Π°Π½ ΠΈΠ· ΠΎΡΠ²Π΅ΡΠ°', isError: true });
|
|
876
|
-
}
|
|
873
|
+
const parsedPlan = JSON.parse(jsonMatch ? jsonMatch[0] : agentResultText);
|
|
874
|
+
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
875
|
+
const plan = {
|
|
876
|
+
featureKey,
|
|
877
|
+
featureLabel: feat?.label || featureKey,
|
|
878
|
+
generatedAt: new Date().toISOString(),
|
|
879
|
+
updatedAt: new Date().toISOString(),
|
|
880
|
+
baseUrl: parsedPlan.baseUrl,
|
|
881
|
+
testCases: (parsedPlan.testCases || []).map((tc) => ({ ...tc, status: 'pending' })),
|
|
882
|
+
};
|
|
883
|
+
saveE2ePlan(projectRoot, plan);
|
|
884
|
+
broadcast('e2e-plan-ready', { featureKey, plan });
|
|
877
885
|
}
|
|
878
886
|
catch (err) {
|
|
879
|
-
broadcast('e2e-plan-error', { featureKey, message: err.message });
|
|
880
|
-
broadcast('agent-output', { line: `β ΠΡΠΈΠ±ΠΊΠ° ΠΏΠ°ΡΡΠΈΠ½Π³Π° ΠΏΠ»Π°Π½Π°: ${err.message}`, isError: true });
|
|
887
|
+
broadcast('e2e-plan-error', { featureKey, message: `ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΡΠ°ΡΠΏΠ°ΡΡΠΈΡΡ ΠΏΠ»Π°Π½: ${err.message}` });
|
|
881
888
|
}
|
|
882
889
|
}
|
|
883
890
|
if (task === 'write-e2e-tests' && featureKey) {
|
|
884
|
-
// Update plan: mark approved test cases as 'written' and set file paths
|
|
885
891
|
const plan = loadE2ePlan(projectRoot, featureKey);
|
|
886
892
|
if (plan) {
|
|
887
893
|
for (const tc of plan.testCases) {
|
|
888
|
-
if (tc.status === 'approved')
|
|
894
|
+
if (tc.status === 'approved')
|
|
889
895
|
tc.status = 'written';
|
|
890
|
-
// Try to match created test files to test cases
|
|
891
|
-
const matchingFile = createdTestFiles.find(fp => fp.replace(/\\/g, '/').includes(tc.id) ||
|
|
892
|
-
fp.replace(/\\/g, '/').includes(featureKey));
|
|
893
|
-
if (matchingFile) {
|
|
894
|
-
tc.testFilePath = path.relative(projectRoot, matchingFile).replace(/\\/g, '/');
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
896
|
}
|
|
898
897
|
saveE2ePlan(projectRoot, plan);
|
|
899
|
-
broadcast('e2e-
|
|
900
|
-
broadcast('agent-output', { line: `β
E2E ΠΏΠ»Π°Π½ ΠΎΠ±Π½ΠΎΠ²Π»ΡΠ½: ΡΠ΅ΡΡΡ ΠΎΡΠΌΠ΅ΡΠ΅Π½Ρ ΠΊΠ°ΠΊ Π½Π°ΠΏΠΈΡΠ°Π½Π½ΡΠ΅` });
|
|
898
|
+
broadcast('e2e-tests-written', { featureKey, files: createdTestFiles });
|
|
901
899
|
}
|
|
902
900
|
}
|
|
903
901
|
process.stdout.write(' β
Agent done, rescanning...\n');
|
|
@@ -906,19 +904,128 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
906
904
|
broadcast('data-updated');
|
|
907
905
|
}
|
|
908
906
|
catch { }
|
|
909
|
-
|
|
907
|
+
processNextInQueue();
|
|
908
|
+
}
|
|
909
|
+
else if (code === 255) {
|
|
910
|
+
process.stdout.write(` β Agent auth error (exit code 255)\n`);
|
|
911
|
+
broadcast('agent-error', {
|
|
912
|
+
message: `${agent === 'claude' ? 'Claude Code' : 'Codex'} Π½Π΅ Π°Π²ΡΠΎΡΠΈΠ·ΠΎΠ²Π°Π½. ΠΠ°ΠΆΠΌΠΈ π ΠΠ΅ΡΠ΅Π»ΠΎΠ³ΠΈΠ½ΠΈΡΡΡΡ Π² ΠΌΠ΅Π½Ρ Π°Π³Π΅Π½ΡΠ°.`,
|
|
913
|
+
authRequired: true,
|
|
914
|
+
agent,
|
|
915
|
+
});
|
|
916
|
+
processNextInQueue();
|
|
910
917
|
}
|
|
911
918
|
else {
|
|
912
919
|
process.stdout.write(` β Agent failed (exit code ${code})\n`);
|
|
913
920
|
broadcast('agent-error', { message: `ΠΠ³Π΅Π½Ρ Π·Π°Π²Π΅ΡΡΠΈΠ»ΡΡ Ρ ΠΊΠΎΠ΄ΠΎΠΌ ${code}` });
|
|
921
|
+
processNextInQueue();
|
|
914
922
|
}
|
|
915
923
|
});
|
|
916
924
|
proc.on('error', (err) => {
|
|
917
925
|
agentRunning = false;
|
|
926
|
+
const isNotFound = err.code === 'ENOENT' || err.message.includes('ENOENT');
|
|
927
|
+
const agentName = agent === 'claude' ? 'Claude Code' : 'Codex';
|
|
928
|
+
const msg = isNotFound
|
|
929
|
+
? `${agentName} Π½Π΅ ΡΡΡΠ°Π½ΠΎΠ²Π»Π΅Π½. Π‘ΠΊΠ°ΡΠ°ΠΉ Ρ ${agent === 'claude' ? 'claude.ai/download' : 'github.com/openai/codex'}`
|
|
930
|
+
: `ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°ΠΏΡΡΡΠΈΡΡ ${agent}: ${err.message}`;
|
|
918
931
|
process.stdout.write(' β Agent spawn error: ' + err.message + '\n');
|
|
919
|
-
broadcast('agent-error', { message:
|
|
932
|
+
broadcast('agent-error', { message: msg, notInstalled: isNotFound, agent });
|
|
933
|
+
processNextInQueue();
|
|
920
934
|
});
|
|
921
935
|
}
|
|
936
|
+
/** Validate task params and enqueue (prompt is built lazily at execution time) */
|
|
937
|
+
function runAgent(task, featureKey, filePath) {
|
|
938
|
+
const agent = currentData.agent;
|
|
939
|
+
if (!agent) {
|
|
940
|
+
broadcast('agent-error', { message: 'ΠΠ³Π΅Π½Ρ Π½Π΅ Π²ΡΠ±ΡΠ°Π½. Π£ΠΊΠ°ΠΆΠΈ agent Π² viberadar.config.json' });
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const agentLabel = agent === 'claude' ? 'Claude Code' : 'Codex';
|
|
944
|
+
let title;
|
|
945
|
+
let savedErrors;
|
|
946
|
+
let savedFailedFiles;
|
|
947
|
+
let savedTestType;
|
|
948
|
+
if (task === 'write-tests') {
|
|
949
|
+
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
950
|
+
if (!feat) {
|
|
951
|
+
broadcast('agent-error', { message: `Π€ΠΈΡΠ° Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π°: ${featureKey}` });
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
title = `${agentLabel} β ΡΠ΅ΡΡΡ Π΄Π»Ρ "${feat.label}"`;
|
|
955
|
+
}
|
|
956
|
+
else if (task === 'write-tests-file') {
|
|
957
|
+
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
958
|
+
if (!feat || !filePath) {
|
|
959
|
+
broadcast('agent-error', { message: 'ΠΠ΅ ΡΠΊΠ°Π·Π°Π½Π° ΡΠΈΡΠ° ΠΈΠ»ΠΈ ΡΠ°ΠΉΠ»' });
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const fileName = filePath.replace(/\\/g, '/').split('/').pop() || filePath;
|
|
963
|
+
title = `${agentLabel} β ΡΠ΅ΡΡ Π΄Π»Ρ "${fileName}"`;
|
|
964
|
+
}
|
|
965
|
+
else if (task === 'fix-tests') {
|
|
966
|
+
if (!filePath) {
|
|
967
|
+
broadcast('agent-error', { message: 'ΠΠ΅ ΡΠΊΠ°Π·Π°Π½ ΡΠ°ΠΉΠ» Π΄Π»Ρ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ' });
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
for (const [fp, detail] of lastTestResults) {
|
|
971
|
+
const rel = path.relative(projectRoot, fp).replace(/\\/g, '/');
|
|
972
|
+
if (rel === filePath.replace(/\\/g, '/') || fp === filePath) {
|
|
973
|
+
savedErrors = detail.errors;
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (!savedErrors || savedErrors.length === 0) {
|
|
978
|
+
broadcast('agent-error', { message: `ΠΠ΅Ρ ΡΠΎΡ
ΡΠ°Π½ΡΠ½Π½ΡΡ
ΠΎΡΠΈΠ±ΠΎΠΊ Π΄Π»Ρ ${filePath}. Π‘Π½Π°ΡΠ°Π»Π° Π·Π°ΠΏΡΡΡΠΈ ΡΠ΅ΡΡΡ.` });
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const fileName = filePath.replace(/\\/g, '/').split('/').pop() || filePath;
|
|
982
|
+
title = `${agentLabel} β ΠΈΡΠΏΡΠ°Π²ΠΈΡΡ ΡΠ΅ΡΡΡ Π² "${fileName}"`;
|
|
983
|
+
}
|
|
984
|
+
else if (task === 'fix-tests-all') {
|
|
985
|
+
savedTestType = filePath || 'unit'; // filePath param is reused for testType in this case
|
|
986
|
+
savedFailedFiles = [];
|
|
987
|
+
for (const [fp, detail] of lastTestResults) {
|
|
988
|
+
const rel = path.relative(projectRoot, fp).replace(/\\/g, '/');
|
|
989
|
+
const mod = currentData.modules.find(m => m.relativePath.replace(/\\/g, '/') === rel && m.testType === savedTestType);
|
|
990
|
+
if (mod && detail.errors.length > 0) {
|
|
991
|
+
savedFailedFiles.push({ filePath: rel, errors: detail.errors });
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (savedFailedFiles.length === 0) {
|
|
995
|
+
broadcast('agent-error', { message: `ΠΠ΅Ρ ΡΠΏΠ°Π²ΡΠΈΡ
${savedTestType} ΡΠ΅ΡΡΠΎΠ². Π‘Π½Π°ΡΠ°Π»Π° Π·Π°ΠΏΡΡΡΠΈ ΡΠ΅ΡΡΡ.` });
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
title = `${agentLabel} β ΠΏΠΎΡΠΈΠ½ΠΈΡΡ Π²ΡΠ΅ ${savedTestType} ΡΠ΅ΡΡΡ (${savedFailedFiles.length} ΡΠ°ΠΉΠ»ΠΎΠ²)`;
|
|
999
|
+
}
|
|
1000
|
+
else if (task === 'generate-e2e-plan') {
|
|
1001
|
+
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
1002
|
+
if (!feat) {
|
|
1003
|
+
broadcast('agent-error', { message: `Π€ΠΈΡΠ° Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π°: ${featureKey}` });
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
title = `${agentLabel} β E2E ΠΏΠ»Π°Π½ Π΄Π»Ρ "${feat.label}"`;
|
|
1007
|
+
}
|
|
1008
|
+
else if (task === 'write-e2e-tests') {
|
|
1009
|
+
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
1010
|
+
if (!feat) {
|
|
1011
|
+
broadcast('agent-error', { message: `Π€ΠΈΡΠ° Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π°: ${featureKey}` });
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
title = `${agentLabel} β ΠΏΠΈΡΡ E2E ΡΠ΅ΡΡΡ Π΄Π»Ρ "${feat.label}"`;
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
title = `${agentLabel} β ΡΠ°Π·ΠΎΠ±ΡΠ°ΡΡ unmapped`;
|
|
1018
|
+
}
|
|
1019
|
+
const item = { task, featureKey, filePath, title, agent, savedErrors, savedFailedFiles, savedTestType };
|
|
1020
|
+
if (agentRunning) {
|
|
1021
|
+
agentQueue.push(item);
|
|
1022
|
+
const ql = agentQueue.length;
|
|
1023
|
+
process.stdout.write(` π Agent busy, queued: "${title}" (queue size: ${ql})\n`);
|
|
1024
|
+
broadcast('agent-queued', { queueLength: ql, title, task, featureKey: featureKey || null, filePath: filePath || null });
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
executeAgentItem(item);
|
|
1028
|
+
}
|
|
922
1029
|
// ββ Chokidar watcher βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
923
1030
|
chokidar_1.default.watch([
|
|
924
1031
|
'**/*.{ts,tsx,js,jsx,vue,svelte}',
|
|
@@ -951,13 +1058,13 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
951
1058
|
testErrors[rel] = detail;
|
|
952
1059
|
}
|
|
953
1060
|
// Check which features have E2E plans
|
|
954
|
-
const e2ePlansExist =
|
|
955
|
-
const plansDir = e2ePlanDir(projectRoot);
|
|
1061
|
+
const e2ePlansExist = {};
|
|
956
1062
|
try {
|
|
957
|
-
|
|
958
|
-
|
|
1063
|
+
const planDir = e2ePlanDir(projectRoot);
|
|
1064
|
+
if (fs.existsSync(planDir)) {
|
|
1065
|
+
for (const f of fs.readdirSync(planDir)) {
|
|
959
1066
|
if (f.endsWith('.json'))
|
|
960
|
-
e2ePlansExist
|
|
1067
|
+
e2ePlansExist[f.replace('.json', '')] = true;
|
|
961
1068
|
}
|
|
962
1069
|
}
|
|
963
1070
|
}
|
|
@@ -968,7 +1075,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
968
1075
|
}
|
|
969
1076
|
if (url === '/api/status') {
|
|
970
1077
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
971
|
-
res.end(JSON.stringify({ coverageRunning, coverageError, agentRunning }));
|
|
1078
|
+
res.end(JSON.stringify({ coverageRunning, coverageError, agentRunning, queueLength: agentQueue.length }));
|
|
972
1079
|
return;
|
|
973
1080
|
}
|
|
974
1081
|
if (url === '/api/run-coverage' && req.method === 'POST') {
|
|
@@ -1025,7 +1132,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1025
1132
|
}
|
|
1026
1133
|
}
|
|
1027
1134
|
}
|
|
1028
|
-
broadcast('agent-output', { line: ' β ΠΠ°ΠΆΠΌΠΈ
|
|
1135
|
+
broadcast('agent-output', { line: ' β ΠΠ°ΠΆΠΌΠΈ ΠΠΎΡΠΈΠ½ΠΈΡΡ ΡΡΠ΄ΠΎΠΌ Ρ ΡΠ°ΠΉΠ»ΠΎΠΌ ΡΡΠΎΠ±Ρ Π°Π³Π΅Π½Ρ ΠΈΡΠΏΡΠ°Π²ΠΈΠ»' });
|
|
1029
1136
|
}
|
|
1030
1137
|
// Send testErrors directly in event β avoids path/timing issues with /api/data fetch
|
|
1031
1138
|
const testErrorsForClient = {};
|
|
@@ -1033,6 +1140,11 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1033
1140
|
const rel = path.relative(projectRoot, fp).replace(/\\/g, '/');
|
|
1034
1141
|
testErrorsForClient[rel] = detail;
|
|
1035
1142
|
}
|
|
1143
|
+
const errKeys = Object.keys(testErrorsForClient);
|
|
1144
|
+
process.stdout.write(` π testErrors to client: ${errKeys.length} keys: ${errKeys.slice(0, 3).join(', ')}\n`);
|
|
1145
|
+
if (result.failed > 0 && errKeys.length === 0) {
|
|
1146
|
+
broadcast('agent-output', { line: `β οΈ ΠΡΡΡ ${result.failed} ΡΠΏΠ°Π²ΡΠΈΡ
ΡΠ΅ΡΡΠ°, Π½ΠΎ Π΄Π΅ΡΠ°Π»ΠΈ ΠΏΠΎ ΡΠ°ΠΉΠ»Π°ΠΌ Π½Π΅ ΠΏΠΎΠ»ΡΡΠ΅Π½Ρ β ΠΏΡΠΎΠ²Π΅ΡΡ viberadar Π»ΠΎΠ³`, isDim: true });
|
|
1147
|
+
}
|
|
1036
1148
|
broadcast('tests-done', { passed: result.passed, failed: result.failed, testErrors: testErrorsForClient });
|
|
1037
1149
|
}
|
|
1038
1150
|
catch (err) {
|
|
@@ -1065,11 +1177,20 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1065
1177
|
}
|
|
1066
1178
|
if (url === '/api/cancel-agent' && req.method === 'POST') {
|
|
1067
1179
|
agentRunning = false;
|
|
1068
|
-
|
|
1180
|
+
agentQueue.length = 0; // clear queue too
|
|
1181
|
+
process.stdout.write(' βΉ Agent state reset by user (queue cleared)\n');
|
|
1069
1182
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1070
1183
|
res.end(JSON.stringify({ ok: true }));
|
|
1071
1184
|
return;
|
|
1072
1185
|
}
|
|
1186
|
+
if (url === '/api/clear-queue' && req.method === 'POST') {
|
|
1187
|
+
const cleared = agentQueue.length;
|
|
1188
|
+
agentQueue.length = 0;
|
|
1189
|
+
process.stdout.write(` π Queue cleared (${cleared} items)\n`);
|
|
1190
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1191
|
+
res.end(JSON.stringify({ ok: true, cleared }));
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1073
1194
|
if (url === '/api/set-agent' && req.method === 'POST') {
|
|
1074
1195
|
let body = '';
|
|
1075
1196
|
req.on('data', d => body += d);
|
|
@@ -1091,7 +1212,19 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1091
1212
|
});
|
|
1092
1213
|
return;
|
|
1093
1214
|
}
|
|
1094
|
-
//
|
|
1215
|
+
// Server-Sent Events endpoint
|
|
1216
|
+
if (url === '/api/events') {
|
|
1217
|
+
res.writeHead(200, {
|
|
1218
|
+
'Content-Type': 'text/event-stream',
|
|
1219
|
+
'Cache-Control': 'no-cache',
|
|
1220
|
+
'Connection': 'keep-alive',
|
|
1221
|
+
});
|
|
1222
|
+
res.write('data: connected\n\n');
|
|
1223
|
+
sseClients.add(res);
|
|
1224
|
+
req.on('close', () => sseClients.delete(res));
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
// ββ E2E routes βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1095
1228
|
if (url === '/api/e2e/generate-plan' && req.method === 'POST') {
|
|
1096
1229
|
let body = '';
|
|
1097
1230
|
req.on('data', d => body += d);
|
|
@@ -1110,18 +1243,17 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1110
1243
|
});
|
|
1111
1244
|
return;
|
|
1112
1245
|
}
|
|
1113
|
-
|
|
1114
|
-
if (
|
|
1115
|
-
const featureKey = decodeURIComponent(
|
|
1246
|
+
const planMatch = url.match(/^\/api\/e2e\/plan\/(.+)$/);
|
|
1247
|
+
if (planMatch && req.method === 'GET') {
|
|
1248
|
+
const featureKey = decodeURIComponent(planMatch[1]);
|
|
1116
1249
|
const plan = loadE2ePlan(projectRoot, featureKey);
|
|
1117
|
-
if (plan) {
|
|
1118
|
-
res.writeHead(
|
|
1119
|
-
res.end(JSON.stringify(
|
|
1120
|
-
|
|
1121
|
-
else {
|
|
1122
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1123
|
-
res.end(JSON.stringify({ error: 'not found' }));
|
|
1250
|
+
if (!plan) {
|
|
1251
|
+
res.writeHead(404);
|
|
1252
|
+
res.end(JSON.stringify({ error: 'Plan not found' }));
|
|
1253
|
+
return;
|
|
1124
1254
|
}
|
|
1255
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1256
|
+
res.end(JSON.stringify(plan));
|
|
1125
1257
|
return;
|
|
1126
1258
|
}
|
|
1127
1259
|
if (url === '/api/e2e/review' && req.method === 'POST') {
|
|
@@ -1137,12 +1269,8 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1137
1269
|
return;
|
|
1138
1270
|
}
|
|
1139
1271
|
const tc = plan.testCases.find(t => t.id === testCaseId);
|
|
1140
|
-
if (
|
|
1141
|
-
|
|
1142
|
-
res.end(JSON.stringify({ error: 'Test case not found' }));
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
tc.status = status;
|
|
1272
|
+
if (tc)
|
|
1273
|
+
tc.status = status;
|
|
1146
1274
|
saveE2ePlan(projectRoot, plan);
|
|
1147
1275
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1148
1276
|
res.end(JSON.stringify({ ok: true, plan }));
|
|
@@ -1166,10 +1294,8 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1166
1294
|
res.end(JSON.stringify({ error: 'Plan not found' }));
|
|
1167
1295
|
return;
|
|
1168
1296
|
}
|
|
1169
|
-
for (const tc of plan.testCases)
|
|
1170
|
-
|
|
1171
|
-
tc.status = status;
|
|
1172
|
-
}
|
|
1297
|
+
for (const tc of plan.testCases)
|
|
1298
|
+
tc.status = status;
|
|
1173
1299
|
saveE2ePlan(projectRoot, plan);
|
|
1174
1300
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1175
1301
|
res.end(JSON.stringify({ ok: true, plan }));
|
|
@@ -1211,58 +1337,26 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1211
1337
|
res.end(JSON.stringify({ error: 'Plan not found' }));
|
|
1212
1338
|
return;
|
|
1213
1339
|
}
|
|
1214
|
-
|
|
1215
|
-
const testFiles =
|
|
1216
|
-
.
|
|
1217
|
-
.
|
|
1218
|
-
|
|
1219
|
-
const e2eDir = path.join(projectRoot, 'e2e', featureKey);
|
|
1220
|
-
if (fs.existsSync(e2eDir)) {
|
|
1221
|
-
for (const f of fs.readdirSync(e2eDir)) {
|
|
1222
|
-
if (/\.(spec|test)\.(ts|js)$/.test(f)) {
|
|
1223
|
-
const relPath = `e2e/${featureKey}/${f}`;
|
|
1224
|
-
if (!testFiles.includes(relPath))
|
|
1225
|
-
testFiles.push(relPath);
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
if (testFiles.length === 0) {
|
|
1230
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1231
|
-
res.end(JSON.stringify({ ok: true, count: 0 }));
|
|
1232
|
-
broadcast('agent-output', { line: 'ΠΠ΅Ρ E2E ΡΠ΅ΡΡ-ΡΠ°ΠΉΠ»ΠΎΠ² Π΄Π»Ρ Π·Π°ΠΏΡΡΠΊΠ°' });
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1340
|
+
const writtenCases = plan.testCases.filter(tc => ['written', 'passed', 'failed'].includes(tc.status));
|
|
1341
|
+
const testFiles = writtenCases
|
|
1342
|
+
.map(tc => tc.testFilePath)
|
|
1343
|
+
.filter((f) => !!f)
|
|
1344
|
+
.map(f => path.isAbsolute(f) ? f : path.join(projectRoot, f));
|
|
1235
1345
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1236
1346
|
res.end(JSON.stringify({ ok: true, count: testFiles.length }));
|
|
1237
1347
|
broadcast('e2e-tests-running', { featureKey, count: testFiles.length });
|
|
1238
|
-
|
|
1239
|
-
const result = await runPlaywrightTests(testFiles, projectRoot);
|
|
1240
|
-
// Collect screenshots
|
|
1241
|
-
const screenshots = collectScreenshots(projectRoot, featureKey);
|
|
1348
|
+
const result = await runPlaywrightTests(testFiles.length > 0 ? testFiles : [`e2e/${featureKey}`], projectRoot);
|
|
1242
1349
|
// Update plan with results
|
|
1243
1350
|
for (const tc of plan.testCases) {
|
|
1244
|
-
if (
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
r.title.toLowerCase().includes(tc.name.toLowerCase()));
|
|
1249
|
-
if (matching) {
|
|
1250
|
-
tc.status = matching.status === 'passed' ? 'passed' : 'failed';
|
|
1251
|
-
tc.lastError = matching.error;
|
|
1351
|
+
if (result.results[tc.name]) {
|
|
1352
|
+
tc.status = result.results[tc.name];
|
|
1353
|
+
if (result.errors[tc.name])
|
|
1354
|
+
tc.lastError = result.errors[tc.name];
|
|
1252
1355
|
}
|
|
1253
|
-
tc.screenshotPaths =
|
|
1356
|
+
tc.screenshotPaths = collectScreenshots(projectRoot, featureKey, tc.id);
|
|
1254
1357
|
}
|
|
1255
1358
|
saveE2ePlan(projectRoot, plan);
|
|
1256
|
-
|
|
1257
|
-
? `β
E2E: Π²ΡΠ΅ ${result.passed} ΡΠ΅ΡΡΠΎΠ² ΠΏΡΠΎΡΠ»ΠΈ`
|
|
1258
|
-
: `β οΈ E2E: ${result.passed} passed, ${result.failed} failed`;
|
|
1259
|
-
broadcast('agent-output', { line: summary });
|
|
1260
|
-
broadcast('e2e-tests-done', {
|
|
1261
|
-
featureKey,
|
|
1262
|
-
passed: result.passed,
|
|
1263
|
-
failed: result.failed,
|
|
1264
|
-
plan: plan,
|
|
1265
|
-
});
|
|
1359
|
+
broadcast('e2e-tests-done', { featureKey, passed: result.passed, failed: result.failed, plan });
|
|
1266
1360
|
}
|
|
1267
1361
|
catch (err) {
|
|
1268
1362
|
res.writeHead(400);
|
|
@@ -1271,42 +1365,29 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1271
1365
|
});
|
|
1272
1366
|
return;
|
|
1273
1367
|
}
|
|
1274
|
-
|
|
1275
|
-
if (url.startsWith('/api/e2e/screenshot/')) {
|
|
1368
|
+
if (url.startsWith('/api/e2e/screenshot/') && req.method === 'GET') {
|
|
1276
1369
|
const relPath = decodeURIComponent(url.slice('/api/e2e/screenshot/'.length));
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
if (!resolved.startsWith(screenshotsRoot)) {
|
|
1370
|
+
// Path traversal protection
|
|
1371
|
+
const screenshotBase = path.join(projectRoot, '.viberadar', 'e2e-screenshots');
|
|
1372
|
+
const safePath = path.resolve(screenshotBase, relPath);
|
|
1373
|
+
if (!safePath.startsWith(screenshotBase)) {
|
|
1282
1374
|
res.writeHead(403);
|
|
1283
1375
|
res.end('Forbidden');
|
|
1284
1376
|
return;
|
|
1285
1377
|
}
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
|
|
1378
|
+
try {
|
|
1379
|
+
const img = fs.readFileSync(safePath);
|
|
1380
|
+
const ext = path.extname(safePath).toLowerCase();
|
|
1381
|
+
const mime = ext === '.png' ? 'image/png' : ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' : 'image/webp';
|
|
1382
|
+
res.writeHead(200, { 'Content-Type': mime });
|
|
1383
|
+
res.end(img);
|
|
1291
1384
|
}
|
|
1292
|
-
|
|
1385
|
+
catch {
|
|
1293
1386
|
res.writeHead(404);
|
|
1294
|
-
res.end('
|
|
1387
|
+
res.end('Not found');
|
|
1295
1388
|
}
|
|
1296
1389
|
return;
|
|
1297
1390
|
}
|
|
1298
|
-
// Server-Sent Events endpoint
|
|
1299
|
-
if (url === '/api/events') {
|
|
1300
|
-
res.writeHead(200, {
|
|
1301
|
-
'Content-Type': 'text/event-stream',
|
|
1302
|
-
'Cache-Control': 'no-cache',
|
|
1303
|
-
'Connection': 'keep-alive',
|
|
1304
|
-
});
|
|
1305
|
-
res.write('data: connected\n\n');
|
|
1306
|
-
sseClients.add(res);
|
|
1307
|
-
req.on('close', () => sseClients.delete(res));
|
|
1308
|
-
return;
|
|
1309
|
-
}
|
|
1310
1391
|
res.writeHead(404);
|
|
1311
1392
|
res.end('Not found');
|
|
1312
1393
|
});
|