@mcoda/core 0.1.20 → 0.1.22

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 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":"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";
@@ -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
+ }
@@ -41,7 +41,7 @@ export interface WorkOnTasksResult {
41
41
  results: TaskExecutionResult[];
42
42
  warnings: string[];
43
43
  }
44
- export type MissingTestsPolicy = "block_job" | "skip_task" | "fail_task";
44
+ export type MissingTestsPolicy = "block_job" | "skip_task" | "fail_task" | "continue_task";
45
45
  export type ExecutionContextPolicy = "best_effort" | "require_any" | "require_sds_or_openapi";
46
46
  export declare class WorkOnTasksService {
47
47
  private workspace;
@@ -1 +1 @@
1
- {"version":3,"file":"WorkOnTasksService.d.ts","sourceRoot":"","sources":["../../../src/services/execution/WorkOnTasksService.ts"],"names":[],"mappings":"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"}
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";
@@ -833,13 +834,22 @@ const splitFileBlocksByExistence = (fileBlocks, cwd) => {
833
834
  }
834
835
  return { existing, remaining };
835
836
  };
836
- const DEFAULT_MISSING_TESTS_POLICY = "block_job";
837
+ const DEFAULT_MISSING_TESTS_POLICY = "continue_task";
837
838
  const DEFAULT_EXECUTION_CONTEXT_POLICY = "best_effort";
