@programinglive/dev-workflow-mcp-server 1.5.0 → 1.5.2

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.
Files changed (99) hide show
  1. package/.agent/workflows/dev-workflow-bugfix.md +250 -250
  2. package/.agent/workflows/dev-workflow-feature.md +201 -201
  3. package/.agent/workflows/dev-workflow-full-cycle.md +400 -400
  4. package/.agent/workflows/dev-workflow-refactor.md +238 -238
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +34 -34
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +25 -25
  7. package/.github/pull_request_template.md +22 -22
  8. package/.github/workflows/npm-publish.yml +50 -47
  9. package/.husky/commit-msg +0 -0
  10. package/CHANGELOG.md +750 -491
  11. package/CODE_OF_CONDUCT.md +79 -79
  12. package/CONTRIBUTING.md +60 -60
  13. package/LICENSE +21 -21
  14. package/README.md +632 -609
  15. package/SECURITY.md +20 -20
  16. package/db/index.js +204 -204
  17. package/docs/NETLIFY_DEPLOYMENT.md +125 -0
  18. package/docs/PLESK_DEPLOYMENT.md +170 -0
  19. package/docs/WORKFLOW-BEST-PRACTICES.md +58 -0
  20. package/docs/antigravity/EXAMPLES.md +535 -535
  21. package/docs/antigravity/GETTING-STARTED.md +292 -292
  22. package/docs/antigravity/TROUBLESHOOTING.md +742 -742
  23. package/docs/antigravity/WALKTHROUGH.md +25 -25
  24. package/docs/antigravity/WORKFLOW-TEMPLATES.md +670 -670
  25. package/docs/product/PRD.md +67 -64
  26. package/docs/release-notes/RELEASE_NOTES.md +226 -132
  27. package/docs/web-dashboard.md +175 -163
  28. package/exec.js +49 -49
  29. package/index.js +279 -279
  30. package/install.js +73 -73
  31. package/package.json +11 -9
  32. package/release-wrapper.js +192 -157
  33. package/scripts/build-docs.js +63 -63
  34. package/scripts/hello-world.js +1 -1
  35. package/scripts/release.cjs +252 -252
  36. package/scripts/update-release-notes.cjs +178 -182
  37. package/server.json +23 -0
  38. package/tailwind.config.js +8 -10
  39. package/tests/build.test.js +83 -48
  40. package/tests/exec.test.js +35 -35
  41. package/tests/handlers.test.js +1058 -980
  42. package/tests/hello-world.test.js +13 -13
  43. package/tests/mcpRegistry.test.js +65 -0
  44. package/tests/release-wrapper.test.js +107 -105
  45. package/tests/workflowState.test.js +505 -502
  46. package/tools/definitions.js +332 -332
  47. package/tools/handlers.js +1019 -1019
  48. package/vite.config.js +43 -43
  49. package/vite.web.config.js +23 -0
  50. package/web/README.md +36 -0
  51. package/web/app/docs/page.tsx +202 -0
  52. package/web/app/favicon.ico +0 -0
  53. package/web/app/globals.css +91 -0
  54. package/web/app/layout.tsx +36 -0
  55. package/web/app/page.tsx +15 -0
  56. package/web/components/ApiReference.tsx +20 -0
  57. package/web/components/Features.tsx +61 -0
  58. package/web/components/Footer.tsx +9 -0
  59. package/web/components/Hero.tsx +96 -0
  60. package/web/components/History.tsx +143 -0
  61. package/web/components/Navbar.tsx +64 -0
  62. package/web/components/Terminal.tsx +114 -0
  63. package/web/eslint.config.mjs +18 -0
  64. package/web/lib/db.js +131 -0
  65. package/web/lib/docs.js +73 -0
  66. package/web/netlify.toml +3 -0
  67. package/web/next.config.ts +12 -0
  68. package/web/package-lock.json +6560 -0
  69. package/web/package.json +28 -0
  70. package/web/postcss.config.mjs +9 -0
  71. package/web/public/assets/android-chrome-192x192.png +0 -0
  72. package/web/public/assets/android-chrome-512x512.png +0 -0
  73. package/web/public/assets/apple-touch-icon.png +0 -0
  74. package/web/public/assets/favicon-16x16.png +0 -0
  75. package/web/public/assets/favicon-32x32.png +0 -0
  76. package/web/public/assets/favicon.ico +0 -0
  77. package/web/public/assets/favicon.png +0 -0
  78. package/web/public/assets/logo.png +0 -0
  79. package/web/public/assets/og-preview.png +0 -0
  80. package/web/public/assets/site.webmanifest +1 -0
  81. package/web/public/file.svg +1 -0
  82. package/web/public/globe.svg +1 -0
  83. package/web/public/next.svg +1 -0
  84. package/web/public/vercel.svg +1 -0
  85. package/web/public/window.svg +1 -0
  86. package/web/server.js +31 -159
  87. package/web/tailwind.config.ts +19 -0
  88. package/web/tsconfig.json +42 -0
  89. package/workflow-state.js +612 -626
  90. package/dist/docs/EXAMPLES.html +0 -411
  91. package/dist/docs/GETTING-STARTED.html +0 -233
  92. package/dist/docs/README.html +0 -535
  93. package/dist/docs/RELEASE_NOTES.html +0 -167
  94. package/dist/docs/TROUBLESHOOTING.html +0 -542
  95. package/dist/docs/WORKFLOW-TEMPLATES.html +0 -591
  96. package/dist/docs/web-dashboard.html +0 -158
  97. package/web/public/index.html +0 -1046
  98. package/web/public/input.css +0 -3
  99. package/web/public/output.css +0 -1
