@mcoda/core 0.1.8 → 0.1.9
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/CHANGELOG.md +3 -0
- package/dist/api/AgentsApi.d.ts +8 -1
- package/dist/api/AgentsApi.d.ts.map +1 -1
- package/dist/api/AgentsApi.js +70 -0
- package/dist/api/QaTasksApi.d.ts.map +1 -1
- package/dist/api/QaTasksApi.js +2 -0
- package/dist/api/TasksApi.d.ts.map +1 -1
- package/dist/api/TasksApi.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/prompts/PdrPrompts.d.ts.map +1 -1
- package/dist/prompts/PdrPrompts.js +3 -1
- package/dist/prompts/SdsPrompts.d.ts.map +1 -1
- package/dist/prompts/SdsPrompts.js +2 -0
- package/dist/services/agents/AgentRatingFormula.d.ts +27 -0
- package/dist/services/agents/AgentRatingFormula.d.ts.map +1 -0
- package/dist/services/agents/AgentRatingFormula.js +45 -0
- package/dist/services/agents/AgentRatingService.d.ts +41 -0
- package/dist/services/agents/AgentRatingService.d.ts.map +1 -0
- package/dist/services/agents/AgentRatingService.js +299 -0
- package/dist/services/agents/GatewayAgentService.d.ts +3 -0
- package/dist/services/agents/GatewayAgentService.d.ts.map +1 -1
- package/dist/services/agents/GatewayAgentService.js +68 -24
- package/dist/services/agents/GatewayHandoff.d.ts +7 -0
- package/dist/services/agents/GatewayHandoff.d.ts.map +1 -0
- package/dist/services/agents/GatewayHandoff.js +108 -0
- package/dist/services/backlog/TaskOrderingService.d.ts +1 -0
- package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
- package/dist/services/backlog/TaskOrderingService.js +19 -16
- package/dist/services/docs/DocsService.d.ts +11 -1
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +240 -52
- package/dist/services/execution/GatewayTrioService.d.ts +133 -0
- package/dist/services/execution/GatewayTrioService.d.ts.map +1 -0
- package/dist/services/execution/GatewayTrioService.js +1125 -0
- package/dist/services/execution/QaFollowupService.d.ts +1 -0
- package/dist/services/execution/QaFollowupService.d.ts.map +1 -1
- package/dist/services/execution/QaFollowupService.js +1 -0
- package/dist/services/execution/QaProfileService.d.ts +6 -0
- package/dist/services/execution/QaProfileService.d.ts.map +1 -1
- package/dist/services/execution/QaProfileService.js +165 -3
- package/dist/services/execution/QaTasksService.d.ts +18 -0
- package/dist/services/execution/QaTasksService.d.ts.map +1 -1
- package/dist/services/execution/QaTasksService.js +712 -34
- package/dist/services/execution/WorkOnTasksService.d.ts +14 -0
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +1497 -240
- package/dist/services/openapi/OpenApiService.d.ts +10 -0
- package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
- package/dist/services/openapi/OpenApiService.js +66 -10
- package/dist/services/planning/CreateTasksService.d.ts +6 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +261 -28
- package/dist/services/planning/RefineTasksService.d.ts +5 -0
- package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
- package/dist/services/planning/RefineTasksService.js +184 -35
- package/dist/services/review/CodeReviewService.d.ts +14 -0
- package/dist/services/review/CodeReviewService.d.ts.map +1 -1
- package/dist/services/review/CodeReviewService.js +657 -61
- package/dist/services/shared/ProjectGuidance.d.ts +6 -0
- package/dist/services/shared/ProjectGuidance.d.ts.map +1 -0
- package/dist/services/shared/ProjectGuidance.js +21 -0
- package/dist/services/tasks/TaskCommentFormatter.d.ts +20 -0
- package/dist/services/tasks/TaskCommentFormatter.d.ts.map +1 -0
- package/dist/services/tasks/TaskCommentFormatter.js +54 -0
- package/dist/workspace/WorkspaceManager.d.ts +4 -0
- package/dist/workspace/WorkspaceManager.d.ts.map +1 -1
- package/dist/workspace/WorkspaceManager.js +3 -0
- package/package.json +5 -5
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { WorkspaceRepository } from '@mcoda/db';
|
|
4
5
|
import { PathHelper } from '@mcoda/shared';
|
|
5
6
|
import { JobService } from '../jobs/JobService.js';
|
|
@@ -17,12 +18,92 @@ import { AgentService } from '@mcoda/agents';
|
|
|
17
18
|
import { GlobalRepository } from '@mcoda/db';
|
|
18
19
|
import { DocdexClient } from '@mcoda/integrations';
|
|
19
20
|
import { RoutingService } from '../agents/RoutingService.js';
|
|
21
|
+
import { AgentRatingService } from '../agents/AgentRatingService.js';
|
|
22
|
+
import { loadProjectGuidance } from '../shared/ProjectGuidance.js';
|
|
23
|
+
import { createTaskCommentSlug, formatTaskCommentBody } from '../tasks/TaskCommentFormatter.js';
|
|
20
24
|
const DEFAULT_QA_PROMPT = [
|
|
21
25
|
'You are the QA agent. Before testing, query docdex with the task key and feature keywords (MCP `docdex_search` limit 4–8 or CLI `docdexd query --repo <repo> --query \"<term>\" --limit 6 --snippets=false`). If results look stale, reindex (`docdex_index` or `docdexd index --repo <repo>`) then re-run. Fetch snippets via `docdex_open` or `/snippet/:doc_id?text_only=true` only for specific hits.',
|
|
22
26
|
'Use docdex snippets to derive acceptance criteria, data contracts, edge cases, and non-functional requirements (performance, accessibility, offline/online assumptions). Note if docdex is unavailable and fall back to local docs.',
|
|
27
|
+
'QA policy: always run automated tests. Use browser (Playwright) tests only when the project has a web UI; otherwise run API/endpoint/CLI tests that simulate real usage.',
|
|
23
28
|
].join('\n');
|
|
29
|
+
const QA_TEST_POLICY = 'QA policy: always run automated tests. Use browser (Playwright) tests only when the project has a web UI; otherwise run API/endpoint/CLI tests that simulate real usage.';
|
|
24
30
|
const DEFAULT_JOB_PROMPT = 'You are an mcoda agent that follows workspace runbooks and responds with actionable, concise output.';
|
|
25
31
|
const DEFAULT_CHARACTER_PROMPT = 'Write clearly, avoid hallucinations, cite assumptions, and prioritize risk mitigation for the user.';
|
|
32
|
+
const RUN_ALL_TESTS_MARKER = 'mcoda_run_all_tests_complete';
|
|
33
|
+
const RUN_ALL_TESTS_GUIDANCE = 'Run-all tests did not emit the expected marker. Ensure tests/all.js prints "MCODA_RUN_ALL_TESTS_COMPLETE".';
|
|
34
|
+
const normalizeSlugList = (input) => {
|
|
35
|
+
if (!Array.isArray(input))
|
|
36
|
+
return [];
|
|
37
|
+
const cleaned = new Set();
|
|
38
|
+
for (const slug of input) {
|
|
39
|
+
if (typeof slug !== 'string')
|
|
40
|
+
continue;
|
|
41
|
+
const trimmed = slug.trim();
|
|
42
|
+
if (trimmed)
|
|
43
|
+
cleaned.add(trimmed);
|
|
44
|
+
}
|
|
45
|
+
return Array.from(cleaned);
|
|
46
|
+
};
|
|
47
|
+
const parseCommentBody = (body) => {
|
|
48
|
+
const trimmed = (body ?? '').trim();
|
|
49
|
+
if (!trimmed)
|
|
50
|
+
return { message: '(no details provided)' };
|
|
51
|
+
const lines = trimmed.split(/\r?\n/);
|
|
52
|
+
const normalize = (value) => value.trim().toLowerCase();
|
|
53
|
+
const messageIndex = lines.findIndex((line) => normalize(line) === 'message:');
|
|
54
|
+
const suggestedIndex = lines.findIndex((line) => {
|
|
55
|
+
const normalized = normalize(line);
|
|
56
|
+
return normalized === 'suggested_fix:' || normalized === 'suggested fix:';
|
|
57
|
+
});
|
|
58
|
+
if (messageIndex >= 0) {
|
|
59
|
+
const messageLines = lines.slice(messageIndex + 1, suggestedIndex >= 0 ? suggestedIndex : undefined);
|
|
60
|
+
const message = messageLines.join('\n').trim();
|
|
61
|
+
const suggestedLines = suggestedIndex >= 0 ? lines.slice(suggestedIndex + 1) : [];
|
|
62
|
+
const suggestedFix = suggestedLines.join('\n').trim();
|
|
63
|
+
return { message: message || trimmed, suggestedFix: suggestedFix || undefined };
|
|
64
|
+
}
|
|
65
|
+
if (suggestedIndex >= 0) {
|
|
66
|
+
const message = lines.slice(0, suggestedIndex).join('\n').trim() || trimmed;
|
|
67
|
+
const inlineFix = lines[suggestedIndex]?.split(/suggested fix:/i)[1]?.trim();
|
|
68
|
+
const suggestedTail = lines.slice(suggestedIndex + 1).join('\n').trim();
|
|
69
|
+
const suggestedFix = inlineFix || suggestedTail || undefined;
|
|
70
|
+
return { message, suggestedFix };
|
|
71
|
+
}
|
|
72
|
+
return { message: trimmed };
|
|
73
|
+
};
|
|
74
|
+
const buildCommentBacklog = (comments) => {
|
|
75
|
+
if (!comments.length)
|
|
76
|
+
return '';
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const lines = [];
|
|
79
|
+
const toSingleLine = (value) => value.replace(/\s+/g, ' ').trim();
|
|
80
|
+
for (const comment of comments) {
|
|
81
|
+
const slug = comment.slug?.trim() || undefined;
|
|
82
|
+
const details = parseCommentBody(comment.body);
|
|
83
|
+
const key = slug ??
|
|
84
|
+
`${comment.sourceCommand}:${comment.file ?? ''}:${comment.line ?? ''}:${details.message || comment.body}`;
|
|
85
|
+
if (seen.has(key))
|
|
86
|
+
continue;
|
|
87
|
+
seen.add(key);
|
|
88
|
+
const location = comment.file
|
|
89
|
+
? `${comment.file}${typeof comment.line === 'number' ? `:${comment.line}` : ''}`
|
|
90
|
+
: '(location not specified)';
|
|
91
|
+
const message = toSingleLine(details.message || comment.body || '(no details provided)');
|
|
92
|
+
lines.push(`- [${slug ?? 'untracked'}] ${location} ${message}`);
|
|
93
|
+
const suggestedFix = comment.metadata?.suggestedFix ?? details.suggestedFix ?? undefined;
|
|
94
|
+
if (suggestedFix) {
|
|
95
|
+
lines.push(` Suggested fix: ${toSingleLine(suggestedFix)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
};
|
|
100
|
+
const formatSlugList = (slugs, limit = 12) => {
|
|
101
|
+
if (!slugs.length)
|
|
102
|
+
return 'none';
|
|
103
|
+
if (slugs.length <= limit)
|
|
104
|
+
return slugs.join(', ');
|
|
105
|
+
return `${slugs.slice(0, limit).join(', ')} (+${slugs.length - limit} more)`;
|
|
106
|
+
};
|
|
26
107
|
const MCODA_GITIGNORE_ENTRY = '.mcoda/\n';
|
|
27
108
|
export class QaTasksService {
|
|
28
109
|
constructor(workspace, deps) {
|
|
@@ -39,6 +120,7 @@ export class QaTasksService {
|
|
|
39
120
|
this.docdex = deps.docdex;
|
|
40
121
|
this.repo = deps.repo;
|
|
41
122
|
this.routingService = deps.routingService;
|
|
123
|
+
this.ratingService = deps.ratingService;
|
|
42
124
|
}
|
|
43
125
|
static async create(workspace, options = {}) {
|
|
44
126
|
const repo = await GlobalRepository.create();
|
|
@@ -151,10 +233,12 @@ export class QaTasksService {
|
|
|
151
233
|
details,
|
|
152
234
|
});
|
|
153
235
|
}
|
|
154
|
-
async ensureTaskBranch(task, taskRunId) {
|
|
236
|
+
async ensureTaskBranch(task, taskRunId, allowDirty) {
|
|
155
237
|
try {
|
|
156
238
|
await this.vcs.ensureRepo(this.workspace.workspaceRoot);
|
|
157
|
-
|
|
239
|
+
if (!allowDirty) {
|
|
240
|
+
await this.vcs.ensureClean(this.workspace.workspaceRoot, true, ["test-results", "playwright-report"]);
|
|
241
|
+
}
|
|
158
242
|
if (task.task.vcsBranch) {
|
|
159
243
|
const exists = await this.vcs.branchExists(this.workspace.workspaceRoot, task.task.vcsBranch);
|
|
160
244
|
if (!exists) {
|
|
@@ -203,6 +287,23 @@ export class QaTasksService {
|
|
|
203
287
|
return 'infra_issue';
|
|
204
288
|
return 'fix_required';
|
|
205
289
|
}
|
|
290
|
+
adjustOutcomeForSkippedTests(profile, result, testCommand) {
|
|
291
|
+
if ((profile.runner ?? 'cli') !== 'cli')
|
|
292
|
+
return result;
|
|
293
|
+
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
|
294
|
+
const outputLower = output.toLowerCase();
|
|
295
|
+
const markers = ['no test script configured', 'skipping tests', 'no tests found'];
|
|
296
|
+
if (markers.some((marker) => outputLower.includes(marker))) {
|
|
297
|
+
return { ...result, outcome: 'infra_issue', exitCode: result.exitCode ?? 1 };
|
|
298
|
+
}
|
|
299
|
+
if (testCommand && testCommand.includes('tests/all.js')) {
|
|
300
|
+
if (!outputLower.includes(RUN_ALL_TESTS_MARKER)) {
|
|
301
|
+
const stderr = [result.stderr, RUN_ALL_TESTS_GUIDANCE].filter(Boolean).join('\n');
|
|
302
|
+
return { ...result, outcome: 'infra_issue', exitCode: result.exitCode ?? 1, stderr };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
206
307
|
combineOutcome(result, recommendation) {
|
|
207
308
|
const base = this.mapOutcome(result);
|
|
208
309
|
if (!recommendation)
|
|
@@ -262,9 +363,84 @@ export class QaTasksService {
|
|
|
262
363
|
});
|
|
263
364
|
return resolved.agent;
|
|
264
365
|
}
|
|
366
|
+
ensureRatingService() {
|
|
367
|
+
if (!this.ratingService) {
|
|
368
|
+
if (!this.repo || !this.agentService || !this.routingService) {
|
|
369
|
+
throw new Error('Agent rating requires routing, agent, and repository services.');
|
|
370
|
+
}
|
|
371
|
+
this.ratingService = new AgentRatingService(this.workspace, {
|
|
372
|
+
workspaceRepo: this.deps.workspaceRepo,
|
|
373
|
+
globalRepo: this.repo,
|
|
374
|
+
agentService: this.agentService,
|
|
375
|
+
routingService: this.routingService,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return this.ratingService;
|
|
379
|
+
}
|
|
380
|
+
resolveTaskComplexity(task) {
|
|
381
|
+
const metadata = task.metadata ?? {};
|
|
382
|
+
const metaComplexity = typeof metadata.complexity === 'number' && Number.isFinite(metadata.complexity) ? metadata.complexity : undefined;
|
|
383
|
+
const storyPoints = typeof task.storyPoints === 'number' && Number.isFinite(task.storyPoints) ? task.storyPoints : undefined;
|
|
384
|
+
const candidate = metaComplexity ?? storyPoints;
|
|
385
|
+
if (!Number.isFinite(candidate ?? NaN))
|
|
386
|
+
return undefined;
|
|
387
|
+
return Math.min(10, Math.max(1, Math.round(candidate)));
|
|
388
|
+
}
|
|
265
389
|
estimateTokens(text) {
|
|
266
390
|
return Math.max(1, Math.ceil((text?.length ?? 0) / 4));
|
|
267
391
|
}
|
|
392
|
+
async fileExists(absolutePath) {
|
|
393
|
+
try {
|
|
394
|
+
await fs.access(absolutePath);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async readPackageJson() {
|
|
402
|
+
const pkgPath = path.join(this.workspace.workspaceRoot, 'package.json');
|
|
403
|
+
try {
|
|
404
|
+
const raw = await fs.readFile(pkgPath, 'utf8');
|
|
405
|
+
return JSON.parse(raw);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async detectPackageManager() {
|
|
412
|
+
const root = this.workspace.workspaceRoot;
|
|
413
|
+
if (await this.fileExists(path.join(root, 'pnpm-lock.yaml')))
|
|
414
|
+
return 'pnpm';
|
|
415
|
+
if (await this.fileExists(path.join(root, 'pnpm-workspace.yaml')))
|
|
416
|
+
return 'pnpm';
|
|
417
|
+
if (await this.fileExists(path.join(root, 'yarn.lock')))
|
|
418
|
+
return 'yarn';
|
|
419
|
+
if (await this.fileExists(path.join(root, 'package-lock.json')))
|
|
420
|
+
return 'npm';
|
|
421
|
+
if (await this.fileExists(path.join(root, 'npm-shrinkwrap.json')))
|
|
422
|
+
return 'npm';
|
|
423
|
+
if (await this.fileExists(path.join(root, 'package.json')))
|
|
424
|
+
return 'npm';
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
async resolveTestCommand(profile, requestTestCommand) {
|
|
428
|
+
if (requestTestCommand)
|
|
429
|
+
return requestTestCommand;
|
|
430
|
+
if ((profile.runner ?? 'cli') !== 'cli')
|
|
431
|
+
return undefined;
|
|
432
|
+
if (profile.test_command)
|
|
433
|
+
return profile.test_command;
|
|
434
|
+
if (await this.fileExists(path.join(this.workspace.workspaceRoot, 'tests', 'all.js'))) {
|
|
435
|
+
return 'node tests/all.js';
|
|
436
|
+
}
|
|
437
|
+
const pkg = await this.readPackageJson();
|
|
438
|
+
if (pkg?.scripts?.test) {
|
|
439
|
+
const pm = (await this.detectPackageManager()) ?? 'npm';
|
|
440
|
+
return `${pm} test`;
|
|
441
|
+
}
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
268
444
|
extractJsonCandidate(raw) {
|
|
269
445
|
const fenced = raw.match(/```json([\s\S]*?)```/i);
|
|
270
446
|
const candidate = fenced ? fenced[1] : raw;
|
|
@@ -272,43 +448,126 @@ export class QaTasksService {
|
|
|
272
448
|
const end = candidate.lastIndexOf('}');
|
|
273
449
|
if (start === -1 || end === -1 || end <= start)
|
|
274
450
|
return undefined;
|
|
451
|
+
const slice = candidate.slice(start, end + 1);
|
|
275
452
|
try {
|
|
276
|
-
return JSON.parse(
|
|
453
|
+
return JSON.parse(slice);
|
|
277
454
|
}
|
|
278
455
|
catch {
|
|
279
|
-
|
|
456
|
+
const cleanedLines = slice
|
|
457
|
+
.split(/\r?\n/)
|
|
458
|
+
.filter((line) => {
|
|
459
|
+
const trimmed = line.trim();
|
|
460
|
+
if (!trimmed)
|
|
461
|
+
return true;
|
|
462
|
+
if (trimmed.startsWith("{") ||
|
|
463
|
+
trimmed.startsWith("}") ||
|
|
464
|
+
trimmed.startsWith("[") ||
|
|
465
|
+
trimmed.startsWith("]") ||
|
|
466
|
+
trimmed.startsWith("\"")) {
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
470
|
+
})
|
|
471
|
+
.join("\n")
|
|
472
|
+
.replace(/,\s*([}\]])/g, "$1");
|
|
473
|
+
const withQuotedKeys = cleanedLines.replace(/([{,]\s*)([A-Za-z0-9_]+)\s*:/g, '$1"$2":');
|
|
474
|
+
try {
|
|
475
|
+
return JSON.parse(withQuotedKeys);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
280
480
|
}
|
|
281
481
|
}
|
|
282
482
|
normalizeAgentOutput(parsed) {
|
|
283
483
|
if (!parsed || typeof parsed !== 'object')
|
|
284
484
|
return undefined;
|
|
485
|
+
const asString = (value) => (typeof value === 'string' ? value.trim() : undefined);
|
|
486
|
+
const asNumber = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
487
|
+
const asStringArray = (value) => Array.isArray(value) ? value.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean) : undefined;
|
|
285
488
|
const recommendation = parsed.recommendation;
|
|
286
489
|
if (!recommendation || !['pass', 'fix_required', 'infra_issue', 'unclear'].includes(recommendation))
|
|
287
490
|
return undefined;
|
|
288
|
-
const
|
|
491
|
+
const rawFollowUps = Array.isArray(parsed.follow_up_tasks)
|
|
289
492
|
? parsed.follow_up_tasks
|
|
290
493
|
: Array.isArray(parsed.follow_ups)
|
|
291
494
|
? parsed.follow_ups
|
|
292
495
|
: undefined;
|
|
496
|
+
const followUps = rawFollowUps
|
|
497
|
+
? rawFollowUps.map((item) => ({
|
|
498
|
+
title: asString(item?.title),
|
|
499
|
+
description: asString(item?.description),
|
|
500
|
+
type: asString(item?.type),
|
|
501
|
+
priority: asNumber(item?.priority),
|
|
502
|
+
story_points: asNumber(item?.story_points ?? item?.storyPoints),
|
|
503
|
+
tags: asStringArray(item?.tags),
|
|
504
|
+
related_task_key: asString(item?.related_task_key ?? item?.relatedTaskKey),
|
|
505
|
+
epic_key: asString(item?.epic_key ?? item?.epicKey),
|
|
506
|
+
story_key: asString(item?.story_key ?? item?.storyKey),
|
|
507
|
+
components: asStringArray(item?.components),
|
|
508
|
+
doc_links: asStringArray(item?.doc_links ?? item?.docLinks),
|
|
509
|
+
evidence_url: asString(item?.evidence_url ?? item?.evidenceUrl),
|
|
510
|
+
artifacts: asStringArray(item?.artifacts),
|
|
511
|
+
}))
|
|
512
|
+
: undefined;
|
|
293
513
|
const failures = Array.isArray(parsed.failures)
|
|
294
514
|
? parsed.failures.map((f) => ({ kind: f.kind, message: f.message ?? String(f), evidence: f.evidence }))
|
|
295
515
|
: undefined;
|
|
516
|
+
const resolvedSlugs = normalizeSlugList(parsed.resolved_slugs ?? parsed.resolvedSlugs);
|
|
517
|
+
const unresolvedSlugs = normalizeSlugList(parsed.unresolved_slugs ?? parsed.unresolvedSlugs);
|
|
296
518
|
return {
|
|
297
519
|
recommendation,
|
|
298
520
|
testedScope: parsed.tested_scope ?? parsed.scope,
|
|
299
521
|
coverageSummary: parsed.coverage_summary ?? parsed.coverage,
|
|
300
522
|
failures,
|
|
301
523
|
followUps,
|
|
524
|
+
resolvedSlugs,
|
|
525
|
+
unresolvedSlugs,
|
|
302
526
|
};
|
|
303
527
|
}
|
|
304
|
-
async interpretResult(task, profile, result, agentName, stream, jobId, commandRunId, taskRunId) {
|
|
528
|
+
async interpretResult(task, profile, result, agentName, stream, jobId, commandRunId, taskRunId, commentBacklog, abortSignal) {
|
|
305
529
|
if (!this.agentService) {
|
|
306
530
|
return { recommendation: this.mapOutcome(result) };
|
|
307
531
|
}
|
|
532
|
+
const resolveAbortReason = () => {
|
|
533
|
+
const reason = abortSignal?.reason;
|
|
534
|
+
if (typeof reason === "string" && reason.trim().length > 0)
|
|
535
|
+
return reason;
|
|
536
|
+
if (reason instanceof Error && reason.message)
|
|
537
|
+
return reason.message;
|
|
538
|
+
return "qa_tasks_aborted";
|
|
539
|
+
};
|
|
540
|
+
const abortIfSignaled = () => {
|
|
541
|
+
if (abortSignal?.aborted) {
|
|
542
|
+
throw new Error(resolveAbortReason());
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
const withAbort = async (promise) => {
|
|
546
|
+
if (!abortSignal)
|
|
547
|
+
return promise;
|
|
548
|
+
if (abortSignal.aborted) {
|
|
549
|
+
throw new Error(resolveAbortReason());
|
|
550
|
+
}
|
|
551
|
+
return await new Promise((resolve, reject) => {
|
|
552
|
+
const onAbort = () => reject(new Error(resolveAbortReason()));
|
|
553
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
554
|
+
promise.then(resolve, reject).finally(() => {
|
|
555
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
};
|
|
308
559
|
try {
|
|
560
|
+
abortIfSignaled();
|
|
309
561
|
const agent = await this.resolveAgent(agentName);
|
|
310
562
|
const prompts = await this.loadPrompts(agent.id);
|
|
311
|
-
const
|
|
563
|
+
const projectGuidance = await loadProjectGuidance(this.workspace.workspaceRoot);
|
|
564
|
+
if (projectGuidance && taskRunId) {
|
|
565
|
+
await this.logTask(taskRunId, `Loaded project guidance from ${projectGuidance.source}`, 'project_guidance');
|
|
566
|
+
}
|
|
567
|
+
const guidanceBlock = projectGuidance?.content ? `Project Guidance (read first):\n${projectGuidance.content}` : undefined;
|
|
568
|
+
const systemPrompt = [guidanceBlock, prompts.jobPrompt, prompts.characterPrompt, prompts.commandPrompt, QA_TEST_POLICY]
|
|
569
|
+
.filter(Boolean)
|
|
570
|
+
.join('\n\n');
|
|
312
571
|
const docCtx = await this.gatherDocContext(task.task, taskRunId);
|
|
313
572
|
const acceptance = (task.task.acceptanceCriteria ?? []).map((line) => `- ${line}`).join('\n');
|
|
314
573
|
const prompt = [
|
|
@@ -319,6 +578,7 @@ export class QaTasksService {
|
|
|
319
578
|
task.task.description ? `Task description:\n${task.task.description}` : '',
|
|
320
579
|
`Epic/Story: ${task.task.epicKey ?? task.task.epicId} / ${task.task.storyKey ?? task.task.userStoryId}`,
|
|
321
580
|
acceptance ? `Acceptance criteria:\n${acceptance}` : 'Acceptance criteria: (not provided)',
|
|
581
|
+
commentBacklog ? `Comment backlog (unresolved slugs):\n${commentBacklog}` : 'Comment backlog: none',
|
|
322
582
|
`QA profile: ${profile.name} (${profile.runner ?? 'cli'})`,
|
|
323
583
|
`Test command / runner outcome: exit=${result.exitCode} outcome=${result.outcome}`,
|
|
324
584
|
result.stdout ? `Stdout (truncated):\n${result.stdout.slice(0, 3000)}` : '',
|
|
@@ -332,9 +592,11 @@ export class QaTasksService {
|
|
|
332
592
|
' "coverage_summary": string,',
|
|
333
593
|
' "failures": [{ "kind": "functional|contract|perf|security|infra", "message": string, "evidence": string }],',
|
|
334
594
|
' "recommendation": "pass|fix_required|infra_issue|unclear",',
|
|
335
|
-
' "follow_up_tasks": [{ "title": string, "description": string, "type": "bug|qa_followup|chore", "priority": number, "story_points": number, "tags": string[], "related_task_key": string, "epic_key": string, "story_key": string, "doc_links": string[], "evidence_url": string, "artifacts": string[] }]',
|
|
595
|
+
' "follow_up_tasks": [{ "title": string, "description": string, "type": "bug|qa_followup|chore", "priority": number, "story_points": number, "tags": string[], "related_task_key": string, "epic_key": string, "story_key": string, "doc_links": string[], "evidence_url": string, "artifacts": string[] }],',
|
|
596
|
+
' "resolvedSlugs": ["Optional list of comment slugs that are confirmed fixed"],',
|
|
597
|
+
' "unresolvedSlugs": ["Optional list of comment slugs still open or reintroduced"]',
|
|
336
598
|
'}',
|
|
337
|
-
'Do not include prose outside the JSON.',
|
|
599
|
+
'Do not include prose outside the JSON. Include resolvedSlugs/unresolvedSlugs when reviewing comment backlog.',
|
|
338
600
|
].join('\n'),
|
|
339
601
|
]
|
|
340
602
|
.filter(Boolean)
|
|
@@ -355,14 +617,19 @@ export class QaTasksService {
|
|
|
355
617
|
let output = '';
|
|
356
618
|
let chunkCount = 0;
|
|
357
619
|
if (stream && this.agentService.invokeStream) {
|
|
358
|
-
const gen = await this.agentService.invokeStream(agent.id, { input: prompt, metadata: { command: 'qa-tasks' } });
|
|
359
|
-
|
|
620
|
+
const gen = await withAbort(this.agentService.invokeStream(agent.id, { input: prompt, metadata: { command: 'qa-tasks' } }));
|
|
621
|
+
while (true) {
|
|
622
|
+
abortIfSignaled();
|
|
623
|
+
const { value, done } = await withAbort(gen.next());
|
|
624
|
+
if (done)
|
|
625
|
+
break;
|
|
626
|
+
const chunk = value;
|
|
360
627
|
output += chunk.output ?? '';
|
|
361
628
|
chunkCount += 1;
|
|
362
629
|
}
|
|
363
630
|
}
|
|
364
631
|
else {
|
|
365
|
-
const res = await this.agentService.invoke(agent.id, { input: prompt, metadata: { command: 'qa-tasks' } });
|
|
632
|
+
const res = await withAbort(this.agentService.invoke(agent.id, { input: prompt, metadata: { command: 'qa-tasks' } }));
|
|
366
633
|
output = res.output ?? '';
|
|
367
634
|
}
|
|
368
635
|
const tokensPrompt = this.estimateTokens(prompt);
|
|
@@ -401,7 +668,69 @@ export class QaTasksService {
|
|
|
401
668
|
modelName: agent.defaultModel,
|
|
402
669
|
};
|
|
403
670
|
}
|
|
404
|
-
|
|
671
|
+
const retryPrompt = `${prompt}\n\nReturn STRICT JSON only. Do not include prose, markdown fences, or comments.`;
|
|
672
|
+
let retryOutput = "";
|
|
673
|
+
if (stream && this.agentService.invokeStream) {
|
|
674
|
+
const gen = await withAbort(this.agentService.invokeStream(agent.id, { input: retryPrompt, metadata: { command: 'qa-tasks' } }));
|
|
675
|
+
while (true) {
|
|
676
|
+
abortIfSignaled();
|
|
677
|
+
const { value, done } = await withAbort(gen.next());
|
|
678
|
+
if (done)
|
|
679
|
+
break;
|
|
680
|
+
const chunk = value;
|
|
681
|
+
retryOutput += chunk.output ?? '';
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
const res = await withAbort(this.agentService.invoke(agent.id, { input: retryPrompt, metadata: { command: 'qa-tasks' } }));
|
|
686
|
+
retryOutput = res.output ?? '';
|
|
687
|
+
}
|
|
688
|
+
const retryTokensPrompt = this.estimateTokens(retryPrompt);
|
|
689
|
+
const retryTokensCompletion = this.estimateTokens(retryOutput);
|
|
690
|
+
if (!this.dryRunGuard) {
|
|
691
|
+
await this.jobService.recordTokenUsage({
|
|
692
|
+
workspaceId: this.workspace.workspaceId,
|
|
693
|
+
agentId: agent.id,
|
|
694
|
+
modelName: agent.defaultModel,
|
|
695
|
+
jobId,
|
|
696
|
+
taskId: task.task.id,
|
|
697
|
+
commandRunId,
|
|
698
|
+
taskRunId,
|
|
699
|
+
tokensPrompt: retryTokensPrompt,
|
|
700
|
+
tokensCompletion: retryTokensCompletion,
|
|
701
|
+
tokensTotal: retryTokensPrompt + retryTokensCompletion,
|
|
702
|
+
timestamp: new Date().toISOString(),
|
|
703
|
+
metadata: {
|
|
704
|
+
commandName: 'qa-tasks',
|
|
705
|
+
action: 'qa-interpret-retry',
|
|
706
|
+
taskKey: task.task.key,
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
const retryParsed = this.extractJsonCandidate(retryOutput);
|
|
711
|
+
const retryNormalized = this.normalizeAgentOutput(retryParsed);
|
|
712
|
+
if (retryNormalized) {
|
|
713
|
+
return {
|
|
714
|
+
...retryNormalized,
|
|
715
|
+
rawOutput: retryOutput,
|
|
716
|
+
tokensPrompt: tokensPrompt + retryTokensPrompt,
|
|
717
|
+
tokensCompletion: tokensCompletion + retryTokensCompletion,
|
|
718
|
+
agentId: agent.id,
|
|
719
|
+
modelName: agent.defaultModel,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
if (taskRunId) {
|
|
723
|
+
await this.logTask(taskRunId, "QA agent returned invalid JSON after retry; falling back to QA outcome.", "qa-agent");
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
recommendation: 'unclear',
|
|
727
|
+
rawOutput: retryOutput || output,
|
|
728
|
+
tokensPrompt: tokensPrompt + retryTokensPrompt,
|
|
729
|
+
tokensCompletion: tokensCompletion + retryTokensCompletion,
|
|
730
|
+
agentId: agent.id,
|
|
731
|
+
modelName: agent.defaultModel,
|
|
732
|
+
invalidJson: true,
|
|
733
|
+
};
|
|
405
734
|
}
|
|
406
735
|
catch (error) {
|
|
407
736
|
if (taskRunId) {
|
|
@@ -410,6 +739,152 @@ export class QaTasksService {
|
|
|
410
739
|
return { recommendation: this.mapOutcome(result) };
|
|
411
740
|
}
|
|
412
741
|
}
|
|
742
|
+
async loadCommentContext(taskId) {
|
|
743
|
+
const comments = await this.deps.workspaceRepo.listTaskComments(taskId, {
|
|
744
|
+
sourceCommands: ['code-review', 'qa-tasks'],
|
|
745
|
+
limit: 50,
|
|
746
|
+
});
|
|
747
|
+
const unresolved = comments.filter((comment) => !comment.resolvedAt);
|
|
748
|
+
return { comments, unresolved };
|
|
749
|
+
}
|
|
750
|
+
resolveFailureSlug(failure) {
|
|
751
|
+
const message = (failure.message ?? '').trim() || 'QA issue';
|
|
752
|
+
return createTaskCommentSlug({
|
|
753
|
+
source: 'qa-tasks',
|
|
754
|
+
message,
|
|
755
|
+
category: failure.kind ?? 'qa_issue',
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
async applyCommentResolutions(params) {
|
|
759
|
+
const existingBySlug = new Map();
|
|
760
|
+
const openBySlug = new Set();
|
|
761
|
+
const resolvedBySlug = new Set();
|
|
762
|
+
for (const comment of params.existingComments) {
|
|
763
|
+
if (!comment.slug)
|
|
764
|
+
continue;
|
|
765
|
+
if (!existingBySlug.has(comment.slug)) {
|
|
766
|
+
existingBySlug.set(comment.slug, comment);
|
|
767
|
+
}
|
|
768
|
+
if (comment.resolvedAt) {
|
|
769
|
+
resolvedBySlug.add(comment.slug);
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
openBySlug.add(comment.slug);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const resolvedSlugs = normalizeSlugList(params.resolvedSlugs ?? undefined);
|
|
776
|
+
const resolvedSet = new Set(resolvedSlugs);
|
|
777
|
+
const unresolvedSet = new Set(normalizeSlugList(params.unresolvedSlugs ?? undefined));
|
|
778
|
+
const failureSlugs = [];
|
|
779
|
+
for (const failure of params.failures ?? []) {
|
|
780
|
+
const slug = this.resolveFailureSlug(failure);
|
|
781
|
+
failureSlugs.push(slug);
|
|
782
|
+
if (!resolvedSet.has(slug)) {
|
|
783
|
+
unresolvedSet.add(slug);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
for (const slug of resolvedSet) {
|
|
787
|
+
unresolvedSet.delete(slug);
|
|
788
|
+
}
|
|
789
|
+
const toResolve = resolvedSlugs.filter((slug) => openBySlug.has(slug));
|
|
790
|
+
const toReopen = Array.from(unresolvedSet).filter((slug) => resolvedBySlug.has(slug));
|
|
791
|
+
if (!this.dryRunGuard) {
|
|
792
|
+
for (const slug of toResolve) {
|
|
793
|
+
await this.deps.workspaceRepo.resolveTaskComment({
|
|
794
|
+
taskId: params.task.id,
|
|
795
|
+
slug,
|
|
796
|
+
resolvedAt: new Date().toISOString(),
|
|
797
|
+
resolvedBy: params.agentId ?? null,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
for (const slug of toReopen) {
|
|
801
|
+
await this.deps.workspaceRepo.reopenTaskComment({ taskId: params.task.id, slug });
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const createdSlugs = new Set();
|
|
805
|
+
for (const failure of params.failures ?? []) {
|
|
806
|
+
const slug = this.resolveFailureSlug(failure);
|
|
807
|
+
if (existingBySlug.has(slug) || createdSlugs.has(slug))
|
|
808
|
+
continue;
|
|
809
|
+
const baseMessage = (failure.message ?? '').trim() || '(no details provided)';
|
|
810
|
+
const message = failure.evidence ? `${baseMessage}\nEvidence: ${failure.evidence}` : baseMessage;
|
|
811
|
+
const body = formatTaskCommentBody({
|
|
812
|
+
slug,
|
|
813
|
+
source: 'qa-tasks',
|
|
814
|
+
message,
|
|
815
|
+
status: 'open',
|
|
816
|
+
category: failure.kind ?? 'qa_issue',
|
|
817
|
+
});
|
|
818
|
+
if (!this.dryRunGuard) {
|
|
819
|
+
await this.deps.workspaceRepo.createTaskComment({
|
|
820
|
+
taskId: params.task.id,
|
|
821
|
+
taskRunId: params.taskRunId,
|
|
822
|
+
jobId: params.jobId,
|
|
823
|
+
sourceCommand: 'qa-tasks',
|
|
824
|
+
authorType: 'agent',
|
|
825
|
+
authorAgentId: params.agentId ?? null,
|
|
826
|
+
category: failure.kind ?? 'qa_issue',
|
|
827
|
+
slug,
|
|
828
|
+
status: 'open',
|
|
829
|
+
body,
|
|
830
|
+
createdAt: new Date().toISOString(),
|
|
831
|
+
metadata: {
|
|
832
|
+
kind: failure.kind,
|
|
833
|
+
evidence: failure.evidence,
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
createdSlugs.add(slug);
|
|
838
|
+
}
|
|
839
|
+
const openSet = new Set(openBySlug);
|
|
840
|
+
for (const slug of unresolvedSet) {
|
|
841
|
+
openSet.add(slug);
|
|
842
|
+
}
|
|
843
|
+
for (const slug of resolvedSet) {
|
|
844
|
+
openSet.delete(slug);
|
|
845
|
+
}
|
|
846
|
+
if ((resolvedSlugs.length || toReopen.length || unresolvedSet.size) && !this.dryRunGuard) {
|
|
847
|
+
const resolutionMessage = [
|
|
848
|
+
`Resolved slugs: ${formatSlugList(toResolve)}`,
|
|
849
|
+
`Reopened slugs: ${formatSlugList(toReopen)}`,
|
|
850
|
+
`Open slugs: ${formatSlugList(Array.from(openSet))}`,
|
|
851
|
+
].join('\n');
|
|
852
|
+
const resolutionSlug = createTaskCommentSlug({
|
|
853
|
+
source: 'qa-tasks',
|
|
854
|
+
message: resolutionMessage,
|
|
855
|
+
category: 'comment_resolution',
|
|
856
|
+
});
|
|
857
|
+
const resolutionBody = formatTaskCommentBody({
|
|
858
|
+
slug: resolutionSlug,
|
|
859
|
+
source: 'qa-tasks',
|
|
860
|
+
message: resolutionMessage,
|
|
861
|
+
status: 'resolved',
|
|
862
|
+
category: 'comment_resolution',
|
|
863
|
+
});
|
|
864
|
+
const createdAt = new Date().toISOString();
|
|
865
|
+
await this.deps.workspaceRepo.createTaskComment({
|
|
866
|
+
taskId: params.task.id,
|
|
867
|
+
taskRunId: params.taskRunId,
|
|
868
|
+
jobId: params.jobId,
|
|
869
|
+
sourceCommand: 'qa-tasks',
|
|
870
|
+
authorType: 'agent',
|
|
871
|
+
authorAgentId: params.agentId ?? null,
|
|
872
|
+
category: 'comment_resolution',
|
|
873
|
+
slug: resolutionSlug,
|
|
874
|
+
status: 'resolved',
|
|
875
|
+
body: resolutionBody,
|
|
876
|
+
createdAt,
|
|
877
|
+
resolvedAt: createdAt,
|
|
878
|
+
resolvedBy: params.agentId ?? null,
|
|
879
|
+
metadata: {
|
|
880
|
+
resolvedSlugs: toResolve,
|
|
881
|
+
reopenedSlugs: toReopen,
|
|
882
|
+
openSlugs: Array.from(openSet),
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
return { resolved: toResolve, reopened: toReopen, open: Array.from(openSet) };
|
|
887
|
+
}
|
|
413
888
|
async createTaskRun(task, jobId, commandRunId) {
|
|
414
889
|
const startedAt = new Date().toISOString();
|
|
415
890
|
return this.deps.workspaceRepo.createTaskRun({
|
|
@@ -456,6 +931,9 @@ export class QaTasksService {
|
|
|
456
931
|
else if (outcome === 'infra_issue') {
|
|
457
932
|
await this.stateService.markBlocked(task, 'qa_infra_issue');
|
|
458
933
|
}
|
|
934
|
+
else if (outcome === 'unclear') {
|
|
935
|
+
await this.stateService.markBlocked(task, 'qa_unclear');
|
|
936
|
+
}
|
|
459
937
|
}
|
|
460
938
|
buildFollowupSuggestion(task, result, notes) {
|
|
461
939
|
const summary = notes || result.stderr || result.stdout || 'QA failure detected';
|
|
@@ -474,6 +952,38 @@ export class QaTasksService {
|
|
|
474
952
|
testName: tests[0],
|
|
475
953
|
};
|
|
476
954
|
}
|
|
955
|
+
buildManualQaFollowup(task, rawOutput) {
|
|
956
|
+
const summary = rawOutput ? rawOutput.slice(0, 1000) : 'QA agent returned invalid JSON after retry.';
|
|
957
|
+
const components = Array.isArray(task.metadata?.components) ? task.metadata.components : [];
|
|
958
|
+
const docLinks = Array.isArray(task.metadata?.doc_links) ? task.metadata.doc_links : [];
|
|
959
|
+
const tests = Array.isArray(task.metadata?.tests) ? task.metadata.tests : [];
|
|
960
|
+
return {
|
|
961
|
+
title: `Manual QA follow-up for ${task.key}`,
|
|
962
|
+
description: `QA agent returned invalid JSON after retry. Manual QA required.\n\nRaw output:\n${summary}`.slice(0, 2000),
|
|
963
|
+
type: 'qa_followup',
|
|
964
|
+
storyPoints: 1,
|
|
965
|
+
priority: 90,
|
|
966
|
+
tags: ['qa', 'manual', ...components],
|
|
967
|
+
components,
|
|
968
|
+
docLinks,
|
|
969
|
+
testName: tests[0],
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
buildFollowupSlug(task, suggestion) {
|
|
973
|
+
const seedParts = [
|
|
974
|
+
task.key,
|
|
975
|
+
suggestion.title ?? '',
|
|
976
|
+
suggestion.description ?? '',
|
|
977
|
+
suggestion.type ?? '',
|
|
978
|
+
suggestion.testName ?? '',
|
|
979
|
+
suggestion.evidenceUrl ?? '',
|
|
980
|
+
...(suggestion.tags ?? []),
|
|
981
|
+
...(suggestion.components ?? []),
|
|
982
|
+
];
|
|
983
|
+
const seed = seedParts.join('|').toLowerCase();
|
|
984
|
+
const digest = createHash('sha1').update(seed).digest('hex').slice(0, 12);
|
|
985
|
+
return `qa-followup-${task.key}-${digest}`;
|
|
986
|
+
}
|
|
477
987
|
toFollowupSuggestion(task, agentFollow, artifacts) {
|
|
478
988
|
const taskComponents = Array.isArray(task.metadata?.components) ? task.metadata.components : [];
|
|
479
989
|
const taskDocLinks = Array.isArray(task.metadata?.doc_links) ? task.metadata.doc_links : [];
|
|
@@ -498,7 +1008,12 @@ export class QaTasksService {
|
|
|
498
1008
|
return [];
|
|
499
1009
|
const agent = await this.resolveAgent(undefined);
|
|
500
1010
|
const prompts = await this.loadPrompts(agent.id);
|
|
501
|
-
const
|
|
1011
|
+
const projectGuidance = await loadProjectGuidance(this.workspace.workspaceRoot);
|
|
1012
|
+
if (projectGuidance && taskRunId) {
|
|
1013
|
+
await this.logTask(taskRunId, `Loaded project guidance from ${projectGuidance.source}`, 'project_guidance');
|
|
1014
|
+
}
|
|
1015
|
+
const guidanceBlock = projectGuidance?.content ? `Project Guidance (read first):\n${projectGuidance.content}` : undefined;
|
|
1016
|
+
const systemPrompt = [guidanceBlock, prompts.jobPrompt, prompts.characterPrompt, prompts.commandPrompt].filter(Boolean).join('\n\n');
|
|
502
1017
|
const docCtx = await this.gatherDocContext(task.task, taskRunId);
|
|
503
1018
|
const prompt = [
|
|
504
1019
|
systemPrompt,
|
|
@@ -591,7 +1106,7 @@ export class QaTasksService {
|
|
|
591
1106
|
}
|
|
592
1107
|
return { taskKey: task.task.key, outcome: 'infra_issue', notes: 'status_gating' };
|
|
593
1108
|
}
|
|
594
|
-
const branchCheck = await this.ensureTaskBranch(task, taskRun.id);
|
|
1109
|
+
const branchCheck = await this.ensureTaskBranch(task, taskRun.id, ctx.request.allowDirty ?? false);
|
|
595
1110
|
if (!branchCheck.ok) {
|
|
596
1111
|
if (!this.dryRunGuard) {
|
|
597
1112
|
await this.applyStateTransition(task.task, 'infra_issue');
|
|
@@ -607,6 +1122,15 @@ export class QaTasksService {
|
|
|
607
1122
|
recommendation: 'infra_issue',
|
|
608
1123
|
metadata: { reason: 'vcs_branch_missing', detail: branchCheck.message },
|
|
609
1124
|
});
|
|
1125
|
+
const message = `VCS validation failed: ${branchCheck.message ?? 'unknown error'}`;
|
|
1126
|
+
const slug = createTaskCommentSlug({ source: 'qa-tasks', message, category: 'qa_issue' });
|
|
1127
|
+
const body = formatTaskCommentBody({
|
|
1128
|
+
slug,
|
|
1129
|
+
source: 'qa-tasks',
|
|
1130
|
+
message,
|
|
1131
|
+
status: 'open',
|
|
1132
|
+
category: 'qa_issue',
|
|
1133
|
+
});
|
|
610
1134
|
await this.deps.workspaceRepo.createTaskComment({
|
|
611
1135
|
taskId: task.task.id,
|
|
612
1136
|
taskRunId: taskRun.id,
|
|
@@ -614,7 +1138,9 @@ export class QaTasksService {
|
|
|
614
1138
|
sourceCommand: 'qa-tasks',
|
|
615
1139
|
authorType: 'agent',
|
|
616
1140
|
category: 'qa_issue',
|
|
617
|
-
|
|
1141
|
+
slug,
|
|
1142
|
+
status: 'open',
|
|
1143
|
+
body,
|
|
618
1144
|
createdAt: new Date().toISOString(),
|
|
619
1145
|
});
|
|
620
1146
|
}
|
|
@@ -684,16 +1210,22 @@ export class QaTasksService {
|
|
|
684
1210
|
}
|
|
685
1211
|
return { taskKey: task.task.key, outcome: 'infra_issue', profile: profile.name, runner: profile.runner, notes: 'no_adapter' };
|
|
686
1212
|
}
|
|
1213
|
+
const testCommand = await this.resolveTestCommand(profile, ctx.request.testCommand);
|
|
687
1214
|
const qaCtx = {
|
|
688
1215
|
workspaceRoot: this.workspace.workspaceRoot,
|
|
689
1216
|
jobId: ctx.jobId,
|
|
690
1217
|
taskKey: task.task.key,
|
|
691
1218
|
env: process.env,
|
|
692
|
-
testCommandOverride:
|
|
1219
|
+
testCommandOverride: testCommand,
|
|
693
1220
|
};
|
|
694
1221
|
const ensure = await adapter.ensureInstalled(profile, qaCtx);
|
|
695
1222
|
if (!ensure.ok) {
|
|
696
|
-
|
|
1223
|
+
const guidance = 'Run "docdex setup" to install Playwright and at least one browser.';
|
|
1224
|
+
const installMessage = ensure.message ?? 'QA install failed';
|
|
1225
|
+
const installMessageWithGuidance = installMessage.includes('docdex setup')
|
|
1226
|
+
? installMessage
|
|
1227
|
+
: `${installMessage} ${guidance}`;
|
|
1228
|
+
await this.logTask(taskRun.id, installMessageWithGuidance, 'qa-install');
|
|
697
1229
|
if (!this.dryRunGuard) {
|
|
698
1230
|
await this.applyStateTransition(task.task, 'infra_issue');
|
|
699
1231
|
await this.finishTaskRun(taskRun, 'failed');
|
|
@@ -708,7 +1240,28 @@ export class QaTasksService {
|
|
|
708
1240
|
runner: profile.runner,
|
|
709
1241
|
rawOutcome: 'infra_issue',
|
|
710
1242
|
recommendation: 'infra_issue',
|
|
711
|
-
metadata: { install:
|
|
1243
|
+
metadata: { install: installMessageWithGuidance, adapter: profile.runner },
|
|
1244
|
+
});
|
|
1245
|
+
const message = `QA infra issue: ${installMessageWithGuidance}`;
|
|
1246
|
+
const slug = createTaskCommentSlug({ source: 'qa-tasks', message, category: 'qa_issue' });
|
|
1247
|
+
const body = formatTaskCommentBody({
|
|
1248
|
+
slug,
|
|
1249
|
+
source: 'qa-tasks',
|
|
1250
|
+
message,
|
|
1251
|
+
status: 'open',
|
|
1252
|
+
category: 'qa_issue',
|
|
1253
|
+
});
|
|
1254
|
+
await this.deps.workspaceRepo.createTaskComment({
|
|
1255
|
+
taskId: task.task.id,
|
|
1256
|
+
taskRunId: taskRun.id,
|
|
1257
|
+
jobId: ctx.jobId,
|
|
1258
|
+
sourceCommand: 'qa-tasks',
|
|
1259
|
+
authorType: 'agent',
|
|
1260
|
+
category: 'qa_issue',
|
|
1261
|
+
slug,
|
|
1262
|
+
status: 'open',
|
|
1263
|
+
body,
|
|
1264
|
+
createdAt: new Date().toISOString(),
|
|
712
1265
|
});
|
|
713
1266
|
}
|
|
714
1267
|
return {
|
|
@@ -716,18 +1269,36 @@ export class QaTasksService {
|
|
|
716
1269
|
outcome: 'infra_issue',
|
|
717
1270
|
profile: profile.name,
|
|
718
1271
|
runner: profile.runner,
|
|
719
|
-
notes:
|
|
1272
|
+
notes: installMessageWithGuidance,
|
|
720
1273
|
};
|
|
721
1274
|
}
|
|
722
1275
|
const artifactDir = path.join(this.workspace.workspaceRoot, '.mcoda', 'jobs', ctx.jobId, 'qa', task.task.key);
|
|
723
1276
|
await PathHelper.ensureDir(artifactDir);
|
|
724
|
-
const
|
|
1277
|
+
const qaEnv = { ...qaCtx.env };
|
|
1278
|
+
const browsersPath = ensure.details?.playwrightBrowsersPath;
|
|
1279
|
+
if (typeof browsersPath === 'string' && browsersPath.trim().length > 0) {
|
|
1280
|
+
qaEnv.PLAYWRIGHT_BROWSERS_PATH = browsersPath;
|
|
1281
|
+
}
|
|
1282
|
+
let result = await adapter.invoke(profile, { ...qaCtx, env: qaEnv, artifactDir });
|
|
1283
|
+
result = this.adjustOutcomeForSkippedTests(profile, result, testCommand);
|
|
725
1284
|
await this.logTask(taskRun.id, `QA run completed with outcome ${result.outcome}`, 'qa-exec', {
|
|
726
1285
|
exitCode: result.exitCode,
|
|
727
1286
|
});
|
|
728
|
-
const
|
|
1287
|
+
const commentContext = await this.loadCommentContext(task.task.id);
|
|
1288
|
+
const commentBacklog = buildCommentBacklog(commentContext.unresolved);
|
|
1289
|
+
const interpretation = await this.interpretResult(task, profile, result, ctx.request.agentName, ctx.request.agentStream ?? true, ctx.jobId, ctx.commandRunId, taskRun.id, commentBacklog, ctx.request.abortSignal);
|
|
729
1290
|
const outcome = this.combineOutcome(result, interpretation.recommendation);
|
|
730
1291
|
const artifacts = result.artifacts ?? [];
|
|
1292
|
+
const commentResolution = await this.applyCommentResolutions({
|
|
1293
|
+
task: task.task,
|
|
1294
|
+
taskRunId: taskRun.id,
|
|
1295
|
+
jobId: ctx.jobId,
|
|
1296
|
+
agentId: interpretation.agentId,
|
|
1297
|
+
failures: interpretation.failures ?? [],
|
|
1298
|
+
resolvedSlugs: interpretation.resolvedSlugs,
|
|
1299
|
+
unresolvedSlugs: interpretation.unresolvedSlugs,
|
|
1300
|
+
existingComments: commentContext.comments,
|
|
1301
|
+
});
|
|
731
1302
|
let qaRun;
|
|
732
1303
|
if (!this.dryRunGuard) {
|
|
733
1304
|
qaRun = await this.deps.workspaceRepo.createTaskQaRun({
|
|
@@ -756,6 +1327,7 @@ export class QaTasksService {
|
|
|
756
1327
|
testedScope: interpretation.testedScope,
|
|
757
1328
|
coverageSummary: interpretation.coverageSummary,
|
|
758
1329
|
failures: interpretation.failures,
|
|
1330
|
+
invalidJson: interpretation.invalidJson ?? false,
|
|
759
1331
|
},
|
|
760
1332
|
});
|
|
761
1333
|
}
|
|
@@ -764,13 +1336,24 @@ export class QaTasksService {
|
|
|
764
1336
|
await this.finishTaskRun(taskRun, outcome === 'pass' ? 'succeeded' : 'failed');
|
|
765
1337
|
}
|
|
766
1338
|
const followups = [];
|
|
767
|
-
|
|
1339
|
+
const wantsFollowups = ctx.request.createFollowupTasks !== 'none';
|
|
1340
|
+
const needsManualFollowup = interpretation.invalidJson === true;
|
|
1341
|
+
if ((outcome === 'fix_required' || needsManualFollowup) && wantsFollowups) {
|
|
768
1342
|
const suggestions = interpretation.followUps?.map((f) => this.toFollowupSuggestion(task.task, f, artifacts)) ?? [];
|
|
769
|
-
if (
|
|
1343
|
+
if (needsManualFollowup) {
|
|
1344
|
+
suggestions.unshift(this.buildManualQaFollowup(task.task, interpretation.rawOutput));
|
|
1345
|
+
}
|
|
1346
|
+
else if (suggestions.length === 0) {
|
|
770
1347
|
suggestions.push(this.buildFollowupSuggestion(task.task, result, ctx.request.notes));
|
|
771
1348
|
}
|
|
772
1349
|
const interactive = ctx.request.createFollowupTasks === 'prompt' && process.stdout.isTTY;
|
|
773
1350
|
for (const suggestion of suggestions) {
|
|
1351
|
+
const followupSlug = this.buildFollowupSlug(task.task, suggestion);
|
|
1352
|
+
const existing = await this.deps.workspaceRepo.listTasksByMetadataValue(task.task.projectId, 'qa_followup_slug', followupSlug);
|
|
1353
|
+
if (existing.length) {
|
|
1354
|
+
await this.logTask(taskRun.id, `Skipped follow-up ${followupSlug}; already exists: ${existing.map((item) => item.key).join(', ')}`, 'qa-followup');
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
774
1357
|
let proceed = ctx.request.createFollowupTasks !== 'prompt';
|
|
775
1358
|
if (interactive) {
|
|
776
1359
|
const rl = readline.createInterface({ input, output });
|
|
@@ -782,7 +1365,7 @@ export class QaTasksService {
|
|
|
782
1365
|
continue;
|
|
783
1366
|
try {
|
|
784
1367
|
if (!this.dryRunGuard) {
|
|
785
|
-
const created = await this.followupService.createFollowupTask({ ...task.task, storyKey: task.task.storyKey, epicKey: task.task.epicKey }, suggestion);
|
|
1368
|
+
const created = await this.followupService.createFollowupTask({ ...task.task, storyKey: task.task.storyKey, epicKey: task.task.epicKey }, { ...suggestion, followupSlug });
|
|
786
1369
|
followups.push(created.task.key);
|
|
787
1370
|
await this.logTask(taskRun.id, `Created follow-up ${created.task.key}`, 'qa-followup');
|
|
788
1371
|
}
|
|
@@ -794,32 +1377,89 @@ export class QaTasksService {
|
|
|
794
1377
|
}
|
|
795
1378
|
const bodyLines = [
|
|
796
1379
|
`QA outcome: ${outcome}`,
|
|
1380
|
+
outcome === 'unclear'
|
|
1381
|
+
? 'QA outcome unclear: provide missing acceptance criteria, reproduction steps, and expected behavior.'
|
|
1382
|
+
: '',
|
|
797
1383
|
profile ? `Profile: ${profile.name} (${profile.runner ?? 'cli'})` : '',
|
|
798
1384
|
interpretation.coverageSummary ? `Coverage: ${interpretation.coverageSummary}` : '',
|
|
799
1385
|
interpretation.failures && interpretation.failures.length
|
|
800
1386
|
? `Failures:\n${interpretation.failures.map((f) => `- [${f.kind ?? 'issue'}] ${f.message}${f.evidence ? ` (${f.evidence})` : ''}`).join('\n')}`
|
|
801
1387
|
: '',
|
|
1388
|
+
commentResolution
|
|
1389
|
+
? `Comment slugs: resolved ${commentResolution.resolved.length}, reopened ${commentResolution.reopened.length}, open ${commentResolution.open.length}`
|
|
1390
|
+
: '',
|
|
1391
|
+
interpretation.invalidJson && interpretation.rawOutput
|
|
1392
|
+
? `QA agent output (invalid JSON):\n${interpretation.rawOutput.slice(0, 4000)}`
|
|
1393
|
+
: '',
|
|
802
1394
|
result.stdout ? `Stdout:\n${result.stdout.slice(0, 4000)}` : '',
|
|
803
1395
|
result.stderr ? `Stderr:\n${result.stderr.slice(0, 4000)}` : '',
|
|
804
1396
|
artifacts.length ? `Artifacts:\n${artifacts.join('\n')}` : '',
|
|
805
1397
|
followups.length ? `Follow-ups: ${followups.join(', ')}` : '',
|
|
806
1398
|
].filter(Boolean);
|
|
807
1399
|
if (!this.dryRunGuard) {
|
|
1400
|
+
const category = outcome === 'pass' ? 'qa_result' : 'qa_issue';
|
|
1401
|
+
const summaryMessage = bodyLines.join('\n\n');
|
|
1402
|
+
const summarySlug = createTaskCommentSlug({
|
|
1403
|
+
source: 'qa-tasks',
|
|
1404
|
+
message: summaryMessage,
|
|
1405
|
+
category,
|
|
1406
|
+
});
|
|
1407
|
+
const status = outcome === 'pass' ? 'resolved' : 'open';
|
|
1408
|
+
const summaryBody = formatTaskCommentBody({
|
|
1409
|
+
slug: summarySlug,
|
|
1410
|
+
source: 'qa-tasks',
|
|
1411
|
+
message: summaryMessage,
|
|
1412
|
+
status,
|
|
1413
|
+
category,
|
|
1414
|
+
});
|
|
1415
|
+
const createdAt = new Date().toISOString();
|
|
808
1416
|
await this.deps.workspaceRepo.createTaskComment({
|
|
809
1417
|
taskId: task.task.id,
|
|
810
1418
|
taskRunId: taskRun.id,
|
|
811
1419
|
jobId: ctx.jobId,
|
|
812
1420
|
sourceCommand: 'qa-tasks',
|
|
813
1421
|
authorType: 'agent',
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1422
|
+
authorAgentId: interpretation.agentId ?? null,
|
|
1423
|
+
category,
|
|
1424
|
+
slug: summarySlug,
|
|
1425
|
+
status,
|
|
1426
|
+
body: summaryBody,
|
|
1427
|
+
createdAt,
|
|
1428
|
+
resolvedAt: status === 'resolved' ? createdAt : null,
|
|
1429
|
+
resolvedBy: status === 'resolved' ? interpretation.agentId ?? null : null,
|
|
817
1430
|
metadata: {
|
|
818
1431
|
...(artifacts.length ? { artifacts } : {}),
|
|
819
1432
|
...(qaRun?.id ? { qaRunId: qaRun.id } : {}),
|
|
820
1433
|
},
|
|
821
1434
|
});
|
|
822
1435
|
}
|
|
1436
|
+
const ratingTokens = (interpretation.tokensPrompt ?? 0) + (interpretation.tokensCompletion ?? 0);
|
|
1437
|
+
if (ctx.request.rateAgents && interpretation.agentId && ratingTokens > 0) {
|
|
1438
|
+
try {
|
|
1439
|
+
const ratingService = this.ensureRatingService();
|
|
1440
|
+
await ratingService.rate({
|
|
1441
|
+
workspace: this.workspace,
|
|
1442
|
+
agentId: interpretation.agentId,
|
|
1443
|
+
commandName: 'qa-tasks',
|
|
1444
|
+
jobId: ctx.jobId,
|
|
1445
|
+
commandRunId: ctx.commandRunId,
|
|
1446
|
+
taskId: task.task.id,
|
|
1447
|
+
taskKey: task.task.key,
|
|
1448
|
+
discipline: task.task.type ?? undefined,
|
|
1449
|
+
complexity: this.resolveTaskComplexity(task.task),
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
catch (error) {
|
|
1453
|
+
const message = `Agent rating failed for ${task.task.key}: ${error instanceof Error ? error.message : String(error)}`;
|
|
1454
|
+
ctx.warnings?.push(message);
|
|
1455
|
+
try {
|
|
1456
|
+
await this.logTask(taskRun.id, message, 'rating');
|
|
1457
|
+
}
|
|
1458
|
+
catch {
|
|
1459
|
+
/* ignore rating log failures */
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
823
1463
|
return {
|
|
824
1464
|
taskKey: task.task.key,
|
|
825
1465
|
outcome,
|
|
@@ -881,6 +1521,12 @@ export class QaTasksService {
|
|
|
881
1521
|
}
|
|
882
1522
|
const interactive = ctx.request.createFollowupTasks === 'prompt' && process.stdout.isTTY;
|
|
883
1523
|
for (const suggestion of suggestions) {
|
|
1524
|
+
const followupSlug = this.buildFollowupSlug(task.task, suggestion);
|
|
1525
|
+
const existing = await this.deps.workspaceRepo.listTasksByMetadataValue(task.task.projectId, 'qa_followup_slug', followupSlug);
|
|
1526
|
+
if (existing.length) {
|
|
1527
|
+
await this.logTask(taskRun.id, `Skipped follow-up ${followupSlug}; already exists: ${existing.map((item) => item.key).join(', ')}`, 'qa-followup');
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
884
1530
|
let proceed = ctx.request.createFollowupTasks === 'auto' || ctx.request.createFollowupTasks === undefined;
|
|
885
1531
|
if (interactive) {
|
|
886
1532
|
const rl = readline.createInterface({ input, output });
|
|
@@ -891,7 +1537,7 @@ export class QaTasksService {
|
|
|
891
1537
|
if (!proceed)
|
|
892
1538
|
continue;
|
|
893
1539
|
try {
|
|
894
|
-
const created = await this.followupService.createFollowupTask({ ...task.task, storyKey: task.task.storyKey, epicKey: task.task.epicKey }, suggestion);
|
|
1540
|
+
const created = await this.followupService.createFollowupTask({ ...task.task, storyKey: task.task.storyKey, epicKey: task.task.epicKey }, { ...suggestion, followupSlug });
|
|
895
1541
|
followups.push(created.task.key);
|
|
896
1542
|
}
|
|
897
1543
|
catch (error) {
|
|
@@ -909,15 +1555,30 @@ export class QaTasksService {
|
|
|
909
1555
|
.filter(Boolean)
|
|
910
1556
|
.join('\n');
|
|
911
1557
|
if (!ctx.request.dryRun) {
|
|
1558
|
+
const category = result === 'pass' ? 'qa_result' : 'qa_issue';
|
|
1559
|
+
const status = result === 'pass' ? 'resolved' : 'open';
|
|
1560
|
+
const slug = createTaskCommentSlug({ source: 'qa-tasks', message: body, category });
|
|
1561
|
+
const formattedBody = formatTaskCommentBody({
|
|
1562
|
+
slug,
|
|
1563
|
+
source: 'qa-tasks',
|
|
1564
|
+
message: body,
|
|
1565
|
+
status,
|
|
1566
|
+
category,
|
|
1567
|
+
});
|
|
1568
|
+
const createdAt = new Date().toISOString();
|
|
912
1569
|
await this.deps.workspaceRepo.createTaskComment({
|
|
913
1570
|
taskId: task.task.id,
|
|
914
1571
|
taskRunId: taskRun.id,
|
|
915
1572
|
jobId: ctx.jobId,
|
|
916
1573
|
sourceCommand: 'qa-tasks',
|
|
917
1574
|
authorType: 'human',
|
|
918
|
-
category
|
|
919
|
-
|
|
920
|
-
|
|
1575
|
+
category,
|
|
1576
|
+
slug,
|
|
1577
|
+
status,
|
|
1578
|
+
body: formattedBody,
|
|
1579
|
+
createdAt,
|
|
1580
|
+
resolvedAt: status === 'resolved' ? createdAt : null,
|
|
1581
|
+
resolvedBy: status === 'resolved' ? 'human' : null,
|
|
921
1582
|
metadata: {
|
|
922
1583
|
...(ctx.request.evidenceUrl ? { evidence: ctx.request.evidenceUrl } : {}),
|
|
923
1584
|
...(artifacts.length ? { artifacts } : {}),
|
|
@@ -949,10 +1610,25 @@ export class QaTasksService {
|
|
|
949
1610
|
taskKeys: effectiveTasks,
|
|
950
1611
|
statusFilter: effectiveStatus,
|
|
951
1612
|
});
|
|
1613
|
+
const abortSignal = request.abortSignal;
|
|
1614
|
+
const resolveAbortReason = () => {
|
|
1615
|
+
const reason = abortSignal?.reason;
|
|
1616
|
+
if (typeof reason === "string" && reason.trim().length > 0)
|
|
1617
|
+
return reason;
|
|
1618
|
+
if (reason instanceof Error && reason.message)
|
|
1619
|
+
return reason.message;
|
|
1620
|
+
return "qa_tasks_aborted";
|
|
1621
|
+
};
|
|
1622
|
+
const abortIfSignaled = () => {
|
|
1623
|
+
if (abortSignal?.aborted) {
|
|
1624
|
+
throw new Error(resolveAbortReason());
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
952
1627
|
this.dryRunGuard = request.dryRun ?? false;
|
|
953
1628
|
if (request.dryRun) {
|
|
954
1629
|
const dryResults = [];
|
|
955
1630
|
for (const task of selection.ordered) {
|
|
1631
|
+
abortIfSignaled();
|
|
956
1632
|
let profile;
|
|
957
1633
|
try {
|
|
958
1634
|
profile = await this.profileService.resolveProfileForTask(task.task, {
|
|
@@ -1062,6 +1738,7 @@ export class QaTasksService {
|
|
|
1062
1738
|
blocked: selection.blocked.map((t) => t.task.key),
|
|
1063
1739
|
completedTaskKeys: Array.from(completedKeys),
|
|
1064
1740
|
});
|
|
1741
|
+
const warnings = [...selection.warnings];
|
|
1065
1742
|
const results = [];
|
|
1066
1743
|
for (const task of selection.ordered) {
|
|
1067
1744
|
if (completedKeys.has(task.task.key)) {
|
|
@@ -1072,12 +1749,13 @@ export class QaTasksService {
|
|
|
1072
1749
|
try {
|
|
1073
1750
|
let processedCount = completedKeys.size;
|
|
1074
1751
|
for (const [index, task] of filteredRemaining.entries()) {
|
|
1752
|
+
abortIfSignaled();
|
|
1075
1753
|
const mode = request.mode ?? 'auto';
|
|
1076
1754
|
if (mode === 'manual') {
|
|
1077
1755
|
results.push(await this.runManual(task, { jobId: job.id, commandRunId: commandRun.id, request }));
|
|
1078
1756
|
}
|
|
1079
1757
|
else {
|
|
1080
|
-
results.push(await this.runAuto(task, { jobId: job.id, commandRunId: commandRun.id, request }));
|
|
1758
|
+
results.push(await this.runAuto(task, { jobId: job.id, commandRunId: commandRun.id, request, warnings }));
|
|
1081
1759
|
}
|
|
1082
1760
|
completedKeys.add(task.task.key);
|
|
1083
1761
|
processedCount = completedKeys.size;
|
|
@@ -1111,7 +1789,7 @@ export class QaTasksService {
|
|
|
1111
1789
|
commandRunId: commandRun.id,
|
|
1112
1790
|
selection,
|
|
1113
1791
|
results,
|
|
1114
|
-
warnings
|
|
1792
|
+
warnings,
|
|
1115
1793
|
};
|
|
1116
1794
|
}
|
|
1117
1795
|
}
|