838
839
  const normalizeMissingTestsPolicy = (value) => {
839
840
  if (typeof value !== "string")
840
841
  return undefined;
841
842
  const normalized = value.trim().toLowerCase().replace(/-/g, "_");
842
- if (normalized === "block_job" || normalized === "skip_task" || normalized === "fail_task") {
843
+ if (normalized === "block_job" ||
844
+ normalized === "skip_task" ||
845
+ normalized === "fail_task" ||
846
+ normalized === "continue_task" ||
847
+ normalized === "continue" ||
848
+ normalized === "allow" ||
849
+ normalized === "warn_task") {
850
+ if (normalized === "continue" || normalized === "allow" || normalized === "warn_task") {
851
+ return "continue_task";
852
+ }
843
853
  return normalized;
844
854
  }
845
855
  return undefined;
@@ -3489,7 +3499,12 @@ export class WorkOnTasksService {
3489
3499
  const enforceCommentBacklog = isCommentBacklogEnforced();
3490
3500
  const commentBacklogMaxFails = resolveCommentBacklogMaxFails();
3491
3501
  const requestedMissingTestsPolicy = normalizeMissingTestsPolicy(request.missingTestsPolicy);
3492
- const missingTestsPolicy = requestedMissingTestsPolicy ?? (request.allowMissingTests ? "skip_task" : DEFAULT_MISSING_TESTS_POLICY);
3502
+ const missingTestsPolicy = requestedMissingTestsPolicy ??
3503
+ (request.allowMissingTests === true
3504
+ ? "continue_task"
3505
+ : request.allowMissingTests === false
3506
+ ? "block_job"
3507
+ : DEFAULT_MISSING_TESTS_POLICY);
3493
3508
  const requestedExecutionContextPolicy = normalizeExecutionContextPolicy(request.executionContextPolicy);
3494
3509
  const executionContextPolicy = requestedExecutionContextPolicy ?? DEFAULT_EXECUTION_CONTEXT_POLICY;
3495
3510
  const baseCodaliEnvOverrides = codaliRequired ? buildCodaliEnvOverrides() : {};
@@ -3675,15 +3690,74 @@ export class WorkOnTasksService {
3675
3690
  warnings.push(...selection.warnings);
3676
3691
  const results = [];
3677
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
+ }
3678
3753
  if (missingTestsPolicy === "block_job") {
3679
- const missingHarnessTasks = await this.findMissingTestHarnessTasks(selection.ordered);
3680
- if (missingHarnessTasks.length > 0) {
3681
- const taskKeys = missingHarnessTasks.map((issue) => issue.taskKey);
3754
+ if (preflightMissingHarnessTasks.length > 0) {
3755
+ const taskKeys = preflightMissingHarnessTasks.map((issue) => issue.taskKey);
3682
3756
  await this.checkpoint(job.id, "tests_preflight_blocked", {
3683
3757
  reason: "missing_test_harness",
3684
3758
  policy: missingTestsPolicy,
3685
3759
  taskKeys,
3686
- issues: missingHarnessTasks.map((issue) => ({
3760
+ issues: preflightMissingHarnessTasks.map((issue) => ({
3687
3761
  taskKey: issue.taskKey,
3688
3762
  testRequirements: issue.testRequirements,
3689
3763
  attemptedCommands: issue.attemptedCommands,
@@ -4212,18 +4286,26 @@ export class WorkOnTasksService {
4212
4286
  await emitTaskEndOnce();
4213
4287
  throw new Error(`missing_test_harness: ${task.task.key} requires tests but no runnable test commands were found`);
4214
4288
  }
4215
- await this.logTask(taskRun.id, "Tests required but no runnable test commands were found; failing task.", "tests", { testRequirements, missingTestsPolicy });
4216
- await this.stateService.markFailed(task.task, "tests_not_configured", statusContext);
4217
- await this.deps.workspaceRepo.updateTaskRun(taskRun.id, {
4218
- status: "failed",
4219
- finishedAt: new Date().toISOString(),
4220
- });
4221
- setFailureReason("tests_not_configured");
4222
- results.push({ taskKey: task.task.key, status: "failed", notes: "tests_not_configured" });
4223
- taskStatus = "failed";
4224
- await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
4225
- await emitTaskEndOnce();
4226
- continue taskLoop;
4289
+ if (missingTestsPolicy === "continue_task") {
4290
+ const warning = `Task ${task.task.key}: tests required but no runnable test harness was found; proceeding without automated tests.` +
4291
+ " Add metadata.tests/testCommands or tests/all.js.";
4292
+ warnings.push(warning);
4293
+ await this.logTask(taskRun.id, "Tests required but no runnable test commands were found; continuing without automated tests due to missing-tests policy.", "tests", { testRequirements, missingTestsPolicy });
4294
+ }
4295
+ else {
4296
+ await this.logTask(taskRun.id, "Tests required but no runnable test commands were found; failing task.", "tests", { testRequirements, missingTestsPolicy });
4297
+ await this.stateService.markFailed(task.task, "tests_not_configured", statusContext);
4298
+ await this.deps.workspaceRepo.updateTaskRun(taskRun.id, {
4299
+ status: "failed",
4300
+ finishedAt: new Date().toISOString(),
4301
+ });
4302
+ setFailureReason("tests_not_configured");
4303
+ results.push({ taskKey: task.task.key, status: "failed", notes: "tests_not_configured" });
4304
+ taskStatus = "failed";
4305
+ await this.deps.jobService.updateJobStatus(job.id, "running", { processedItems: index + 1 });
4306
+ await emitTaskEndOnce();
4307
+ continue taskLoop;
4308
+ }
4227
4309
  }
4228
4310
  const shouldRunTests = !request.dryRun && (testsRequired ? hasRunnableTests : testCommands.length > 0);
4229
4311
  let mergeConflicts = [];
@@ -105,6 +105,8 @@ export declare class CreateTasksService {
105
105
  private parseEpics;
106
106
  private generateStoriesForEpic;
107
107
  private generateTasksForStory;
108
+ private buildFallbackStoryForEpic;
109
+ private buildFallbackTasksForStory;
108
110
  private generatePlanFromAgent;
109
111
  private writePlanArtifacts;
110
112
  private persistPlanToDb;
@@ -1 +1 @@
1
- {"version":3,"file":"CreateTasksService.d.ts","sourceRoot":"","sources":["../../../src/services/planning/CreateTasksService.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CreateTasksService.d.ts","sourceRoot":"","sources":["../../../src/services/planning/CreateTasksService.ts"],"names":[],"mappings":"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"}
@@ -2360,6 +2360,71 @@ export class CreateTasksService {
2360
2360
  })
2361
2361
  .filter((t) => t.title);
2362
2362
  }
2363
+ buildFallbackStoryForEpic(epic) {
2364
+ const criteria = epic.acceptanceCriteria?.filter(Boolean) ?? [];
2365
+ return {
2366
+ localId: "us-fallback-1",
2367
+ title: `Deliver ${epic.title}`,
2368
+ userStory: `As a delivery team, we need an executable implementation story for ${epic.title}.`,
2369
+ description: [
2370
+ `Deterministic fallback story generated because model output for epic "${epic.title}" could not be parsed reliably.`,
2371
+ "Use SDS and related docs to decompose this story into concrete implementation tasks.",
2372
+ ].join("\n"),
2373
+ acceptanceCriteria: criteria.length > 0
2374
+ ? criteria
2375
+ : [
2376
+ "Story has actionable implementation tasks.",
2377
+ "Dependencies are explicit and story-scoped.",
2378
+ "Tasks are ready for execution.",
2379
+ ],
2380
+ relatedDocs: epic.relatedDocs ?? [],
2381
+ priorityHint: 1,
2382
+ tasks: [],
2383
+ };
2384
+ }
2385
+ buildFallbackTasksForStory(story) {
2386
+ const criteriaLines = (story.acceptanceCriteria ?? [])
2387
+ .slice(0, 6)
2388
+ .map((criterion) => `- ${criterion}`)
2389
+ .join("\n");
2390
+ return [
2391
+ {
2392
+ localId: "t-fallback-1",
2393
+ title: `Fallback planning for ${story.title}`,
2394
+ type: "chore",
2395
+ description: [
2396
+ `Draft a concrete implementation plan for story "${story.title}" using SDS/OpenAPI context.`,
2397
+ "List exact files/modules to touch and implementation order.",
2398
+ criteriaLines ? `Acceptance criteria to satisfy:\n${criteriaLines}` : "Acceptance criteria: use story definition.",
2399
+ ].join("\n"),
2400
+ estimatedStoryPoints: 2,
2401
+ priorityHint: 1,
2402
+ dependsOnKeys: [],
2403
+ relatedDocs: story.relatedDocs ?? [],
2404
+ unitTests: [],
2405
+ componentTests: [],
2406
+ integrationTests: [],
2407
+ apiTests: [],
2408
+ },
2409
+ {
2410
+ localId: "t-fallback-2",
2411
+ title: `Fallback implementation for ${story.title}`,
2412
+ type: "feature",
2413
+ description: [
2414
+ `Implement story "${story.title}" according to the fallback planning task.`,
2415
+ "Ensure done criteria and test requirements are explicitly documented for execution.",
2416
+ ].join("\n"),
2417
+ estimatedStoryPoints: 3,
2418
+ priorityHint: 2,
2419
+ dependsOnKeys: ["t-fallback-1"],
2420
+ relatedDocs: story.relatedDocs ?? [],
2421
+ unitTests: [],
2422
+ componentTests: [],
2423
+ integrationTests: [],
2424
+ apiTests: [],
2425
+ },
2426
+ ];
2427
+ }
2363
2428
  async generatePlanFromAgent(epics, agent, docSummary, options) {
2364
2429
  const planEpics = epics.map((epic, idx) => ({
2365
2430
  ...epic,
@@ -2367,20 +2432,56 @@ export class CreateTasksService {
2367
2432
  }));
2368
2433
  const planStories = [];
2369
2434
  const planTasks = [];
2435
+ const fallbackStoryScopes = new Set();
2370
2436
  for (const epic of planEpics) {
2371
- const stories = await this.generateStoriesForEpic(agent, { ...epic }, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
2372
- const limitedStories = stories.slice(0, options.maxStoriesPerEpic ?? stories.length);
2437
+ let stories = [];
2438
+ let usedFallbackStories = false;
2439
+ try {
2440
+ stories = await this.generateStoriesForEpic(agent, { ...epic }, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
2441
+ }
2442
+ catch (error) {
2443
+ usedFallbackStories = true;
2444
+ await this.jobService.appendLog(options.jobId, `Story generation failed for epic "${epic.title}". Using deterministic fallback story. Reason: ${error.message ?? String(error)}\n`);
2445
+ stories = [this.buildFallbackStoryForEpic(epic)];
2446
+ }
2447
+ let limitedStories = stories.slice(0, options.maxStoriesPerEpic ?? stories.length);
2448
+ if (limitedStories.length === 0) {
2449
+ usedFallbackStories = true;
2450
+ await this.jobService.appendLog(options.jobId, `Story generation returned no stories for epic "${epic.title}". Using deterministic fallback story.\n`);
2451
+ limitedStories = [this.buildFallbackStoryForEpic(epic)];
2452
+ }
2373
2453
  limitedStories.forEach((story, idx) => {
2374
- planStories.push({
2454
+ const planStory = {
2375
2455
  ...story,
2376
2456
  localId: story.localId ?? `us${idx + 1}`,
2377
2457
  epicLocalId: epic.localId,
2378
- });
2458
+ };
2459
+ planStories.push(planStory);
2460
+ if (usedFallbackStories) {
2461
+ fallbackStoryScopes.add(this.storyScopeKey(planStory.epicLocalId, planStory.localId));
2462
+ }
2379
2463
  });
2380
2464
  }
2381
2465
  for (const story of planStories) {
2382
- const tasks = await this.generateTasksForStory(agent, { key: story.epicLocalId, title: story.title }, story, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
2383
- const limitedTasks = tasks.slice(0, options.maxTasksPerStory ?? tasks.length);
2466
+ const storyScope = this.storyScopeKey(story.epicLocalId, story.localId);
2467
+ let tasks = [];
2468
+ if (fallbackStoryScopes.has(storyScope)) {
2469
+ tasks = this.buildFallbackTasksForStory(story);
2470
+ }
2471
+ else {
2472
+ try {
2473
+ tasks = await this.generateTasksForStory(agent, { key: story.epicLocalId, title: story.title }, story, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
2474
+ }
2475
+ catch (error) {
2476
+ await this.jobService.appendLog(options.jobId, `Task generation failed for story "${story.title}" (${storyScope}). Using deterministic fallback tasks. Reason: ${error.message ?? String(error)}\n`);
2477
+ tasks = this.buildFallbackTasksForStory(story);
2478
+ }
2479
+ }
2480
+ let limitedTasks = tasks.slice(0, options.maxTasksPerStory ?? tasks.length);
2481
+ if (limitedTasks.length === 0) {
2482
+ await this.jobService.appendLog(options.jobId, `Task generation returned no tasks for story "${story.title}" (${storyScope}). Using deterministic fallback tasks.\n`);
2483
+ limitedTasks = this.buildFallbackTasksForStory(story).slice(0, options.maxTasksPerStory ?? Number.MAX_SAFE_INTEGER);
2484
+ }
2384
2485
  limitedTasks.forEach((task, idx) => {
2385
2486
  planTasks.push({
2386
2487
  ...task,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcoda/core",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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.20",
36
- "@mcoda/db": "0.1.20",
37
- "@mcoda/agents": "0.1.20",
38
- "@mcoda/generators": "0.1.20",
39
- "@mcoda/integrations": "0.1.20"
35
+ "@mcoda/db": "0.1.22",
36
+ "@mcoda/agents": "0.1.22",
37
+ "@mcoda/generators": "0.1.22",
38
+ "@mcoda/integrations": "0.1.22",
39
+ "@mcoda/shared": "0.1.22"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsc -p tsconfig.json",