@@ -1,502 +1,505 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import {
4
- mkdtemp,
5
- rm,
6
- mkdir,
7
- realpath,
8
- readFile,
9
- writeFile,
10
- access,
11
- copyFile,
12
- } from "node:fs/promises";
13
- import { constants as fsConstants } from "node:fs";
14
- import { tmpdir } from "node:os";
15
- import path from "node:path";
16
- import { fileURLToPath, pathToFileURL } from "node:url";
17
- import {
18
- WorkflowState,
19
- getNextStep,
20
- containsTestFilesInStatus,
21
- createCommitMessageParts,
22
- determineReleaseTypeFromCommit,
23
- workingTreeSummary,
24
- } from "../index.js";
25
-
26
- async function withWorkflowState(callback) {
27
- const tempDir = await mkdtemp(path.join(tmpdir(), "workflow-state-"));
28
- const stateFile = path.join(tempDir, "state.json");
29
- const state = new WorkflowState(stateFile);
30
- await state.load();
31
-
32
- try {
33
- await callback(state);
34
- } finally {
35
- await rm(tempDir, { recursive: true, force: true });
36
- }
37
- }
38
-
39
- test("resolveStateFile prefers current working directory when multiple project markers exist", async () => {
40
- const tempRoot = await mkdtemp(path.join(tmpdir(), "workflow-prefers-cwd-"));
41
- const moduleDir = path.join(tempRoot, "module");
42
- const projectA = path.join(tempRoot, "project-a");
43
- const projectB = path.join(tempRoot, "project-b");
44
- const projectBNested = path.join(projectB, "sub", "app");
45
-
46
- await mkdir(moduleDir, { recursive: true });
47
- await mkdir(projectA, { recursive: true });
48
- await mkdir(projectBNested, { recursive: true });
49
-
50
- const sourceModulePath = fileURLToPath(new URL("../workflow-state.js", import.meta.url));
51
- await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
52
-
53
- await writeFile(path.join(moduleDir, "package.json"), JSON.stringify({ name: "module" }));
54
- await writeFile(path.join(projectA, "package.json"), JSON.stringify({ name: "project-a" }));
55
- await writeFile(path.join(projectB, "package.json"), JSON.stringify({ name: "project-b" }));
56
-
57
- const moduleUrl = pathToFileURL(path.join(moduleDir, "workflow-state.js"));
58
- const { WorkflowState: TempWorkflowState } = await import(`${moduleUrl.href}?preferCwd=${Date.now()}`);
59
-
60
- const originalCwd = process.cwd();
61
- const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
62
- const originalInitCwd = process.env.INIT_CWD;
63
-
64
- try {
65
- process.chdir(projectBNested);
66
- process.env.INIT_CWD = projectA;
67
-
68
- const state = new TempWorkflowState();
69
- const resolvedStateFile = state.stateFile;
70
-
71
- assert.ok(
72
- resolvedStateFile.startsWith(path.join(projectB, ".state", "users")),
73
- "State file should resolve under the current working directory's project"
74
- );
75
-
76
- assert.ok(
77
- !resolvedStateFile.startsWith(path.join(projectA, ".state")),
78
- "State file should not use INIT_CWD project when cwd has its own markers"
79
- );
80
- } finally {
81
- process.chdir(originalCwd);
82
- if (initWasSet) {
83
- process.env.INIT_CWD = originalInitCwd;
84
- } else {
85
- delete process.env.INIT_CWD;
86
- }
87
- await rm(tempRoot, { recursive: true, force: true });
88
- }
89
- });
90
-
91
- test("WorkflowState mirrors state file to compatibility locations", async () => {
92
- const tempRoot = await mkdtemp(path.join(tmpdir(), "workflow-compat-"));
93
- const moduleDir = path.join(tempRoot, "module");
94
- const projectRoot = path.join(tempRoot, "project");
95
- const stateDir = path.join(projectRoot, ".state");
96
- const sourceModulePath = fileURLToPath(new URL("../workflow-state.js", import.meta.url));
97
- await mkdir(moduleDir, { recursive: true });
98
- await mkdir(path.join(moduleDir, "dist"), { recursive: true });
99
- await mkdir(path.join(tempRoot, "dist"), { recursive: true });
100
- await mkdir(stateDir, { recursive: true });
101
- await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
102
- await writeFile(path.join(projectRoot, "package.json"), JSON.stringify({ name: "temp" }));
103
-
104
- const stateFile = path.join(stateDir, "workflow-state.json");
105
- await writeFile(stateFile, JSON.stringify({ currentPhase: "testing" }, null, 2));
106
-
107
- const moduleUrl = pathToFileURL(path.join(moduleDir, "workflow-state.js"));
108
- const { WorkflowState: TempWorkflowState } = await import(moduleUrl.href);
109
-
110
- const state = new TempWorkflowState(stateFile);
111
- await state.load();
112
- state.state.currentPhase = "commit";
113
- await state.save();
114
-
115
- // Ensure primary state file was written correctly
116
- const primaryContent = await readFile(stateFile, "utf-8");
117
- const primaryParsed = JSON.parse(primaryContent);
118
- assert.equal(primaryParsed.currentPhase, "commit", "primary state file should be updated");
119
-
120
- await rm(tempRoot, { recursive: true, force: true });
121
- });
122
-
123
- test("WorkflowState avoids filesystem root when no project markers found", async () => {
124
- const tempRoot = await mkdtemp(path.join(tmpdir(), "workflow-rootless-"));
125
- const moduleDir = path.join(tempRoot, "module");
126
- await mkdir(moduleDir, { recursive: true });
127
-
128
- const sourceModulePath = fileURLToPath(new URL("../workflow-state.js", import.meta.url));
129
- await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
130
-
131
- const moduleUrl = pathToFileURL(path.join(moduleDir, "workflow-state.js"));
132
- const { WorkflowState: TempWorkflowState } = await import(moduleUrl.href);
133
-
134
- const originalCwd = process.cwd();
135
- const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
136
- const originalInitCwd = process.env.INIT_CWD;
137
-
138
- try {
139
- const cwdWithoutMarkers = path.join(tempRoot, "random", "deep");
140
- await mkdir(cwdWithoutMarkers, { recursive: true });
141
-
142
- process.chdir(cwdWithoutMarkers);
143
- process.env.INIT_CWD = cwdWithoutMarkers;
144
-
145
- const state = new TempWorkflowState();
146
- const resolvedTempRoot = await realpath(tempRoot);
147
-
148
- assert.ok(
149
- state.stateFile.startsWith(resolvedTempRoot),
150
- "state file should remain inside the temporary root when no project markers exist"
151
- );
152
- assert.ok(
153
- !state.stateFile.startsWith(path.join(path.sep, ".state")),
154
- "state file should not resolve to filesystem root"
155
- );
156
- } finally {
157
- process.chdir(originalCwd);
158
- if (initWasSet) {
159
- process.env.INIT_CWD = originalInitCwd;
160
- } else {
161
- delete process.env.INIT_CWD;
162
- }
163
-
164
- await rm(tempRoot, { recursive: true, force: true });
165
- }
166
- });
167
-
168
- test("resolveUserId persists generated ID", async () => {
169
- const projectRoot = await mkdtemp(path.join(tmpdir(), "workflow-userid-"));
170
- await writeFile(path.join(projectRoot, "package.json"), JSON.stringify({ name: "temp" }));
171
-
172
- const originalCwd = process.cwd();
173
- const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
174
- const originalInitCwd = process.env.INIT_CWD;
175
- const userEnvWasSet = Object.prototype.hasOwnProperty.call(process.env, "DEV_WORKFLOW_USER_ID");
176
- const originalUserEnv = process.env.DEV_WORKFLOW_USER_ID;
177
-
178
- try {
179
- process.chdir(projectRoot);
180
- process.env.INIT_CWD = projectRoot;
181
- if (userEnvWasSet) {
182
- delete process.env.DEV_WORKFLOW_USER_ID;
183
- }
184
-
185
- const firstModuleHref = new URL("../workflow-state.js?resolveUserId1", import.meta.url).href;
186
- const { WorkflowState: FirstWorkflowState } = await import(firstModuleHref);
187
-
188
- const state = new FirstWorkflowState();
189
- await state.ensurePrimaryFile();
190
-
191
- const userDir = path.dirname(state.stateFile);
192
- const userId = path.basename(userDir);
193
- assert.ok(userId.startsWith("user-"), "Generated user ID should use user- prefix");
194
-
195
- const userIdFile = path.join(projectRoot, ".state", "user-id");
196
- const persistedId = (await readFile(userIdFile, "utf-8")).trim();
197
- assert.equal(persistedId, userId, "user-id file should match generated user ID");
198
-
199
- const secondModuleHref = new URL("../workflow-state.js?resolveUserId2", import.meta.url).href;
200
- const { WorkflowState: SecondWorkflowState } = await import(secondModuleHref);
201
-
202
- const reloadedState = new SecondWorkflowState();
203
- await reloadedState.ensurePrimaryFile();
204
-
205
- assert.equal(reloadedState.stateFile, state.stateFile, "State file path should persist across reloads");
206
- const reloadedUserId = path.basename(path.dirname(reloadedState.stateFile));
207
- assert.equal(reloadedUserId, userId, "Reloaded module should reuse persisted user ID");
208
- } finally {
209
- process.chdir(originalCwd);
210
- if (initWasSet) {
211
- process.env.INIT_CWD = originalInitCwd;
212
- } else {
213
- delete process.env.INIT_CWD;
214
- }
215
- if (userEnvWasSet) {
216
- process.env.DEV_WORKFLOW_USER_ID = originalUserEnv;
217
- } else {
218
- delete process.env.DEV_WORKFLOW_USER_ID;
219
- }
220
- await rm(projectRoot, { recursive: true, force: true });
221
- }
222
- });
223
-
224
- test("WorkflowState generates project summary on load and updates on save", async () => {
225
- await withWorkflowState(async (state) => {
226
- const summaryPath = path.join(path.dirname(state.stateFile), "project-summary.json");
227
-
228
- const initialSummary = JSON.parse(await readFile(summaryPath, "utf-8"));
229
- assert.equal(initialSummary.totalTasks, 0);
230
- assert.deepEqual(initialSummary.taskTypes, {});
231
-
232
- state.addToHistory({
233
- taskDescription: "Seed summary",
234
- taskType: "feature",
235
- commitMessage: "feat: seed",
236
- });
237
- await state.save();
238
-
239
- const updatedSummary = JSON.parse(await readFile(summaryPath, "utf-8"));
240
- assert.equal(updatedSummary.totalTasks, 1);
241
- assert.equal(updatedSummary.taskTypes.feature, 1);
242
- assert.equal(updatedSummary.recentTasks[0].description, "Seed summary");
243
- });
244
- });
245
-
246
- test("WorkflowState persists data across save/load", async () => {
247
- await withWorkflowState(async (state) => {
248
- state.state.currentPhase = "testing";
249
- state.state.taskDescription = "Test persistence";
250
- state.state.bugFixed = true;
251
- state.state.testsCreated = true;
252
- state.state.readyCheckCompleted = true;
253
- state.state.released = true;
254
- state.state.releaseCommand = "npm run release";
255
- state.state.releaseNotes = "Automated release";
256
- state.state.commitAndPushCompleted = true;
257
- state.state.lastCommitMessage = "test: persistence";
258
- state.state.lastPushBranch = "main";
259
- state.state.testsSkipped = true;
260
- state.state.testsSkippedReason = "Legacy app uses manual QA";
261
- await state.save();
262
-
263
- const reloaded = new WorkflowState(state.stateFile);
264
- await reloaded.load();
265
-
266
- assert.equal(reloaded.state.currentPhase, "testing");
267
- assert.equal(reloaded.state.taskDescription, "Test persistence");
268
- assert.equal(reloaded.state.bugFixed, true);
269
- assert.equal(reloaded.state.testsCreated, true);
270
- assert.equal(reloaded.state.readyCheckCompleted, true);
271
- assert.equal(reloaded.state.released, true);
272
- assert.equal(reloaded.state.releaseCommand, "npm run release");
273
- assert.equal(reloaded.state.releaseNotes, "Automated release");
274
- assert.equal(reloaded.state.commitAndPushCompleted, true);
275
- assert.equal(reloaded.state.lastCommitMessage, "test: persistence");
276
- assert.equal(reloaded.state.lastPushBranch, "main");
277
- assert.equal(reloaded.state.testsSkipped, true);
278
- assert.equal(
279
- reloaded.state.testsSkippedReason,
280
- "Legacy app uses manual QA"
281
- );
282
- });
283
- });
284
-
285
- test("WorkflowState resolves state file relative to project root", async () => {
286
- const projectRoot = await mkdtemp(path.join(tmpdir(), "workflow-project-"));
287
- const originalCwd = process.cwd();
288
- const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
289
- const originalInitCwd = process.env.INIT_CWD;
290
- const devStateWasSet = Object.prototype.hasOwnProperty.call(
291
- process.env,
292
- "DEV_WORKFLOW_STATE_FILE"
293
- );
294
- const originalDevState = process.env.DEV_WORKFLOW_STATE_FILE;
295
-
296
- try {
297
- await mkdir(path.join(projectRoot, ".git"));
298
- const nestedDir = path.join(projectRoot, "dist", "server");
299
- await mkdir(nestedDir, { recursive: true });
300
-
301
- process.chdir(projectRoot);
302
- process.env.INIT_CWD = projectRoot;
303
- if (devStateWasSet) {
304
- delete process.env.DEV_WORKFLOW_STATE_FILE;
305
- }
306
- const state = new WorkflowState();
307
- assert.ok(
308
- state.stateFile.startsWith(path.join(projectRoot, ".state", "users")),
309
- "state file should resolve under project .state/users directory"
310
- );
311
-
312
- process.chdir(nestedDir);
313
- delete process.env.INIT_CWD;
314
- delete process.env.DEV_WORKFLOW_STATE_FILE;
315
-
316
- const state2 = new WorkflowState();
317
- const resolvedProjectRoot = await realpath(projectRoot);
318
- assert.ok(
319
- state2.stateFile.startsWith(path.join(resolvedProjectRoot, ".state", "users")),
320
- "state file should resolve to project root"
321
- );
322
- } finally {
323
- process.chdir(originalCwd);
324
- if (initWasSet) {
325
- process.env.INIT_CWD = originalInitCwd;
326
- } else {
327
- delete process.env.INIT_CWD;
328
- }
329
- if (devStateWasSet) {
330
- process.env.DEV_WORKFLOW_STATE_FILE = originalDevState;
331
- } else {
332
- delete process.env.DEV_WORKFLOW_STATE_FILE;
333
- }
334
- await rm(projectRoot, { recursive: true, force: true });
335
- }
336
- });
337
-
338
- test("WorkflowState reset keeps history but clears progress", async () => {
339
- await withWorkflowState(async (state) => {
340
- state.state.currentPhase = "ready";
341
- state.state.testsCreated = true;
342
- state.state.testsPassed = true;
343
- state.state.documentationCreated = true;
344
- state.state.readyCheckCompleted = true;
345
- state.state.released = true;
346
- state.state.commitAndPushCompleted = true;
347
- state.state.testsSkipped = true;
348
- state.state.testsSkippedReason = "Manual QA";
349
- state.addToHistory({ taskDescription: "Sample task" });
350
-
351
- state.reset();
352
-
353
- assert.equal(state.state.currentPhase, "idle");
354
- assert.equal(state.state.testsCreated, false);
355
- assert.equal(state.state.testsPassed, false);
356
- assert.equal(state.state.documentationCreated, false);
357
- assert.equal(state.state.readyCheckCompleted, false);
358
- assert.equal(state.state.released, false);
359
- assert.equal(state.state.commitAndPushCompleted, false);
360
- assert.equal(state.state.testsSkipped, false);
361
- assert.equal(state.state.testsSkippedReason, "");
362
- assert.equal(state.state.history.length, 1);
363
- assert.equal(state.state.history[0].taskDescription, "Sample task");
364
- });
365
- });
366
-
367
- test("getNextStep treats skipped tests as satisfied", () => {
368
- const skippedState = {
369
- bugFixed: true,
370
- testsCreated: true,
371
- testsPassed: true,
372
- testsSkipped: true,
373
- testsSkippedReason: "Legacy system",
374
- documentationCreated: false,
375
- readyCheckCompleted: false,
376
- commitAndPushCompleted: false,
377
- released: false,
378
- };
379
-
380
- assert.equal(getNextStep(skippedState), "Create documentation");
381
- });
382
-
383
- test("getNextStep guides workflow progression", () => {
384
- const baseState = {
385
- bugFixed: false,
386
- testsCreated: false,
387
- testsPassed: false,
388
- documentationCreated: false,
389
- readyCheckCompleted: false,
390
- commitAndPushCompleted: false,
391
- released: false,
392
- };
393
-
394
- assert.equal(
395
- getNextStep(baseState),
396
- "Mark feature/bug as fixed"
397
- );
398
-
399
- const afterFix = { ...baseState, bugFixed: true };
400
- assert.equal(
401
- getNextStep(afterFix),
402
- "Create tests"
403
- );
404
-
405
- const afterTestsCreated = { ...afterFix, testsCreated: true };
406
- assert.equal(getNextStep(afterTestsCreated), "Run tests");
407
-
408
- const afterTests = { ...afterTestsCreated, testsPassed: true };
409
- assert.equal(
410
- getNextStep(afterTests),
411
- "Create documentation"
412
- );
413
-
414
- const afterDocs = { ...afterTests, documentationCreated: true };
415
- assert.equal(getNextStep(afterDocs), "Run 'check_ready_to_commit'");
416
-
417
- const afterReadyCheck = { ...afterDocs, readyCheckCompleted: true };
418
- assert.equal(getNextStep(afterReadyCheck), "Run 'commit_and_push'");
419
-
420
- const afterCommit = { ...afterReadyCheck, commitAndPushCompleted: true };
421
- assert.equal(getNextStep(afterCommit), "Run 'perform_release'");
422
-
423
- const afterRelease = { ...afterCommit, released: true };
424
- assert.equal(getNextStep(afterRelease), "Complete the task");
425
- });
426
-
427
- test("containsTestFilesInStatus detects modified test files", () => {
428
- const statusOutput = [
429
- " M src/app.test.js",
430
- "A tests/helpers/util.js",
431
- "R src/old.js -> src/new.spec.ts",
432
- ].join("\n");
433
-
434
- assert.equal(containsTestFilesInStatus(statusOutput), true);
435
- });
436
-
437
- test("containsTestFilesInStatus returns false when no test files present", () => {
438
- const statusOutput = [
439
- " M src/app.js",
440
- "A docs/README.md",
441
- "?? assets/logo.png",
442
- ].join("\n");
443
-
444
- assert.equal(containsTestFilesInStatus(statusOutput), false);
445
- });
446
-
447
- test("createCommitMessageParts generates test prefixed summary with details", () => {
448
- const changes = [
449
- { status: "M", path: "src/app.test.js" },
450
- { status: "A", path: "tests/helpers/util.js" },
451
- ];
452
-
453
- const { summary, body } = createCommitMessageParts(changes);
454
-
455
- assert.equal(summary.startsWith("test:"), true);
456
- assert.equal(body.includes("- modified src/app.test.js"), true);
457
- assert.equal(body.includes("- added tests/helpers/util.js"), true);
458
- });
459
-
460
- test("createCommitMessageParts respects provided summary when present", () => {
461
- const changes = [{ status: "M", path: "docs/guide.md" }];
462
- const provided = "docs: update guide";
463
-
464
- const { summary, body } = createCommitMessageParts(changes, provided);
465
-
466
- assert.equal(summary, provided);
467
- assert.equal(body.includes("- modified docs/guide.md"), true);
468
- });
469
-
470
- test("determineReleaseTypeFromCommit returns major for breaking change", () => {
471
- const message = "feat!: overhaul API\n\nBREAKING CHANGE: new behavior";
472
- assert.equal(determineReleaseTypeFromCommit(message), "major");
473
- });
474
-
475
- test("determineReleaseTypeFromCommit returns minor for feat", () => {
476
- const message = "feat: add dashboard";
477
- assert.equal(determineReleaseTypeFromCommit(message), "minor");
478
- });
479
-
480
- test("determineReleaseTypeFromCommit returns patch for fix", () => {
481
- const message = "fix: resolve login issue";
482
- assert.equal(determineReleaseTypeFromCommit(message), "patch");
483
- });
484
-
485
- test("determineReleaseTypeFromCommit defaults to patch for other commit types", () => {
486
- const message = "chore: update dependencies";
487
- assert.equal(determineReleaseTypeFromCommit(message), "patch");
488
- });
489
-
490
- test("workingTreeSummary reports no changes for empty output", () => {
491
- const summary = workingTreeSummary("\n \n");
492
- assert.equal(summary.hasChanges, false);
493
- assert.deepEqual(summary.lines, []);
494
- });
495
-
496
- test("workingTreeSummary lists trimmed status lines", () => {
497
- const statusOutput = " M src/app.js\nA tests/new.test.js\n";
498
- const summary = workingTreeSummary(statusOutput);
499
-
500
- assert.equal(summary.hasChanges, true);
501
- assert.deepEqual(summary.lines, ["M src/app.js", "A tests/new.test.js"]);
502
- });
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ mkdtemp,
5
+ rm,
6
+ mkdir,
7
+ realpath,
8
+ readFile,
9
+ writeFile,
10
+ access,
11
+ copyFile,
12
+ } from "node:fs/promises";
13
+ import { constants as fsConstants } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import path from "node:path";
16
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
+ import {
18
+ WorkflowState,
19
+ getNextStep,
20
+ containsTestFilesInStatus,
21
+ createCommitMessageParts,
22
+ determineReleaseTypeFromCommit,
23
+ workingTreeSummary,
24
+ } from "../index.js";
25
+
26
+ async function withWorkflowState(callback) {
27
+ const tempDir = await mkdtemp(path.join(tmpdir(), "workflow-state-"));
28
+ const stateFile = path.join(tempDir, "state.json");
29
+ const state = new WorkflowState(stateFile);
30
+ await state.load();
31
+
32
+ try {
33
+ await callback(state);
34
+ } finally {
35
+ await rm(tempDir, { recursive: true, force: true });
36
+ }
37
+ }
38
+
39
+ test("resolveStateFile prefers current working directory when multiple project markers exist", async () => {
40
+ const tempRoot = await mkdtemp(path.join(tmpdir(), "workflow-prefers-cwd-"));
41
+ const moduleDir = path.join(tempRoot, "module");
42
+ const projectA = path.join(tempRoot, "project-a");
43
+ const projectB = path.join(tempRoot, "project-b");
44
+ const projectBNested = path.join(projectB, "sub", "app");
45
+
46
+ await mkdir(moduleDir, { recursive: true });
47
+ await mkdir(projectA, { recursive: true });
48
+ await mkdir(projectBNested, { recursive: true });
49
+
50
+ const sourceModulePath = fileURLToPath(new URL("../workflow-state.js", import.meta.url));
51
+ await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
52
+
53
+ await writeFile(path.join(moduleDir, "package.json"), JSON.stringify({ name: "module" }));
54
+ await writeFile(path.join(projectA, "package.json"), JSON.stringify({ name: "project-a" }));
55
+ await writeFile(path.join(projectB, "package.json"), JSON.stringify({ name: "project-b" }));
56
+
57
+ const moduleUrl = pathToFileURL(path.join(moduleDir, "workflow-state.js"));
58
+ const { WorkflowState: TempWorkflowState } = await import(`${moduleUrl.href}?preferCwd=${Date.now()}`);
59
+
60
+ const originalCwd = process.cwd();
61
+ const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
62
+ const originalInitCwd = process.env.INIT_CWD;
63
+
64
+ try {
65
+ process.chdir(projectBNested);
66
+ process.env.INIT_CWD = projectA;
67
+
68
+ const state = new TempWorkflowState();
69
+ const resolvedStateFile = state.stateFile;
70
+ const resolvedProjectA = await realpath(projectA);
71
+ const resolvedProjectB = await realpath(projectB);
72
+
73
+ assert.ok(
74
+ resolvedStateFile.startsWith(path.join(resolvedProjectB, ".state", "users")),
75
+ "State file should resolve under the current working directory's project"
76
+ );
77
+
78
+ assert.ok(
79
+ !resolvedStateFile.startsWith(path.join(resolvedProjectA, ".state")),
80
+ "State file should not use INIT_CWD project when cwd has its own markers"
81
+ );
82
+ } finally {
83
+ process.chdir(originalCwd);
84
+ if (initWasSet) {
85
+ process.env.INIT_CWD = originalInitCwd;
86
+ } else {
87
+ delete process.env.INIT_CWD;
88
+ }
89
+ await rm(tempRoot, { recursive: true, force: true });
90
+ }
91
+ });
92
+
93
+ test("WorkflowState mirrors state file to compatibility locations", async () => {
94
+ const tempRoot = await mkdtemp(path.join(tmpdir(), "workflow-compat-"));
95
+ const moduleDir = path.join(tempRoot, "module");
96
+ const projectRoot = path.join(tempRoot, "project");
97
+ const stateDir = path.join(projectRoot, ".state");
98
+ const sourceModulePath = fileURLToPath(new URL("../workflow-state.js", import.meta.url));
99
+ await mkdir(moduleDir, { recursive: true });
100
+ await mkdir(path.join(moduleDir, "dist"), { recursive: true });
101
+ await mkdir(path.join(tempRoot, "dist"), { recursive: true });
102
+ await mkdir(stateDir, { recursive: true });
103
+ await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
104
+ await writeFile(path.join(projectRoot, "package.json"), JSON.stringify({ name: "temp" }));
105
+
106
+ const stateFile = path.join(stateDir, "workflow-state.json");
107
+ await writeFile(stateFile, JSON.stringify({ currentPhase: "testing" }, null, 2));
108
+
109
+ const moduleUrl = pathToFileURL(path.join(moduleDir, "workflow-state.js"));
110
+ const { WorkflowState: TempWorkflowState } = await import(moduleUrl.href);
111
+
112
+ const state = new TempWorkflowState(stateFile);
113
+ await state.load();
114
+ state.state.currentPhase = "commit";
115
+ await state.save();
116
+
117
+ // Ensure primary state file was written correctly
118
+ const primaryContent = await readFile(stateFile, "utf-8");
119
+ const primaryParsed = JSON.parse(primaryContent);
120
+ assert.equal(primaryParsed.currentPhase, "commit", "primary state file should be updated");
121
+
122
+ await rm(tempRoot, { recursive: true, force: true });
123
+ });
124
+
125
+ test("WorkflowState avoids filesystem root when no project markers found", async () => {
126
+ const tempRoot = await mkdtemp(path.join(tmpdir(), "workflow-rootless-"));
127
+ const moduleDir = path.join(tempRoot, "module");
128
+ await mkdir(moduleDir, { recursive: true });
129
+
130
+ const sourceModulePath = fileURLToPath(new URL("../workflow-state.js", import.meta.url));
131
+ await copyFile(sourceModulePath, path.join(moduleDir, "workflow-state.js"));
132
+
133
+ const moduleUrl = pathToFileURL(path.join(moduleDir, "workflow-state.js"));
134
+ const { WorkflowState: TempWorkflowState } = await import(moduleUrl.href);
135
+
136
+ const originalCwd = process.cwd();
137
+ const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
138
+ const originalInitCwd = process.env.INIT_CWD;
139
+
140
+ try {
141
+ const cwdWithoutMarkers = path.join(tempRoot, "random", "deep");
142
+ await mkdir(cwdWithoutMarkers, { recursive: true });
143
+
144
+ process.chdir(cwdWithoutMarkers);
145
+ process.env.INIT_CWD = cwdWithoutMarkers;
146
+
147
+ const state = new TempWorkflowState();
148
+ const resolvedTempRoot = await realpath(tempRoot);
149
+
150
+ assert.ok(
151
+ state.stateFile.startsWith(resolvedTempRoot),
152
+ "state file should remain inside the temporary root when no project markers exist"
153
+ );
154
+ assert.ok(
155
+ !state.stateFile.startsWith(path.join(path.sep, ".state")),
156
+ "state file should not resolve to filesystem root"
157
+ );
158
+ } finally {
159
+ process.chdir(originalCwd);
160
+ if (initWasSet) {
161
+ process.env.INIT_CWD = originalInitCwd;
162
+ } else {
163
+ delete process.env.INIT_CWD;
164
+ }
165
+
166
+ await rm(tempRoot, { recursive: true, force: true });
167
+ }
168
+ });
169
+
170
+ test("resolveUserId persists generated ID", async () => {
171
+ const projectRoot = await mkdtemp(path.join(tmpdir(), "workflow-userid-"));
172
+ await writeFile(path.join(projectRoot, "package.json"), JSON.stringify({ name: "temp" }));
173
+
174
+ const originalCwd = process.cwd();
175
+ const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
176
+ const originalInitCwd = process.env.INIT_CWD;
177
+ const userEnvWasSet = Object.prototype.hasOwnProperty.call(process.env, "DEV_WORKFLOW_USER_ID");
178
+ const originalUserEnv = process.env.DEV_WORKFLOW_USER_ID;
179
+
180
+ try {
181
+ process.chdir(projectRoot);
182
+ process.env.INIT_CWD = projectRoot;
183
+ if (userEnvWasSet) {
184
+ delete process.env.DEV_WORKFLOW_USER_ID;
185
+ }
186
+
187
+ const firstModuleHref = new URL("../workflow-state.js?resolveUserId1", import.meta.url).href;
188
+ const { WorkflowState: FirstWorkflowState } = await import(firstModuleHref);
189
+
190
+ const state = new FirstWorkflowState();
191
+ await state.ensurePrimaryFile();
192
+
193
+ const userDir = path.dirname(state.stateFile);
194
+ const userId = path.basename(userDir);
195
+ assert.ok(userId.startsWith("user-"), "Generated user ID should use user- prefix");
196
+
197
+ const userIdFile = path.join(projectRoot, ".state", "user-id");
198
+ const persistedId = (await readFile(userIdFile, "utf-8")).trim();
199
+ assert.equal(persistedId, userId, "user-id file should match generated user ID");
200
+
201
+ const secondModuleHref = new URL("../workflow-state.js?resolveUserId2", import.meta.url).href;
202
+ const { WorkflowState: SecondWorkflowState } = await import(secondModuleHref);
203
+
204
+ const reloadedState = new SecondWorkflowState();
205
+ await reloadedState.ensurePrimaryFile();
206
+
207
+ assert.equal(reloadedState.stateFile, state.stateFile, "State file path should persist across reloads");
208
+ const reloadedUserId = path.basename(path.dirname(reloadedState.stateFile));
209
+ assert.equal(reloadedUserId, userId, "Reloaded module should reuse persisted user ID");
210
+ } finally {
211
+ process.chdir(originalCwd);
212
+ if (initWasSet) {
213
+ process.env.INIT_CWD = originalInitCwd;
214
+ } else {
215
+ delete process.env.INIT_CWD;
216
+ }
217
+ if (userEnvWasSet) {
218
+ process.env.DEV_WORKFLOW_USER_ID = originalUserEnv;
219
+ } else {
220
+ delete process.env.DEV_WORKFLOW_USER_ID;
221
+ }
222
+ await rm(projectRoot, { recursive: true, force: true });
223
+ }
224
+ });
225
+
226
+ test("WorkflowState generates project summary on load and updates on save", async () => {
227
+ await withWorkflowState(async (state) => {
228
+ const summaryPath = path.join(path.dirname(state.stateFile), "project-summary.json");
229
+
230
+ const initialSummary = JSON.parse(await readFile(summaryPath, "utf-8"));
231
+ assert.equal(initialSummary.totalTasks, 0);
232
+ assert.deepEqual(initialSummary.taskTypes, {});
233
+
234
+ state.addToHistory({
235
+ taskDescription: "Seed summary",
236
+ taskType: "feature",
237
+ commitMessage: "feat: seed",
238
+ });
239
+ await state.save();
240
+
241
+ const updatedSummary = JSON.parse(await readFile(summaryPath, "utf-8"));
242
+ assert.equal(updatedSummary.totalTasks, 1);
243
+ assert.equal(updatedSummary.taskTypes.feature, 1);
244
+ assert.equal(updatedSummary.recentTasks[0].description, "Seed summary");
245
+ });
246
+ });
247
+
248
+ test("WorkflowState persists data across save/load", async () => {
249
+ await withWorkflowState(async (state) => {
250
+ state.state.currentPhase = "testing";
251
+ state.state.taskDescription = "Test persistence";
252
+ state.state.bugFixed = true;
253
+ state.state.testsCreated = true;
254
+ state.state.readyCheckCompleted = true;
255
+ state.state.released = true;
256
+ state.state.releaseCommand = "npm run release";
257
+ state.state.releaseNotes = "Automated release";
258
+ state.state.commitAndPushCompleted = true;
259
+ state.state.lastCommitMessage = "test: persistence";
260
+ state.state.lastPushBranch = "main";
261
+ state.state.testsSkipped = true;
262
+ state.state.testsSkippedReason = "Legacy app uses manual QA";
263
+ await state.save();
264
+
265
+ const reloaded = new WorkflowState(state.stateFile);
266
+ await reloaded.load();
267
+
268
+ assert.equal(reloaded.state.currentPhase, "testing");
269
+ assert.equal(reloaded.state.taskDescription, "Test persistence");
270
+ assert.equal(reloaded.state.bugFixed, true);
271
+ assert.equal(reloaded.state.testsCreated, true);
272
+ assert.equal(reloaded.state.readyCheckCompleted, true);
273
+ assert.equal(reloaded.state.released, true);
274
+ assert.equal(reloaded.state.releaseCommand, "npm run release");
275
+ assert.equal(reloaded.state.releaseNotes, "Automated release");
276
+ assert.equal(reloaded.state.commitAndPushCompleted, true);
277
+ assert.equal(reloaded.state.lastCommitMessage, "test: persistence");
278
+ assert.equal(reloaded.state.lastPushBranch, "main");
279
+ assert.equal(reloaded.state.testsSkipped, true);
280
+ assert.equal(
281
+ reloaded.state.testsSkippedReason,
282
+ "Legacy app uses manual QA"
283
+ );
284
+ });
285
+ });
286
+
287
+ test("WorkflowState resolves state file relative to project root", async () => {
288
+ const projectRoot = await mkdtemp(path.join(tmpdir(), "workflow-project-"));
289
+ const originalCwd = process.cwd();
290
+ const initWasSet = Object.prototype.hasOwnProperty.call(process.env, "INIT_CWD");
291
+ const originalInitCwd = process.env.INIT_CWD;
292
+ const devStateWasSet = Object.prototype.hasOwnProperty.call(
293
+ process.env,
294
+ "DEV_WORKFLOW_STATE_FILE"
295
+ );
296
+ const originalDevState = process.env.DEV_WORKFLOW_STATE_FILE;
297
+
298
+ try {
299
+ await mkdir(path.join(projectRoot, ".git"));
300
+ const nestedDir = path.join(projectRoot, "dist", "server");
301
+ await mkdir(nestedDir, { recursive: true });
302
+
303
+ process.chdir(projectRoot);
304
+ process.env.INIT_CWD = projectRoot;
305
+ if (devStateWasSet) {
306
+ delete process.env.DEV_WORKFLOW_STATE_FILE;
307
+ }
308
+ const resolvedProjectRoot = await realpath(projectRoot);
309
+
310
+ const state = new WorkflowState();
311
+ assert.ok(
312
+ state.stateFile.startsWith(path.join(resolvedProjectRoot, ".state", "users")),
313
+ "state file should resolve under project .state/users directory"
314
+ );
315
+
316
+ process.chdir(nestedDir);
317
+ delete process.env.INIT_CWD;
318
+ delete process.env.DEV_WORKFLOW_STATE_FILE;
319
+
320
+ const state2 = new WorkflowState();
321
+ assert.ok(
322
+ state2.stateFile.startsWith(path.join(resolvedProjectRoot, ".state", "users")),
323
+ "state file should resolve to project root"
324
+ );
325
+ } finally {
326
+ process.chdir(originalCwd);
327
+ if (initWasSet) {
328
+ process.env.INIT_CWD = originalInitCwd;
329
+ } else {
330
+ delete process.env.INIT_CWD;
331
+ }
332
+ if (devStateWasSet) {
333
+ process.env.DEV_WORKFLOW_STATE_FILE = originalDevState;
334
+ } else {
335
+ delete process.env.DEV_WORKFLOW_STATE_FILE;
336
+ }
337
+ await rm(projectRoot, { recursive: true, force: true });
338
+ }
339
+ });
340
+
341
+ test("WorkflowState reset keeps history but clears progress", async () => {
342
+ await withWorkflowState(async (state) => {
343
+ state.state.currentPhase = "ready";
344
+ state.state.testsCreated = true;
345
+ state.state.testsPassed = true;
346
+ state.state.documentationCreated = true;
347
+ state.state.readyCheckCompleted = true;
348
+ state.state.released = true;
349
+ state.state.commitAndPushCompleted = true;
350
+ state.state.testsSkipped = true;
351
+ state.state.testsSkippedReason = "Manual QA";
352
+ state.addToHistory({ taskDescription: "Sample task" });
353
+
354
+ state.reset();
355
+
356
+ assert.equal(state.state.currentPhase, "idle");
357
+ assert.equal(state.state.testsCreated, false);
358
+ assert.equal(state.state.testsPassed, false);
359
+ assert.equal(state.state.documentationCreated, false);
360
+ assert.equal(state.state.readyCheckCompleted, false);
361
+ assert.equal(state.state.released, false);
362
+ assert.equal(state.state.commitAndPushCompleted, false);
363
+ assert.equal(state.state.testsSkipped, false);
364
+ assert.equal(state.state.testsSkippedReason, "");
365
+ assert.equal(state.state.history.length, 1);
366
+ assert.equal(state.state.history[0].taskDescription, "Sample task");
367
+ });
368
+ });
369
+
370
+ test("getNextStep treats skipped tests as satisfied", () => {
371
+ const skippedState = {
372
+ bugFixed: true,
373
+ testsCreated: true,
374
+ testsPassed: true,
375
+ testsSkipped: true,
376
+ testsSkippedReason: "Legacy system",
377
+ documentationCreated: false,
378
+ readyCheckCompleted: false,
379
+ commitAndPushCompleted: false,
380
+ released: false,
381
+ };
382
+
383
+ assert.equal(getNextStep(skippedState), "Create documentation");
384
+ });
385
+
386
+ test("getNextStep guides workflow progression", () => {
387
+ const baseState = {
388
+ bugFixed: false,
389
+ testsCreated: false,
390
+ testsPassed: false,
391
+ documentationCreated: false,
392
+ readyCheckCompleted: false,
393
+ commitAndPushCompleted: false,
394
+ released: false,
395
+ };
396
+
397
+ assert.equal(
398
+ getNextStep(baseState),
399
+ "Mark feature/bug as fixed"
400
+ );
401
+
402
+ const afterFix = { ...baseState, bugFixed: true };
403
+ assert.equal(
404
+ getNextStep(afterFix),
405
+ "Create tests"
406
+ );
407
+
408
+ const afterTestsCreated = { ...afterFix, testsCreated: true };
409
+ assert.equal(getNextStep(afterTestsCreated), "Run tests");
410
+
411
+ const afterTests = { ...afterTestsCreated, testsPassed: true };
412
+ assert.equal(
413
+ getNextStep(afterTests),
414
+ "Create documentation"
415
+ );
416
+
417
+ const afterDocs = { ...afterTests, documentationCreated: true };
418
+ assert.equal(getNextStep(afterDocs), "Run 'check_ready_to_commit'");
419
+
420
+ const afterReadyCheck = { ...afterDocs, readyCheckCompleted: true };
421
+ assert.equal(getNextStep(afterReadyCheck), "Run 'commit_and_push'");
422
+
423
+ const afterCommit = { ...afterReadyCheck, commitAndPushCompleted: true };
424
+ assert.equal(getNextStep(afterCommit), "Run 'perform_release'");
425
+
426
+ const afterRelease = { ...afterCommit, released: true };
427
+ assert.equal(getNextStep(afterRelease), "Complete the task");
428
+ });
429
+
430
+ test("containsTestFilesInStatus detects modified test files", () => {
431
+ const statusOutput = [
432
+ " M src/app.test.js",
433
+ "A tests/helpers/util.js",
434
+ "R src/old.js -> src/new.spec.ts",
435
+ ].join("\n");
436
+
437
+ assert.equal(containsTestFilesInStatus(statusOutput), true);
438
+ });
439
+
440
+ test("containsTestFilesInStatus returns false when no test files present", () => {
441
+ const statusOutput = [
442
+ " M src/app.js",
443
+ "A docs/README.md",
444
+ "?? assets/logo.png",
445
+ ].join("\n");
446
+
447
+ assert.equal(containsTestFilesInStatus(statusOutput), false);
448
+ });
449
+
450
+ test("createCommitMessageParts generates test prefixed summary with details", () => {
451
+ const changes = [
452
+ { status: "M", path: "src/app.test.js" },
453
+ { status: "A", path: "tests/helpers/util.js" },
454
+ ];
455
+
456
+ const { summary, body } = createCommitMessageParts(changes);
457
+
458
+ assert.equal(summary.startsWith("test:"), true);
459
+ assert.equal(body.includes("- modified src/app.test.js"), true);
460
+ assert.equal(body.includes("- added tests/helpers/util.js"), true);
461
+ });
462
+
463
+ test("createCommitMessageParts respects provided summary when present", () => {
464
+ const changes = [{ status: "M", path: "docs/guide.md" }];
465
+ const provided = "docs: update guide";
466
+
467
+ const { summary, body } = createCommitMessageParts(changes, provided);
468
+
469
+ assert.equal(summary, provided);
470
+ assert.equal(body.includes("- modified docs/guide.md"), true);
471
+ });
472
+
473
+ test("determineReleaseTypeFromCommit returns major for breaking change", () => {
474
+ const message = "feat!: overhaul API\n\nBREAKING CHANGE: new behavior";
475
+ assert.equal(determineReleaseTypeFromCommit(message), "major");
476
+ });
477
+
478
+ test("determineReleaseTypeFromCommit returns minor for feat", () => {
479
+ const message = "feat: add dashboard";
480
+ assert.equal(determineReleaseTypeFromCommit(message), "minor");
481
+ });
482
+
483
+ test("determineReleaseTypeFromCommit returns patch for fix", () => {
484
+ const message = "fix: resolve login issue";
485
+ assert.equal(determineReleaseTypeFromCommit(message), "patch");
486
+ });
487
+
488
+ test("determineReleaseTypeFromCommit defaults to patch for other commit types", () => {
489
+ const message = "chore: update dependencies";
490
+ assert.equal(determineReleaseTypeFromCommit(message), "patch");
491
+ });
492
+
493
+ test("workingTreeSummary reports no changes for empty output", () => {
494
+ const summary = workingTreeSummary("\n \n");
495
+ assert.equal(summary.hasChanges, false);
496
+ assert.deepEqual(summary.lines, []);
497
+ });
498
+
499
+ test("workingTreeSummary lists trimmed status lines", () => {
500
+ const statusOutput = " M src/app.js\nA tests/new.test.js\n";
501
+ const summary = workingTreeSummary(statusOutput);
502
+
503
+ assert.equal(summary.hasChanges, true);
504
+ assert.deepEqual(summary.lines, ["M src/app.js", "A tests/new.test.js"]);
505
+ });