@programinglive/dev-workflow-mcp-server 1.0.4 → 1.0.7
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 +16 -0
- package/index.js +358 -15
- package/package.json +1 -1
- package/tests/workflowState.test.js +85 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [1.0.7](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.6...v1.0.7) (2025-10-20)
|
|
6
|
+
|
|
7
|
+
### [1.0.6](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.5...v1.0.6) (2025-10-20)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### ✅ Tests
|
|
11
|
+
|
|
12
|
+
* enforce clean release workflow ([c261a7b](https://github.com/programinglive/dev-workflow-mcp-server/commit/c261a7b3d141ba4472567bd47e4f6b6e4d2ee3fd))
|
|
13
|
+
|
|
14
|
+
### [1.0.5](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.4...v1.0.5) (2025-10-20)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### ✅ Tests
|
|
18
|
+
|
|
19
|
+
* update index.js and tests/workflowState.test.js ([b9df3c8](https://github.com/programinglive/dev-workflow-mcp-server/commit/b9df3c8643303769dbe4c5a0992995b6328cdfcf))
|
|
20
|
+
|
|
5
21
|
### [1.0.4](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.3...v1.0.4) (2025-10-20)
|
|
6
22
|
|
|
7
23
|
|
package/index.js
CHANGED
|
@@ -186,6 +186,279 @@ async function hasWorkingChanges() {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
export function workingTreeSummary(statusOutput) {
|
|
190
|
+
if (typeof statusOutput !== "string" || statusOutput.trim() === "") {
|
|
191
|
+
return {
|
|
192
|
+
hasChanges: false,
|
|
193
|
+
lines: [],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const lines = statusOutput
|
|
198
|
+
.split("\n")
|
|
199
|
+
.map((line) => line.replace(/\s+/g, " ").trim())
|
|
200
|
+
.filter(Boolean);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
hasChanges: lines.length > 0,
|
|
204
|
+
lines,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isTestFilePath(filePath) {
|
|
209
|
+
if (!filePath) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (/\.(test|spec)\.[^.]+$/i.test(filePath)) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return /(\/|^)(__tests__|tests?|spec)\//i.test(filePath);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function extractPathFromStatusLine(line) {
|
|
221
|
+
if (!line || line.length < 4) {
|
|
222
|
+
return "";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const rawPath = line.slice(3).trim();
|
|
226
|
+
if (!rawPath) {
|
|
227
|
+
return "";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (rawPath.includes(" -> ")) {
|
|
231
|
+
const parts = rawPath.split(" -> ");
|
|
232
|
+
return parts[parts.length - 1].trim();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return rawPath;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function containsTestFilesInStatus(statusOutput) {
|
|
239
|
+
if (typeof statusOutput !== "string" || statusOutput.trim() === "") {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return statusOutput
|
|
244
|
+
.split("\n")
|
|
245
|
+
.map((line) => extractPathFromStatusLine(line))
|
|
246
|
+
.some((path) => isTestFilePath(path));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isDocumentationFilePath(filePath) {
|
|
250
|
+
if (!filePath) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const normalized = filePath.toLowerCase();
|
|
255
|
+
return (
|
|
256
|
+
normalized.startsWith("docs/") ||
|
|
257
|
+
normalized.startsWith("documentation/") ||
|
|
258
|
+
normalized.endsWith(".md") ||
|
|
259
|
+
normalized.endsWith(".rst") ||
|
|
260
|
+
normalized.endsWith(".adoc") ||
|
|
261
|
+
normalized.includes("readme")
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function parseNameStatusLine(line) {
|
|
266
|
+
if (!line) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const parts = line.split("\t").map((part) => part.trim());
|
|
271
|
+
const status = parts[0];
|
|
272
|
+
if (!status) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (status.startsWith("R") || status.startsWith("C")) {
|
|
277
|
+
const originalPath = parts[1] || "";
|
|
278
|
+
const newPath = parts[2] || originalPath;
|
|
279
|
+
return {
|
|
280
|
+
status,
|
|
281
|
+
path: newPath,
|
|
282
|
+
originalPath,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
status,
|
|
288
|
+
path: parts[1] || "",
|
|
289
|
+
originalPath: "",
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function formatFileList(paths) {
|
|
294
|
+
const uniquePaths = [...new Set(paths.filter(Boolean))];
|
|
295
|
+
|
|
296
|
+
if (uniquePaths.length === 0) {
|
|
297
|
+
return "updates";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (uniquePaths.length === 1) {
|
|
301
|
+
return uniquePaths[0];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (uniquePaths.length === 2) {
|
|
305
|
+
return `${uniquePaths[0]} and ${uniquePaths[1]}`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (uniquePaths.length === 3) {
|
|
309
|
+
return `${uniquePaths[0]}, ${uniquePaths[1]}, and ${uniquePaths[2]}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const remaining = uniquePaths.length - 3;
|
|
313
|
+
return `${uniquePaths[0]}, ${uniquePaths[1]}, ${uniquePaths[2]} +${remaining} more`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function determineCommitType(changes) {
|
|
317
|
+
const hasTestChange = changes.some((change) =>
|
|
318
|
+
[change.path, change.originalPath].some((p) => isTestFilePath(p))
|
|
319
|
+
);
|
|
320
|
+
const hasDocsChange = changes.some((change) =>
|
|
321
|
+
[change.path, change.originalPath].some((p) => isDocumentationFilePath(p))
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
if (hasTestChange) {
|
|
325
|
+
return "test";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (hasDocsChange) {
|
|
329
|
+
return "docs";
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return "chore";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function describeChange(change) {
|
|
336
|
+
const statusCode = change.status || "";
|
|
337
|
+
const path = change.path || change.originalPath || "";
|
|
338
|
+
|
|
339
|
+
if (statusCode.startsWith("R")) {
|
|
340
|
+
return `renamed ${change.originalPath} -> ${change.path}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (statusCode.startsWith("C")) {
|
|
344
|
+
return `copied ${change.originalPath} -> ${change.path}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
switch (statusCode[0]) {
|
|
348
|
+
case "A":
|
|
349
|
+
return `added ${path}`;
|
|
350
|
+
case "M":
|
|
351
|
+
return `modified ${path}`;
|
|
352
|
+
case "D":
|
|
353
|
+
return `removed ${path}`;
|
|
354
|
+
case "U":
|
|
355
|
+
return `updated ${path}`;
|
|
356
|
+
default:
|
|
357
|
+
return `${statusCode} ${path}`.trim();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function createCommitMessageParts(changes, providedSummary = "") {
|
|
362
|
+
const summaryInput = typeof providedSummary === "string" ? providedSummary.trim() : "";
|
|
363
|
+
|
|
364
|
+
if (!Array.isArray(changes) || changes.length === 0) {
|
|
365
|
+
return {
|
|
366
|
+
summary: summaryInput,
|
|
367
|
+
body: "",
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const commitType = determineCommitType(changes);
|
|
372
|
+
const summaryPaths = changes
|
|
373
|
+
.map((change) => change.path || change.originalPath)
|
|
374
|
+
.filter(Boolean);
|
|
375
|
+
const generatedSummary = `${commitType}: update ${formatFileList(summaryPaths)}`;
|
|
376
|
+
const summary = summaryInput || generatedSummary;
|
|
377
|
+
|
|
378
|
+
const detailLines = changes
|
|
379
|
+
.map((change) => describeChange(change))
|
|
380
|
+
.filter((line) => line && line.trim().length > 0)
|
|
381
|
+
.map((line) => `- ${line}`);
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
summary,
|
|
385
|
+
body: detailLines.join("\n"),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function getStagedChanges() {
|
|
390
|
+
try {
|
|
391
|
+
const { stdout } = await exec("git diff --cached --name-status");
|
|
392
|
+
return stdout
|
|
393
|
+
.split("\n")
|
|
394
|
+
.map((line) => parseNameStatusLine(line))
|
|
395
|
+
.filter(Boolean);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function hasTestChanges() {
|
|
402
|
+
try {
|
|
403
|
+
const { stdout } = await exec("git status --porcelain");
|
|
404
|
+
return containsTestFilesInStatus(stdout);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function normalizeCommitHeader(message) {
|
|
411
|
+
if (typeof message !== "string") {
|
|
412
|
+
return "";
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const header = message.split("\n")[0] || "";
|
|
416
|
+
return header.trim();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function containsBreakingChange(message) {
|
|
420
|
+
if (typeof message !== "string") {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return /breaking change/i.test(message);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function determineReleaseTypeFromCommit(message) {
|
|
428
|
+
const header = normalizeCommitHeader(message).toLowerCase();
|
|
429
|
+
const hasBreaking = containsBreakingChange(message) || /!/.test(header.split(":")[0] || "");
|
|
430
|
+
|
|
431
|
+
if (hasBreaking) {
|
|
432
|
+
return "major";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const typeMatch = header.match(/^([a-z]+)(\(|:)/i);
|
|
436
|
+
const commitType = typeMatch ? typeMatch[1] : "";
|
|
437
|
+
|
|
438
|
+
if (commitType === "feat") {
|
|
439
|
+
return "minor";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (commitType === "fix" || commitType === "perf") {
|
|
443
|
+
return "patch";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (!commitType && containsBreakingChange(message)) {
|
|
447
|
+
return "major";
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return "patch";
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function getLastCommitMessage() {
|
|
454
|
+
try {
|
|
455
|
+
const { stdout } = await exec("git log -1 --pretty=%B");
|
|
456
|
+
return stdout.trim();
|
|
457
|
+
} catch (error) {
|
|
458
|
+
return "";
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
189
462
|
async function getCurrentBranch() {
|
|
190
463
|
try {
|
|
191
464
|
const { stdout } = await exec("git rev-parse --abbrev-ref HEAD");
|
|
@@ -325,7 +598,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
325
598
|
"Optional branch name to push to (defaults to current branch)",
|
|
326
599
|
},
|
|
327
600
|
},
|
|
328
|
-
required: [
|
|
601
|
+
required: [],
|
|
329
602
|
},
|
|
330
603
|
},
|
|
331
604
|
{
|
|
@@ -671,26 +944,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
671
944
|
};
|
|
672
945
|
}
|
|
673
946
|
|
|
674
|
-
const
|
|
947
|
+
const providedCommitMessage =
|
|
675
948
|
typeof args.commitMessage === "string" ? args.commitMessage.trim() : "";
|
|
676
949
|
|
|
677
|
-
if (!
|
|
950
|
+
if (!(await hasWorkingChanges())) {
|
|
678
951
|
return {
|
|
679
952
|
content: [
|
|
680
953
|
{
|
|
681
954
|
type: "text",
|
|
682
|
-
text: "⚠️
|
|
955
|
+
text: "⚠️ No changes detected. Make sure you have modifications to commit before running 'commit_and_push'.",
|
|
683
956
|
},
|
|
684
957
|
],
|
|
685
958
|
};
|
|
686
959
|
}
|
|
687
960
|
|
|
688
|
-
if (!(await
|
|
961
|
+
if (!(await hasTestChanges())) {
|
|
689
962
|
return {
|
|
690
963
|
content: [
|
|
691
964
|
{
|
|
692
965
|
type: "text",
|
|
693
|
-
text: "⚠️
|
|
966
|
+
text: "⚠️ Please include test updates in your changes before running 'commit_and_push'. Ensure at least one test file is modified.",
|
|
694
967
|
},
|
|
695
968
|
],
|
|
696
969
|
};
|
|
@@ -709,8 +982,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
709
982
|
};
|
|
710
983
|
}
|
|
711
984
|
|
|
985
|
+
const stagedChanges = await getStagedChanges();
|
|
986
|
+
const { summary: generatedSummary, body: commitBody } =
|
|
987
|
+
createCommitMessageParts(stagedChanges, providedCommitMessage);
|
|
988
|
+
|
|
989
|
+
if (!generatedSummary) {
|
|
990
|
+
return {
|
|
991
|
+
content: [
|
|
992
|
+
{
|
|
993
|
+
type: "text",
|
|
994
|
+
text: "⚠️ Unable to generate commit message. Please provide a 'commitMessage' manually.",
|
|
995
|
+
},
|
|
996
|
+
],
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
712
1000
|
try {
|
|
713
|
-
|
|
1001
|
+
if (commitBody) {
|
|
1002
|
+
await exec(
|
|
1003
|
+
`git commit -m ${shellEscape(generatedSummary)} -m ${shellEscape(commitBody)}`
|
|
1004
|
+
);
|
|
1005
|
+
} else {
|
|
1006
|
+
await exec(`git commit -m ${shellEscape(generatedSummary)}`);
|
|
1007
|
+
}
|
|
714
1008
|
} catch (error) {
|
|
715
1009
|
const output = error.stderr || error.stdout || error.message || "Unknown error";
|
|
716
1010
|
if (output.includes("nothing to commit")) {
|
|
@@ -737,9 +1031,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
737
1031
|
const requestedBranch =
|
|
738
1032
|
typeof args.branch === "string" ? args.branch.trim() : "";
|
|
739
1033
|
const currentBranch = await getCurrentBranch();
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
? `git push origin ${shellEscape(pushBranch)}`
|
|
1034
|
+
const pushCommand = requestedBranch
|
|
1035
|
+
? `git push origin ${shellEscape(requestedBranch)}`
|
|
743
1036
|
: "git push";
|
|
744
1037
|
|
|
745
1038
|
try {
|
|
@@ -756,8 +1049,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
756
1049
|
}
|
|
757
1050
|
|
|
758
1051
|
workflowState.state.commitAndPushCompleted = true;
|
|
759
|
-
workflowState.state.lastCommitMessage =
|
|
760
|
-
workflowState.state.lastPushBranch =
|
|
1052
|
+
workflowState.state.lastCommitMessage = generatedSummary;
|
|
1053
|
+
workflowState.state.lastPushBranch =
|
|
1054
|
+
requestedBranch || currentBranch || "";
|
|
761
1055
|
workflowState.state.currentPhase = workflowState.state.released
|
|
762
1056
|
? "ready_to_complete"
|
|
763
1057
|
: "release";
|
|
@@ -767,7 +1061,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
767
1061
|
content: [
|
|
768
1062
|
{
|
|
769
1063
|
type: "text",
|
|
770
|
-
text: `✅ Commit & push completed!\n\nCommit message: ${
|
|
1064
|
+
text: `✅ Commit & push completed!\n\nCommit message: ${generatedSummary}\nPushed to: ${
|
|
1065
|
+
requestedBranch || currentBranch || "(default upstream)"
|
|
1066
|
+
}\n\nNext: Run 'perform_release' to record the release.`,
|
|
771
1067
|
},
|
|
772
1068
|
],
|
|
773
1069
|
};
|
|
@@ -807,8 +1103,55 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
807
1103
|
};
|
|
808
1104
|
}
|
|
809
1105
|
|
|
1106
|
+
const releaseCommand =
|
|
1107
|
+
typeof args.command === "string" ? args.command.trim() : "";
|
|
1108
|
+
|
|
1109
|
+
if (!releaseCommand) {
|
|
1110
|
+
return {
|
|
1111
|
+
content: [
|
|
1112
|
+
{
|
|
1113
|
+
type: "text",
|
|
1114
|
+
text: "⚠️ Please provide a non-empty 'command' when using 'perform_release'.",
|
|
1115
|
+
},
|
|
1116
|
+
],
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (await hasWorkingChanges()) {
|
|
1121
|
+
return {
|
|
1122
|
+
content: [
|
|
1123
|
+
{
|
|
1124
|
+
type: "text",
|
|
1125
|
+
text: "⚠️ Working tree not clean! Please commit or stash all changes before running 'perform_release'.",
|
|
1126
|
+
},
|
|
1127
|
+
],
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
const lastCommitMessage = await getLastCommitMessage();
|
|
1132
|
+
const releaseType = determineReleaseTypeFromCommit(
|
|
1133
|
+
lastCommitMessage || workflowState.state.lastCommitMessage
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
const finalCommand = releaseCommand.includes("--release-as")
|
|
1137
|
+
? releaseCommand
|
|
1138
|
+
: `${releaseCommand} -- --release-as ${releaseType}`;
|
|
1139
|
+
|
|
1140
|
+
try {
|
|
1141
|
+
await exec(finalCommand);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
return {
|
|
1144
|
+
content: [
|
|
1145
|
+
{
|
|
1146
|
+
type: "text",
|
|
1147
|
+
text: `❌ Release command failed:\n\n${error.stderr || error.stdout || error.message}`,
|
|
1148
|
+
},
|
|
1149
|
+
],
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
|
|
810
1153
|
workflowState.state.released = true;
|
|
811
|
-
workflowState.state.releaseCommand =
|
|
1154
|
+
workflowState.state.releaseCommand = finalCommand;
|
|
812
1155
|
workflowState.state.releaseNotes =
|
|
813
1156
|
typeof args.notes === "string" ? args.notes : "";
|
|
814
1157
|
workflowState.state.currentPhase = "ready_to_complete";
|
|
@@ -818,7 +1161,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
818
1161
|
content: [
|
|
819
1162
|
{
|
|
820
1163
|
type: "text",
|
|
821
|
-
text: `🚀 Release recorded!\n\nCommand: ${
|
|
1164
|
+
text: `🚀 Release recorded!\n\nCommand: ${finalCommand}\nRelease type: ${releaseType}\n${workflowState.state.releaseNotes ? `Notes: ${workflowState.state.releaseNotes}\n` : ""}\n✅ Next: Run 'complete_task' to wrap up.`,
|
|
822
1165
|
},
|
|
823
1166
|
],
|
|
824
1167
|
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,14 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
WorkflowState,
|
|
8
|
+
getNextStep,
|
|
9
|
+
containsTestFilesInStatus,
|
|
10
|
+
createCommitMessageParts,
|
|
11
|
+
determineReleaseTypeFromCommit,
|
|
12
|
+
workingTreeSummary,
|
|
13
|
+
} from "../index.js";
|
|
7
14
|
|
|
8
15
|
async function withWorkflowState(callback) {
|
|
9
16
|
const tempDir = await mkdtemp(path.join(tmpdir(), "workflow-state-"));
|
|
@@ -118,3 +125,80 @@ test("getNextStep guides workflow progression", () => {
|
|
|
118
125
|
const afterRelease = { ...afterCommit, released: true };
|
|
119
126
|
assert.equal(getNextStep(afterRelease), "Complete the task");
|
|
120
127
|
});
|
|
128
|
+
|
|
129
|
+
test("containsTestFilesInStatus detects modified test files", () => {
|
|
130
|
+
const statusOutput = [
|
|
131
|
+
" M src/app.test.js",
|
|
132
|
+
"A tests/helpers/util.js",
|
|
133
|
+
"R src/old.js -> src/new.spec.ts",
|
|
134
|
+
].join("\n");
|
|
135
|
+
|
|
136
|
+
assert.equal(containsTestFilesInStatus(statusOutput), true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("containsTestFilesInStatus returns false when no test files present", () => {
|
|
140
|
+
const statusOutput = [
|
|
141
|
+
" M src/app.js",
|
|
142
|
+
"A docs/README.md",
|
|
143
|
+
"?? assets/logo.png",
|
|
144
|
+
].join("\n");
|
|
145
|
+
|
|
146
|
+
assert.equal(containsTestFilesInStatus(statusOutput), false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("createCommitMessageParts generates test prefixed summary with details", () => {
|
|
150
|
+
const changes = [
|
|
151
|
+
{ status: "M", path: "src/app.test.js" },
|
|
152
|
+
{ status: "A", path: "tests/helpers/util.js" },
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const { summary, body } = createCommitMessageParts(changes);
|
|
156
|
+
|
|
157
|
+
assert.equal(summary.startsWith("test:"), true);
|
|
158
|
+
assert.equal(body.includes("- modified src/app.test.js"), true);
|
|
159
|
+
assert.equal(body.includes("- added tests/helpers/util.js"), true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("createCommitMessageParts respects provided summary when present", () => {
|
|
163
|
+
const changes = [{ status: "M", path: "docs/guide.md" }];
|
|
164
|
+
const provided = "docs: update guide";
|
|
165
|
+
|
|
166
|
+
const { summary, body } = createCommitMessageParts(changes, provided);
|
|
167
|
+
|
|
168
|
+
assert.equal(summary, provided);
|
|
169
|
+
assert.equal(body.includes("- modified docs/guide.md"), true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("determineReleaseTypeFromCommit returns major for breaking change", () => {
|
|
173
|
+
const message = "feat!: overhaul API\n\nBREAKING CHANGE: new behavior";
|
|
174
|
+
assert.equal(determineReleaseTypeFromCommit(message), "major");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("determineReleaseTypeFromCommit returns minor for feat", () => {
|
|
178
|
+
const message = "feat: add dashboard";
|
|
179
|
+
assert.equal(determineReleaseTypeFromCommit(message), "minor");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("determineReleaseTypeFromCommit returns patch for fix", () => {
|
|
183
|
+
const message = "fix: resolve login issue";
|
|
184
|
+
assert.equal(determineReleaseTypeFromCommit(message), "patch");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("determineReleaseTypeFromCommit defaults to patch for other commit types", () => {
|
|
188
|
+
const message = "chore: update dependencies";
|
|
189
|
+
assert.equal(determineReleaseTypeFromCommit(message), "patch");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("workingTreeSummary reports no changes for empty output", () => {
|
|
193
|
+
const summary = workingTreeSummary("\n \n");
|
|
194
|
+
assert.equal(summary.hasChanges, false);
|
|
195
|
+
assert.deepEqual(summary.lines, []);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("workingTreeSummary lists trimmed status lines", () => {
|
|
199
|
+
const statusOutput = " M src/app.js\nA tests/new.test.js\n";
|
|
200
|
+
const summary = workingTreeSummary(statusOutput);
|
|
201
|
+
|
|
202
|
+
assert.equal(summary.hasChanges, true);
|
|
203
|
+
assert.deepEqual(summary.lines, ["M src/app.js", "A tests/new.test.js"]);
|
|
204
|
+
});
|