@mcoda/core 0.1.21 → 0.1.23
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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
- package/dist/services/backlog/TaskOrderingService.js +75 -30
- package/dist/services/execution/AddTestsService.d.ts +48 -0
- package/dist/services/execution/AddTestsService.d.ts.map +1 -0
- package/dist/services/execution/AddTestsService.js +346 -0
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +64 -4
- package/package.json +6 -6
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./services/planning/RefineTasksService.js";
|
|
|
9
9
|
export * from "./services/planning/KeyHelpers.js";
|
|
10
10
|
export * from "./services/execution/TaskSelectionService.js";
|
|
11
11
|
export * from "./services/execution/TaskStateService.js";
|
|
12
|
+
export * from "./services/execution/AddTestsService.js";
|
|
12
13
|
export * from "./services/execution/WorkOnTasksService.js";
|
|
13
14
|
export * from "./services/execution/QaTasksService.js";
|
|
14
15
|
export * from "./services/execution/GatewayTrioService.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sCAAsC,CAAC;AACrD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,mCAAmC,CAAC;AAClD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,0CAA0C,CAAC;AACzD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,wCAAwC,CAAC;AACvD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,wCAAwC,CAAC;AACvD,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sCAAsC,CAAC;AACrD,cAAc,wCAAwC,CAAC;AACvD,cAAc,wCAAwC,CAAC;AACvD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,0CAA0C,CAAC;AACzD,cAAc,uCAAuC,CAAC;AACtD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,qCAAqC,CAAC;AACpD,cAAc,0CAA0C,CAAC;AACzD,cAAc,qCAAqC,CAAC;AACpD,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,sCAAsC,CAAC;AACrD,cAAc,iCAAiC,CAAC;AAChD,cAAc,0CAA0C,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sCAAsC,CAAC;AACrD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,mCAAmC,CAAC;AAClD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,0CAA0C,CAAC;AACzD,cAAc,yCAAyC,CAAC;AACxD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,wCAAwC,CAAC;AACvD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,wCAAwC,CAAC;AACvD,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sCAAsC,CAAC;AACrD,cAAc,wCAAwC,CAAC;AACvD,cAAc,wCAAwC,CAAC;AACvD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,0CAA0C,CAAC;AACzD,cAAc,uCAAuC,CAAC;AACtD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,qCAAqC,CAAC;AACpD,cAAc,0CAA0C,CAAC;AACzD,cAAc,qCAAqC,CAAC;AACpD,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,sCAAsC,CAAC;AACrD,cAAc,iCAAiC,CAAC;AAChD,cAAc,0CAA0C,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./services/planning/RefineTasksService.js";
|
|
|
9
9
|
export * from "./services/planning/KeyHelpers.js";
|
|
10
10
|
export * from "./services/execution/TaskSelectionService.js";
|
|
11
11
|
export * from "./services/execution/TaskStateService.js";
|
|
12
|
+
export * from "./services/execution/AddTestsService.js";
|
|
12
13
|
export * from "./services/execution/WorkOnTasksService.js";
|
|
13
14
|
export * from "./services/execution/QaTasksService.js";
|
|
14
15
|
export * from "./services/execution/GatewayTrioService.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TaskOrderingService.d.ts","sourceRoot":"","sources":["../../../src/services/backlog/TaskOrderingService.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAG1E,OAAO,EAAgB,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAwEtE,KAAK,qBAAqB,GAAG,aAAa,GAAG,aAAa,GAAG,wBAAwB,CAAC;AAEtF,UAAU,UAAU;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,UAAU,OAAO;IACf,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;
|
|
1
|
+
{"version":3,"file":"TaskOrderingService.d.ts","sourceRoot":"","sources":["../../../src/services/backlog/TaskOrderingService.ts"],"names":[],"mappings":"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"}
|
|
@@ -472,6 +472,7 @@ export class TaskOrderingService {
|
|
|
472
472
|
SELECT
|
|
473
473
|
td.task_id,
|
|
474
474
|
td.depends_on_task_id,
|
|
475
|
+
td.relation_type,
|
|
475
476
|
dep.key as depends_on_key,
|
|
476
477
|
dep.status as depends_on_status
|
|
477
478
|
FROM task_dependencies td
|
|
@@ -646,47 +647,81 @@ export class TaskOrderingService {
|
|
|
646
647
|
return false;
|
|
647
648
|
}
|
|
648
649
|
async injectFoundationDependencies(tasks, deps, warnings, persist) {
|
|
650
|
+
const relationType = "inferred_foundation";
|
|
649
651
|
const classification = new Map();
|
|
650
652
|
for (const task of tasks) {
|
|
651
653
|
classification.set(task.id, this.resolveClassification(task));
|
|
652
654
|
}
|
|
653
655
|
const foundationTasks = tasks.filter((task) => classification.get(task.id)?.foundation);
|
|
654
656
|
const nonFoundationTasks = tasks.filter((task) => !classification.get(task.id)?.foundation);
|
|
655
|
-
|
|
656
|
-
|
|
657
|
+
let removedInferred = 0;
|
|
658
|
+
for (const task of tasks) {
|
|
659
|
+
const rows = deps.get(task.id) ?? [];
|
|
660
|
+
if (rows.length === 0)
|
|
661
|
+
continue;
|
|
662
|
+
const kept = rows.filter((row) => row.relation_type?.toLowerCase() !== relationType);
|
|
663
|
+
removedInferred += rows.length - kept.length;
|
|
664
|
+
deps.set(task.id, kept);
|
|
665
|
+
}
|
|
666
|
+
const foundationsByStory = new Map();
|
|
667
|
+
const foundationsByEpic = new Map();
|
|
668
|
+
for (const foundation of foundationTasks) {
|
|
669
|
+
const storyEntries = foundationsByStory.get(foundation.story_id) ?? [];
|
|
670
|
+
storyEntries.push(foundation);
|
|
671
|
+
foundationsByStory.set(foundation.story_id, storyEntries);
|
|
672
|
+
const epicEntries = foundationsByEpic.get(foundation.epic_id) ?? [];
|
|
673
|
+
epicEntries.push(foundation);
|
|
674
|
+
foundationsByEpic.set(foundation.epic_id, epicEntries);
|
|
675
|
+
}
|
|
676
|
+
const byRank = (a, b) => {
|
|
677
|
+
const priorityA = a.priority ?? Number.MAX_SAFE_INTEGER;
|
|
678
|
+
const priorityB = b.priority ?? Number.MAX_SAFE_INTEGER;
|
|
679
|
+
if (priorityA !== priorityB)
|
|
680
|
+
return priorityA - priorityB;
|
|
681
|
+
const createdA = Date.parse(a.created_at) || 0;
|
|
682
|
+
const createdB = Date.parse(b.created_at) || 0;
|
|
683
|
+
if (createdA !== createdB)
|
|
684
|
+
return createdA - createdB;
|
|
685
|
+
return a.key.localeCompare(b.key);
|
|
686
|
+
};
|
|
687
|
+
for (const list of foundationsByStory.values())
|
|
688
|
+
list.sort(byRank);
|
|
689
|
+
for (const list of foundationsByEpic.values())
|
|
690
|
+
list.sort(byRank);
|
|
657
691
|
const taskById = new Map(tasks.map((task) => [task.id, task]));
|
|
658
692
|
const dependencyGraph = this.buildDependencyGraph(tasks, deps);
|
|
659
693
|
const inserts = [];
|
|
660
694
|
let skippedCycles = 0;
|
|
661
695
|
const skippedEdges = [];
|
|
662
696
|
for (const task of nonFoundationTasks) {
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
skippedEdges.push(`${task.key}->${foundation.key}`);
|
|
675
|
-
}
|
|
676
|
-
continue;
|
|
697
|
+
const existingDeps = (deps.get(task.id) ?? []).filter((dep) => Boolean(dep.depends_on_task_id));
|
|
698
|
+
if (existingDeps.length > 0)
|
|
699
|
+
continue;
|
|
700
|
+
const scopedCandidates = foundationsByStory.get(task.story_id) ?? foundationsByEpic.get(task.epic_id) ?? [];
|
|
701
|
+
const foundation = scopedCandidates.find((candidate) => candidate.id !== task.id);
|
|
702
|
+
if (!foundation)
|
|
703
|
+
continue;
|
|
704
|
+
if (this.hasDependencyPath(dependencyGraph, foundation.id, task.id)) {
|
|
705
|
+
skippedCycles += 1;
|
|
706
|
+
if (skippedEdges.length < 5) {
|
|
707
|
+
skippedEdges.push(`${task.key}->${foundation.key}`);
|
|
677
708
|
}
|
|
678
|
-
|
|
679
|
-
taskId: task.id,
|
|
680
|
-
dependsOnTaskId: foundation.id,
|
|
681
|
-
relationType: "inferred_foundation",
|
|
682
|
-
});
|
|
683
|
-
existing.add(foundation.id);
|
|
684
|
-
const edges = dependencyGraph.get(task.id) ?? new Set();
|
|
685
|
-
edges.add(foundation.id);
|
|
686
|
-
dependencyGraph.set(task.id, edges);
|
|
709
|
+
continue;
|
|
687
710
|
}
|
|
711
|
+
inserts.push({
|
|
712
|
+
taskId: task.id,
|
|
713
|
+
dependsOnTaskId: foundation.id,
|
|
714
|
+
relationType,
|
|
715
|
+
});
|
|
716
|
+
const edges = dependencyGraph.get(task.id) ?? new Set();
|
|
717
|
+
edges.add(foundation.id);
|
|
718
|
+
dependencyGraph.set(task.id, edges);
|
|
688
719
|
}
|
|
689
|
-
if (
|
|
720
|
+
if (persist && removedInferred > 0) {
|
|
721
|
+
const placeholders = tasks.map(() => "?").join(", ");
|
|
722
|
+
await this.db.run(`DELETE FROM task_dependencies WHERE relation_type = ? AND task_id IN (${placeholders})`, relationType, ...tasks.map((task) => task.id));
|
|
723
|
+
}
|
|
724
|
+
if (inserts.length === 0 && removedInferred === 0) {
|
|
690
725
|
if (skippedCycles > 0) {
|
|
691
726
|
warnings.push(`Skipped ${skippedCycles} inferred foundation deps due to cycles.`);
|
|
692
727
|
if (skippedEdges.length > 0) {
|
|
@@ -704,16 +739,25 @@ export class TaskOrderingService {
|
|
|
704
739
|
depList.push({
|
|
705
740
|
task_id: insert.taskId,
|
|
706
741
|
depends_on_task_id: insert.dependsOnTaskId,
|
|
742
|
+
relation_type: relationType,
|
|
707
743
|
depends_on_key: dependsOn?.key,
|
|
708
744
|
depends_on_status: dependsOn?.status,
|
|
709
745
|
});
|
|
710
746
|
deps.set(insert.taskId, depList);
|
|
711
747
|
}
|
|
712
|
-
if (persist) {
|
|
713
|
-
warnings.push(`
|
|
748
|
+
if (persist && removedInferred > 0) {
|
|
749
|
+
warnings.push(`Reconciled ${removedInferred} inferred foundation deps before re-inference.`);
|
|
714
750
|
}
|
|
715
|
-
else {
|
|
716
|
-
warnings.push(`Dry run:
|
|
751
|
+
else if (!persist && removedInferred > 0) {
|
|
752
|
+
warnings.push(`Dry run: would reconcile ${removedInferred} inferred foundation deps.`);
|
|
753
|
+
}
|
|
754
|
+
if (inserts.length > 0) {
|
|
755
|
+
if (persist) {
|
|
756
|
+
warnings.push(`Injected ${inserts.length} scoped inferred foundation deps.`);
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
warnings.push(`Dry run: inferred ${inserts.length} scoped foundation deps (not persisted).`);
|
|
760
|
+
}
|
|
717
761
|
}
|
|
718
762
|
if (skippedCycles > 0) {
|
|
719
763
|
warnings.push(`Skipped ${skippedCycles} inferred foundation deps due to cycles.`);
|
|
@@ -782,6 +826,7 @@ export class TaskOrderingService {
|
|
|
782
826
|
depList.push({
|
|
783
827
|
task_id: insert.taskId,
|
|
784
828
|
depends_on_task_id: insert.dependsOnTaskId,
|
|
829
|
+
relation_type: "inferred_agent",
|
|
785
830
|
depends_on_key: dependsOn?.key,
|
|
786
831
|
depends_on_status: dependsOn?.status,
|
|
787
832
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { WorkspaceRepository } from "@mcoda/db";
|
|
2
|
+
import { VcsClient } from "@mcoda/integrations";
|
|
3
|
+
import { WorkspaceResolution } from "../../workspace/WorkspaceManager.js";
|
|
4
|
+
import { TaskSelectionFilters, TaskSelectionService } from "./TaskSelectionService.js";
|
|
5
|
+
type AddTestsDeps = {
|
|
6
|
+
workspaceRepo: WorkspaceRepository;
|
|
7
|
+
selectionService: TaskSelectionService;
|
|
8
|
+
vcsClient?: VcsClient;
|
|
9
|
+
};
|
|
10
|
+
export interface AddTestsRequest extends TaskSelectionFilters {
|
|
11
|
+
projectKey: string;
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
commit?: boolean;
|
|
14
|
+
baseBranch?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface AddTestsResult {
|
|
17
|
+
projectKey: string;
|
|
18
|
+
selectedTaskKeys: string[];
|
|
19
|
+
tasksRequiringTests: string[];
|
|
20
|
+
updatedTaskKeys: string[];
|
|
21
|
+
skippedTaskKeys: string[];
|
|
22
|
+
createdFiles: string[];
|
|
23
|
+
runAllScriptPath?: string;
|
|
24
|
+
runAllCommand?: string;
|
|
25
|
+
branch?: string;
|
|
26
|
+
commitSha?: string;
|
|
27
|
+
warnings: string[];
|
|
28
|
+
}
|
|
29
|
+
export declare class AddTestsService {
|
|
30
|
+
private workspace;
|
|
31
|
+
private readonly workspaceRepo;
|
|
32
|
+
private readonly selectionService;
|
|
33
|
+
private readonly vcs;
|
|
34
|
+
private readonly ownsWorkspaceRepo;
|
|
35
|
+
private readonly ownsSelectionService;
|
|
36
|
+
constructor(workspace: WorkspaceResolution, deps: AddTestsDeps, ownership?: {
|
|
37
|
+
ownsWorkspaceRepo?: boolean;
|
|
38
|
+
ownsSelectionService?: boolean;
|
|
39
|
+
});
|
|
40
|
+
static create(workspace: WorkspaceResolution): Promise<AddTestsService>;
|
|
41
|
+
close(): Promise<void>;
|
|
42
|
+
private resolveTaskSelection;
|
|
43
|
+
private resolveTaskCommands;
|
|
44
|
+
private ensureBaseBranchForCommit;
|
|
45
|
+
addTests(request: AddTestsRequest): Promise<AddTestsResult>;
|
|
46
|
+
}
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=AddTestsService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AddTestsService.d.ts","sourceRoot":"","sources":["../../../src/services/execution/AddTestsService.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAE1E,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAA0B,MAAM,2BAA2B,CAAC;AAa/G,KAAK,YAAY,GAAG;IAClB,aAAa,EAAE,mBAAmB,CAAC;IACnC,gBAAgB,EAAE,oBAAoB,CAAC;IACvC,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB,CAAC;AAEF,MAAM,WAAW,eAAgB,SAAQ,oBAAoB;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA+ID,qBAAa,eAAe;IAQxB,OAAO,CAAC,SAAS;IAPnB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsB;IACpD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAuB;IACxD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAY;IAChC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAU;gBAGrC,SAAS,EAAE,mBAAmB,EACtC,IAAI,EAAE,YAAY,EAClB,SAAS,GAAE;QAAE,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAAC,oBAAoB,CAAC,EAAE,OAAO,CAAA;KAAO;WASpE,MAAM,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,CAAC;IAUvE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAUd,oBAAoB;YAqBpB,mBAAmB;YAoBnB,yBAAyB;IAqBjC,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC;CAoIlE"}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { WorkspaceRepository } from "@mcoda/db";
|
|
4
|
+
import { VcsClient } from "@mcoda/integrations";
|
|
5
|
+
import { PathHelper, WORK_ALLOWED_STATUSES, filterTaskStatuses } from "@mcoda/shared";
|
|
6
|
+
import { QaTestCommandBuilder } from "./QaTestCommandBuilder.js";
|
|
7
|
+
import { TaskSelectionService } from "./TaskSelectionService.js";
|
|
8
|
+
const DEFAULT_BASE_BRANCH = "mcoda-dev";
|
|
9
|
+
const MISSING_HARNESS_BLOCKER = /No runnable test harness discovered/i;
|
|
10
|
+
const FALLBACK_BOOTSTRAP_COMMAND = "node -e \"console.log('mcoda add-tests bootstrap placeholder')\"";
|
|
11
|
+
const normalizeStringArray = (value) => {
|
|
12
|
+
if (!Array.isArray(value))
|
|
13
|
+
return [];
|
|
14
|
+
return value
|
|
15
|
+
.filter((item) => typeof item === "string")
|
|
16
|
+
.map((item) => item.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
};
|
|
19
|
+
const normalizeTestCommands = (value) => {
|
|
20
|
+
if (typeof value === "string") {
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
return trimmed ? [trimmed] : [];
|
|
23
|
+
}
|
|
24
|
+
return normalizeStringArray(value);
|
|
25
|
+
};
|
|
26
|
+
const normalizeTestRequirements = (value) => {
|
|
27
|
+
const raw = value && typeof value === "object" ? value : {};
|
|
28
|
+
return {
|
|
29
|
+
unit: normalizeStringArray(raw.unit),
|
|
30
|
+
component: normalizeStringArray(raw.component),
|
|
31
|
+
integration: normalizeStringArray(raw.integration),
|
|
32
|
+
api: normalizeStringArray(raw.api),
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const hasTestRequirements = (requirements) => requirements.unit.length > 0 ||
|
|
36
|
+
requirements.component.length > 0 ||
|
|
37
|
+
requirements.integration.length > 0 ||
|
|
38
|
+
requirements.api.length > 0;
|
|
39
|
+
const dedupeCommands = (commands) => {
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const result = [];
|
|
42
|
+
for (const command of commands) {
|
|
43
|
+
const trimmed = command.trim();
|
|
44
|
+
if (!trimmed || seen.has(trimmed))
|
|
45
|
+
continue;
|
|
46
|
+
seen.add(trimmed);
|
|
47
|
+
result.push(trimmed);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
const resolveNodeCommand = () => {
|
|
52
|
+
const override = process.env.NODE_BIN?.trim();
|
|
53
|
+
const resolved = override || (process.platform === "win32" ? "node.exe" : "node");
|
|
54
|
+
return resolved.includes(" ") ? `"${resolved}"` : resolved;
|
|
55
|
+
};
|
|
56
|
+
const quoteShellPath = (value) => (value.includes(" ") ? `"${value}"` : value);
|
|
57
|
+
const buildRunAllTestsCommand = (relativePath) => {
|
|
58
|
+
const normalized = relativePath.split(path.sep).join("/");
|
|
59
|
+
if (normalized.endsWith(".js"))
|
|
60
|
+
return `${resolveNodeCommand()} ${normalized}`;
|
|
61
|
+
if (normalized.endsWith(".ps1")) {
|
|
62
|
+
const shell = process.platform === "win32" ? "powershell" : "pwsh";
|
|
63
|
+
return `${shell} -File ${quoteShellPath(normalized)}`;
|
|
64
|
+
}
|
|
65
|
+
if (normalized.endsWith(".sh"))
|
|
66
|
+
return `bash ${quoteShellPath(normalized)}`;
|
|
67
|
+
if (normalized.startsWith("."))
|
|
68
|
+
return normalized;
|
|
69
|
+
return `./${normalized}`;
|
|
70
|
+
};
|
|
71
|
+
const detectRunAllTestsScript = (workspaceRoot) => {
|
|
72
|
+
const candidates = ["tests/all.js", "tests/all.sh", "tests/all.ps1", "tests/all"];
|
|
73
|
+
for (const candidate of candidates) {
|
|
74
|
+
if (fs.existsSync(path.join(workspaceRoot, ...candidate.split("/"))))
|
|
75
|
+
return candidate;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
};
|
|
79
|
+
const detectRunAllTestsCommand = (workspaceRoot) => {
|
|
80
|
+
const script = detectRunAllTestsScript(workspaceRoot);
|
|
81
|
+
if (!script)
|
|
82
|
+
return undefined;
|
|
83
|
+
return buildRunAllTestsCommand(script);
|
|
84
|
+
};
|
|
85
|
+
const pickSeedTestCategory = (requirements) => {
|
|
86
|
+
const order = ["unit", "component", "integration", "api"];
|
|
87
|
+
const active = order.filter((key) => requirements[key].length > 0);
|
|
88
|
+
if (active.length === 1)
|
|
89
|
+
return active[0];
|
|
90
|
+
return "unit";
|
|
91
|
+
};
|
|
92
|
+
const buildRunAllTestsScript = (seedCategory, seedCommands) => {
|
|
93
|
+
const suites = {
|
|
94
|
+
unit: [],
|
|
95
|
+
component: [],
|
|
96
|
+
integration: [],
|
|
97
|
+
api: [],
|
|
98
|
+
};
|
|
99
|
+
suites[seedCategory] = seedCommands;
|
|
100
|
+
return [
|
|
101
|
+
"#!/usr/bin/env node",
|
|
102
|
+
'const { spawnSync } = require("node:child_process");',
|
|
103
|
+
"",
|
|
104
|
+
"// Register test commands per discipline.",
|
|
105
|
+
`const testSuites = ${JSON.stringify(suites, null, 2)};`,
|
|
106
|
+
"",
|
|
107
|
+
'const entries = Object.entries(testSuites).flatMap(([label, commands]) =>',
|
|
108
|
+
" commands.map((command) => ({ label, command }))",
|
|
109
|
+
");",
|
|
110
|
+
"if (!entries.length) {",
|
|
111
|
+
' console.error("No test commands registered in tests/all.js. Add unit/component/integration/api commands.");',
|
|
112
|
+
" process.exit(1);",
|
|
113
|
+
"}",
|
|
114
|
+
"",
|
|
115
|
+
'console.log("MCODA_RUN_ALL_TESTS_START");',
|
|
116
|
+
"let failed = false;",
|
|
117
|
+
"for (const entry of entries) {",
|
|
118
|
+
" const result = spawnSync(entry.command, { shell: true, stdio: \"inherit\" });",
|
|
119
|
+
" const status = typeof result.status === \"number\" ? result.status : 1;",
|
|
120
|
+
" if (status !== 0) failed = true;",
|
|
121
|
+
"}",
|
|
122
|
+
'console.log(`MCODA_RUN_ALL_TESTS_COMPLETE status=${failed ? "failed" : "passed"}`);',
|
|
123
|
+
'console.log("MCODA_RUN_ALL_TESTS_END");',
|
|
124
|
+
"process.exit(failed ? 1 : 0);",
|
|
125
|
+
"",
|
|
126
|
+
].join("\n");
|
|
127
|
+
};
|
|
128
|
+
const stripHarnessBlocker = (metadata) => {
|
|
129
|
+
const rawQa = metadata.qa;
|
|
130
|
+
if (!rawQa || typeof rawQa !== "object")
|
|
131
|
+
return metadata;
|
|
132
|
+
const qa = { ...rawQa };
|
|
133
|
+
const blockers = normalizeStringArray(qa.blockers).filter((entry) => !MISSING_HARNESS_BLOCKER.test(entry));
|
|
134
|
+
if (blockers.length > 0) {
|
|
135
|
+
qa.blockers = blockers;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
delete qa.blockers;
|
|
139
|
+
}
|
|
140
|
+
return { ...metadata, qa };
|
|
141
|
+
};
|
|
142
|
+
export class AddTestsService {
|
|
143
|
+
constructor(workspace, deps, ownership = {}) {
|
|
144
|
+
this.workspace = workspace;
|
|
145
|
+
this.workspaceRepo = deps.workspaceRepo;
|
|
146
|
+
this.selectionService = deps.selectionService;
|
|
147
|
+
this.vcs = deps.vcsClient ?? new VcsClient();
|
|
148
|
+
this.ownsWorkspaceRepo = ownership.ownsWorkspaceRepo === true;
|
|
149
|
+
this.ownsSelectionService = ownership.ownsSelectionService === true;
|
|
150
|
+
}
|
|
151
|
+
static async create(workspace) {
|
|
152
|
+
const workspaceRepo = await WorkspaceRepository.create(workspace.workspaceRoot);
|
|
153
|
+
const selectionService = new TaskSelectionService(workspace, workspaceRepo);
|
|
154
|
+
return new AddTestsService(workspace, { workspaceRepo, selectionService }, { ownsWorkspaceRepo: true, ownsSelectionService: true });
|
|
155
|
+
}
|
|
156
|
+
async close() {
|
|
157
|
+
if (this.ownsSelectionService) {
|
|
158
|
+
await this.selectionService.close();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (this.ownsWorkspaceRepo) {
|
|
162
|
+
await this.workspaceRepo.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async resolveTaskSelection(request) {
|
|
166
|
+
const ignoreStatusFilter = request.taskKeys?.length ? true : request.ignoreStatusFilter;
|
|
167
|
+
const { filtered } = ignoreStatusFilter
|
|
168
|
+
? { filtered: request.statusFilter ?? [] }
|
|
169
|
+
: filterTaskStatuses(request.statusFilter, WORK_ALLOWED_STATUSES, WORK_ALLOWED_STATUSES);
|
|
170
|
+
return this.selectionService.selectTasks({
|
|
171
|
+
projectKey: request.projectKey,
|
|
172
|
+
epicKey: request.epicKey,
|
|
173
|
+
storyKey: request.storyKey,
|
|
174
|
+
taskKeys: request.taskKeys,
|
|
175
|
+
statusFilter: filtered,
|
|
176
|
+
ignoreStatusFilter,
|
|
177
|
+
includeTypes: request.includeTypes,
|
|
178
|
+
excludeTypes: request.excludeTypes,
|
|
179
|
+
limit: request.limit,
|
|
180
|
+
parallel: request.parallel,
|
|
181
|
+
ignoreDependencies: request.ignoreDependencies ?? true,
|
|
182
|
+
missingContextPolicy: request.missingContextPolicy ?? "allow",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async resolveTaskCommands(task) {
|
|
186
|
+
const metadata = (task.task.metadata ?? {});
|
|
187
|
+
const requirements = normalizeTestRequirements(metadata.test_requirements ?? metadata.testRequirements);
|
|
188
|
+
const existingCommands = dedupeCommands(normalizeTestCommands(metadata.tests ?? metadata.testCommands));
|
|
189
|
+
if (!hasTestRequirements(requirements)) {
|
|
190
|
+
return { requirements, existingCommands, discoveredCommands: [] };
|
|
191
|
+
}
|
|
192
|
+
if (existingCommands.length > 0) {
|
|
193
|
+
return { requirements, existingCommands, discoveredCommands: existingCommands };
|
|
194
|
+
}
|
|
195
|
+
const commandBuilder = new QaTestCommandBuilder(this.workspace.workspaceRoot);
|
|
196
|
+
try {
|
|
197
|
+
const plan = await commandBuilder.build({ task: task.task });
|
|
198
|
+
const discoveredCommands = dedupeCommands(plan.commands);
|
|
199
|
+
return { requirements, existingCommands, discoveredCommands };
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return { requirements, existingCommands, discoveredCommands: [] };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async ensureBaseBranchForCommit(baseBranch) {
|
|
206
|
+
const cwd = this.workspace.workspaceRoot;
|
|
207
|
+
const isRepo = await this.vcs.isRepo(cwd);
|
|
208
|
+
if (!isRepo) {
|
|
209
|
+
return { warning: "add-tests commit skipped: workspace is not a git repository." };
|
|
210
|
+
}
|
|
211
|
+
const status = await this.vcs.status(cwd);
|
|
212
|
+
if (status.trim().length > 0) {
|
|
213
|
+
return { warning: "add-tests commit skipped: working tree is dirty before bootstrap." };
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
await this.vcs.ensureBaseBranch(cwd, baseBranch);
|
|
217
|
+
await this.vcs.checkoutBranch(cwd, baseBranch);
|
|
218
|
+
return { branch: baseBranch };
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
return {
|
|
222
|
+
warning: `add-tests commit skipped: failed to prepare base branch ${baseBranch} (${error.message}).`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async addTests(request) {
|
|
227
|
+
const warnings = [];
|
|
228
|
+
const createdFiles = [];
|
|
229
|
+
const updatedTaskKeys = [];
|
|
230
|
+
const skippedTaskKeys = [];
|
|
231
|
+
const dryRun = request.dryRun === true;
|
|
232
|
+
const commitEnabled = request.commit !== false && !dryRun;
|
|
233
|
+
const selection = await this.resolveTaskSelection(request);
|
|
234
|
+
const selectedTaskKeys = selection.ordered.map((entry) => entry.task.key);
|
|
235
|
+
warnings.push(...selection.warnings);
|
|
236
|
+
const requiringTests = [];
|
|
237
|
+
for (const entry of selection.ordered) {
|
|
238
|
+
const commands = await this.resolveTaskCommands(entry);
|
|
239
|
+
if (!hasTestRequirements(commands.requirements))
|
|
240
|
+
continue;
|
|
241
|
+
requiringTests.push({ entry, commands });
|
|
242
|
+
}
|
|
243
|
+
const tasksRequiringTests = requiringTests.map((item) => item.entry.task.key);
|
|
244
|
+
if (tasksRequiringTests.length === 0) {
|
|
245
|
+
return {
|
|
246
|
+
projectKey: request.projectKey,
|
|
247
|
+
selectedTaskKeys,
|
|
248
|
+
tasksRequiringTests: [],
|
|
249
|
+
updatedTaskKeys,
|
|
250
|
+
skippedTaskKeys,
|
|
251
|
+
createdFiles,
|
|
252
|
+
warnings,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
let runAllScriptPath = detectRunAllTestsScript(this.workspace.workspaceRoot);
|
|
256
|
+
let runAllCommand = runAllScriptPath ? buildRunAllTestsCommand(runAllScriptPath) : undefined;
|
|
257
|
+
let commitBranch;
|
|
258
|
+
let commitSha;
|
|
259
|
+
const baseBranch = (request.baseBranch ?? this.workspace.config?.branch ?? DEFAULT_BASE_BRANCH).trim() || DEFAULT_BASE_BRANCH;
|
|
260
|
+
const seedRequirements = requiringTests[0]?.commands.requirements ?? {
|
|
261
|
+
unit: [],
|
|
262
|
+
component: [],
|
|
263
|
+
integration: [],
|
|
264
|
+
api: [],
|
|
265
|
+
};
|
|
266
|
+
const discoveredSeedCommands = dedupeCommands(requiringTests.flatMap((item) => item.commands.discoveredCommands));
|
|
267
|
+
const seedCommands = discoveredSeedCommands.length > 0 ? discoveredSeedCommands : [FALLBACK_BOOTSTRAP_COMMAND];
|
|
268
|
+
if (!runAllScriptPath) {
|
|
269
|
+
if (!dryRun) {
|
|
270
|
+
if (commitEnabled) {
|
|
271
|
+
const branchPrep = await this.ensureBaseBranchForCommit(baseBranch);
|
|
272
|
+
if (branchPrep.branch) {
|
|
273
|
+
commitBranch = branchPrep.branch;
|
|
274
|
+
}
|
|
275
|
+
else if (branchPrep.warning) {
|
|
276
|
+
warnings.push(branchPrep.warning);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const scriptPath = path.join(this.workspace.workspaceRoot, "tests", "all.js");
|
|
280
|
+
await PathHelper.ensureDir(path.dirname(scriptPath));
|
|
281
|
+
const seedCategory = pickSeedTestCategory(seedRequirements);
|
|
282
|
+
const contents = buildRunAllTestsScript(seedCategory, seedCommands);
|
|
283
|
+
await fs.promises.writeFile(scriptPath, contents, "utf8");
|
|
284
|
+
createdFiles.push("tests/all.js");
|
|
285
|
+
runAllScriptPath = "tests/all.js";
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
warnings.push("Dry-run: add-tests would create tests/all.js.");
|
|
289
|
+
}
|
|
290
|
+
runAllCommand = buildRunAllTestsCommand("tests/all.js");
|
|
291
|
+
if (discoveredSeedCommands.length === 0) {
|
|
292
|
+
warnings.push("No stack-specific test commands were discovered; created a placeholder run-all harness. Replace tests/all.js commands with real suites.");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const item of requiringTests) {
|
|
296
|
+
const metadata = (item.entry.task.metadata ?? {});
|
|
297
|
+
const existingCommands = item.commands.existingCommands;
|
|
298
|
+
const fallbackCommands = item.commands.discoveredCommands.length > 0
|
|
299
|
+
? item.commands.discoveredCommands
|
|
300
|
+
: runAllCommand
|
|
301
|
+
? [runAllCommand]
|
|
302
|
+
: [];
|
|
303
|
+
const resolvedCommands = dedupeCommands(existingCommands.length > 0 ? existingCommands : fallbackCommands);
|
|
304
|
+
if (resolvedCommands.length === 0) {
|
|
305
|
+
skippedTaskKeys.push(item.entry.task.key);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const nextMetadata = stripHarnessBlocker({
|
|
309
|
+
...metadata,
|
|
310
|
+
tests: resolvedCommands,
|
|
311
|
+
testCommands: resolvedCommands,
|
|
312
|
+
});
|
|
313
|
+
const before = JSON.stringify(metadata);
|
|
314
|
+
const after = JSON.stringify(nextMetadata);
|
|
315
|
+
if (before !== after) {
|
|
316
|
+
if (!dryRun) {
|
|
317
|
+
await this.workspaceRepo.updateTask(item.entry.task.id, { metadata: nextMetadata });
|
|
318
|
+
}
|
|
319
|
+
updatedTaskKeys.push(item.entry.task.key);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (commitEnabled && createdFiles.includes("tests/all.js")) {
|
|
323
|
+
try {
|
|
324
|
+
await this.vcs.stage(this.workspace.workspaceRoot, ["tests/all.js"]);
|
|
325
|
+
await this.vcs.commit(this.workspace.workspaceRoot, "chore(mcoda): bootstrap test harness");
|
|
326
|
+
commitSha = await this.vcs.lastCommitSha(this.workspace.workspaceRoot);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
warnings.push(`add-tests commit failed: ${error.message}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
projectKey: request.projectKey,
|
|
334
|
+
selectedTaskKeys,
|
|
335
|
+
tasksRequiringTests,
|
|
336
|
+
updatedTaskKeys,
|
|
337
|
+
skippedTaskKeys,
|
|
338
|
+
createdFiles,
|
|
339
|
+
runAllScriptPath,
|
|
340
|
+
runAllCommand,
|
|
341
|
+
branch: commitBranch,
|
|
342
|
+
commitSha,
|
|
343
|
+
warnings,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkOnTasksService.d.ts","sourceRoot":"","sources":["../../../src/services/execution/WorkOnTasksService.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAgD,MAAM,eAAe,CAAC;AAC3F,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAE9D,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAuB,MAAM,WAAW,CAAC;AAEvF,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAiB,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC1G,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"WorkOnTasksService.d.ts","sourceRoot":"","sources":["../../../src/services/execution/WorkOnTasksService.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,6 +12,7 @@ import { JobService } from "../jobs/JobService.js";
|
|
|
12
12
|
import { TaskSelectionService } from "./TaskSelectionService.js";
|
|
13
13
|
import { TaskStateService } from "./TaskStateService.js";
|
|
14
14
|
import { QaTestCommandBuilder } from "./QaTestCommandBuilder.js";
|
|
15
|
+
import { AddTestsService } from "./AddTestsService.js";
|
|
15
16
|
import { RoutingService } from "../agents/RoutingService.js";
|
|
16
17
|
import { GATEWAY_HANDOFF_ENV_PATH } from "../agents/GatewayHandoff.js";
|
|
17
18
|
import { AgentRatingService } from "../agents/AgentRatingService.js";
|
|
@@ -3689,15 +3690,74 @@ export class WorkOnTasksService {
|
|
|
3689
3690
|
warnings.push(...selection.warnings);
|
|
3690
3691
|
const results = [];
|
|
3691
3692
|
const taskSummaries = new Map();
|
|
3693
|
+
let preflightMissingHarnessTasks = await this.findMissingTestHarnessTasks(selection.ordered);
|
|
3694
|
+
if (preflightMissingHarnessTasks.length > 0 && !request.dryRun) {
|
|
3695
|
+
const bootstrapProjectKey = selection.project?.key ?? request.projectKey;
|
|
3696
|
+
if (!bootstrapProjectKey) {
|
|
3697
|
+
warnings.push("add-tests bootstrap skipped: project key could not be resolved.");
|
|
3698
|
+
}
|
|
3699
|
+
else {
|
|
3700
|
+
try {
|
|
3701
|
+
const addTestsService = new AddTestsService(this.workspace, {
|
|
3702
|
+
workspaceRepo: this.deps.workspaceRepo,
|
|
3703
|
+
selectionService: this.selectionService,
|
|
3704
|
+
vcsClient: this.vcs,
|
|
3705
|
+
});
|
|
3706
|
+
const bootstrap = await addTestsService.addTests({
|
|
3707
|
+
projectKey: bootstrapProjectKey,
|
|
3708
|
+
taskKeys: preflightMissingHarnessTasks.map((issue) => issue.taskKey),
|
|
3709
|
+
ignoreStatusFilter: true,
|
|
3710
|
+
ignoreDependencies: true,
|
|
3711
|
+
dryRun: false,
|
|
3712
|
+
commit: !(request.noCommit ?? false),
|
|
3713
|
+
baseBranch,
|
|
3714
|
+
});
|
|
3715
|
+
if (bootstrap.createdFiles.length > 0) {
|
|
3716
|
+
warnings.push(`add-tests bootstrap created: ${bootstrap.createdFiles.join(", ")}`);
|
|
3717
|
+
}
|
|
3718
|
+
if (bootstrap.commitSha) {
|
|
3719
|
+
warnings.push(`add-tests bootstrap commit: ${bootstrap.commitSha}${bootstrap.branch ? ` on ${bootstrap.branch}` : ""}`);
|
|
3720
|
+
}
|
|
3721
|
+
warnings.push(...bootstrap.warnings.map((warning) => `add-tests: ${warning}`));
|
|
3722
|
+
await this.checkpoint(job.id, "tests_bootstrap", {
|
|
3723
|
+
createdFiles: bootstrap.createdFiles,
|
|
3724
|
+
updatedTaskKeys: bootstrap.updatedTaskKeys,
|
|
3725
|
+
skippedTaskKeys: bootstrap.skippedTaskKeys,
|
|
3726
|
+
commitSha: bootstrap.commitSha ?? null,
|
|
3727
|
+
branch: bootstrap.branch ?? null,
|
|
3728
|
+
});
|
|
3729
|
+
const refreshedRows = await this.deps.workspaceRepo.getTasksByIds(selection.ordered.map((entry) => entry.task.id));
|
|
3730
|
+
const refreshedById = new Map(refreshedRows.map((row) => [row.id, row]));
|
|
3731
|
+
selection = {
|
|
3732
|
+
...selection,
|
|
3733
|
+
ordered: selection.ordered.map((entry) => {
|
|
3734
|
+
const refreshed = refreshedById.get(entry.task.id);
|
|
3735
|
+
if (!refreshed)
|
|
3736
|
+
return entry;
|
|
3737
|
+
return {
|
|
3738
|
+
...entry,
|
|
3739
|
+
task: {
|
|
3740
|
+
...entry.task,
|
|
3741
|
+
metadata: refreshed.metadata,
|
|
3742
|
+
},
|
|
3743
|
+
};
|
|
3744
|
+
}),
|
|
3745
|
+
};
|
|
3746
|
+
}
|
|
3747
|
+
catch (error) {
|
|
3748
|
+
warnings.push(`add-tests bootstrap failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
preflightMissingHarnessTasks = await this.findMissingTestHarnessTasks(selection.ordered);
|
|
3752
|
+
}
|
|
3692
3753
|
if (missingTestsPolicy === "block_job") {
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
const taskKeys = missingHarnessTasks.map((issue) => issue.taskKey);
|
|
3754
|
+
if (preflightMissingHarnessTasks.length > 0) {
|
|
3755
|
+
const taskKeys = preflightMissingHarnessTasks.map((issue) => issue.taskKey);
|
|
3696
3756
|
await this.checkpoint(job.id, "tests_preflight_blocked", {
|
|
3697
3757
|
reason: "missing_test_harness",
|
|
3698
3758
|
policy: missingTestsPolicy,
|
|
3699
3759
|
taskKeys,
|
|
3700
|
-
issues:
|
|
3760
|
+
issues: preflightMissingHarnessTasks.map((issue) => ({
|
|
3701
3761
|
taskKey: issue.taskKey,
|
|
3702
3762
|
testRequirements: issue.testRequirements,
|
|
3703
3763
|
attemptedCommands: issue.attemptedCommands,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcoda/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "Core services and APIs for the mcoda CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@apidevtools/swagger-parser": "^10.1.0",
|
|
34
34
|
"yaml": "^2.4.2",
|
|
35
|
-
"@mcoda/shared": "0.1.
|
|
36
|
-
"@mcoda/db": "0.1.
|
|
37
|
-
"@mcoda/
|
|
38
|
-
"@mcoda/
|
|
39
|
-
"@mcoda/integrations": "0.1.
|
|
35
|
+
"@mcoda/shared": "0.1.23",
|
|
36
|
+
"@mcoda/db": "0.1.23",
|
|
37
|
+
"@mcoda/generators": "0.1.23",
|
|
38
|
+
"@mcoda/agents": "0.1.23",
|
|
39
|
+
"@mcoda/integrations": "0.1.23"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc -p tsconfig.json",
|