@programinglive/dev-workflow-mcp-server 1.7.16 → 1.8.0
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 +25 -0
- package/README.md +3 -2
- package/docs/release-notes/RELEASE_NOTES.md +10 -0
- package/git-helpers.js +9 -0
- package/package.json +4 -2
- package/scripts/release.cjs +28 -1
- package/tests/build.test.js +1 -1
- package/tests/handlers-commit.test.js +174 -0
- package/tests/handlers-release.test.js +212 -0
- package/tests/handlers-workflow.test.js +68 -0
- package/tests/handlers.test.js +79 -0
- package/tests/test-helpers.js +50 -0
- package/tools/definitions.js +15 -0
- package/tools/handler-modules/commit-handlers.js +159 -0
- package/tools/handler-modules/index.js +222 -0
- package/tools/handler-modules/release-handlers.js +193 -0
- package/tools/handler-modules/shared.js +187 -0
- package/tools/handler-modules/status-handlers.js +134 -0
- package/tools/handler-modules/summary-handlers.js +46 -0
- package/tools/handler-modules/workflow-handlers.js +219 -0
- package/tools/handlers-new.js +1 -0
- package/tools/handlers.js +1 -1030
- package/workflow-state.js +10 -0
- /package/docs/{product/PRD.md → PRD.md} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
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.8.0](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.7.17...v1.8.0) (2025-12-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ♻️ Refactors
|
|
9
|
+
|
|
10
|
+
* complete handlers.js modularization and fix all tests ([af5b3d6](https://github.com/programinglive/dev-workflow-mcp-server/commit/af5b3d60db025611e9a84a27ccc78f5999d0ead7))
|
|
11
|
+
* split handlers.test.js into smaller focused test modules ([321bbe5](https://github.com/programinglive/dev-workflow-mcp-server/commit/321bbe50d34348202f9e096e5b1903cae519dac6))
|
|
12
|
+
|
|
13
|
+
### [1.7.17](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.7.16...v1.7.17) (2025-12-26)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### 📝 Documentation
|
|
17
|
+
|
|
18
|
+
* update release notes for v1.7.16 ([0afd96b](https://github.com/programinglive/dev-workflow-mcp-server/commit/0afd96b754717953eb162f65426129457fc8626e))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### ✨ Features
|
|
22
|
+
|
|
23
|
+
* allow skipping release step ([004e4fa](https://github.com/programinglive/dev-workflow-mcp-server/commit/004e4fae071e064b2c2efb9d69ecd97e794d3cb8))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### 🧹 Chores
|
|
27
|
+
|
|
28
|
+
* add @programinglive/commiter setup ([1d2a19d](https://github.com/programinglive/dev-workflow-mcp-server/commit/1d2a19d0453b50c6f0db35b15924b51a27c4891a))
|
|
29
|
+
|
|
5
30
|
### [1.7.16](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.7.15...v1.7.16) (2025-12-24)
|
|
6
31
|
|
|
7
32
|
|
package/README.md
CHANGED
|
@@ -512,7 +512,7 @@ This keeps workflow state local to each developer's machine.
|
|
|
512
512
|
- `create_documentation` - Mark documentation as created
|
|
513
513
|
- `check_ready_to_commit` - Verify all steps are complete
|
|
514
514
|
- `commit_and_push` - Commit and push changes
|
|
515
|
-
- `perform_release` - Record release details
|
|
515
|
+
- `perform_release` - Record release details (or use `skip_release` when the project has no release automation)
|
|
516
516
|
- `complete_task` - Mark task as complete and reset
|
|
517
517
|
- `force_complete_task` - Force completion with reason
|
|
518
518
|
- `drop_task` - Abandon current task
|
|
@@ -553,6 +553,7 @@ The tool will:
|
|
|
553
553
|
5. `check_ready_to_commit`
|
|
554
554
|
6. `commit_and_push` with `commitMessage` and optional `branch`
|
|
555
555
|
7. `perform_release` with `releaseCommand`, plus optional `releaseNotes`, `releaseType`, and `preset`
|
|
556
|
+
- Alternatively call `skip_release` with a justification when the repository has no Node-based release step (e.g., Python-only or docs-only tasks)
|
|
556
557
|
8. `complete_task` reusing `commitMessage`
|
|
557
558
|
|
|
558
559
|
All arguments except the optional flags are required and must be non-empty strings.
|
|
@@ -571,7 +572,7 @@ The package ships with a release guard (`release-wrapper.js`) that backs the `np
|
|
|
571
572
|
|
|
572
573
|
If any requirements are missing, the guard exits with guidance to return to the MCP tools. This prevents accidentally bumping versions or tagging releases outside the managed workflow. To release correctly:
|
|
573
574
|
|
|
574
|
-
1. Use `perform_release {"command":"patch"}` (or `minor`/`major`) via the MCP client.
|
|
575
|
+
1. Use `perform_release {"command":"patch"}` (or `minor`/`major`) via the MCP client, or `skip_release {"reason":"<explanation>"}` if no release applies.
|
|
575
576
|
2. The guard runs automatically, verifies the workflow state, and records the release before letting you finish with `complete_task`.
|
|
576
577
|
|
|
577
578
|
### Automated npm Publishing
|
|
@@ -4,6 +4,7 @@ Authoritative history of user-visible changes for the Development Workflow MCP S
|
|
|
4
4
|
|
|
5
5
|
| Version | Date | Notes |
|
|
6
6
|
|---------|------|-------|
|
|
7
|
+
| 1.7.17 | 2025-12-26 | update release notes for v1.7.16 (0afd96b) |
|
|
7
8
|
| 1.7.16 | 2025-12-24 | align database schema with dashboard and improve user ID handling (69e7f38) |
|
|
8
9
|
| 1.7.15 | 2025-12-19 | optimize startup performance and add database connection resilience (bff2714) |
|
|
9
10
|
| 1.7.14 | 2025-12-19 | add API dependencies and update login to use API client (94bb42f) |
|
|
@@ -44,6 +45,15 @@ Authoritative history of user-visible changes for the Development Workflow MCP S
|
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## 1.7.17 – 📝 Documentation
|
|
51
|
+
|
|
52
|
+
Released on **2025-12-26**.
|
|
53
|
+
|
|
54
|
+
- update release notes for v1.7.16 (0afd96b)
|
|
55
|
+
- allow skipping release step (004e4fa)
|
|
56
|
+
- add @programinglive/commiter setup (1d2a19d)
|
|
47
57
|
|
|
48
58
|
## 1.7.16 – 🐛 Bug Fixes
|
|
49
59
|
|
package/git-helpers.js
CHANGED
|
@@ -151,6 +151,15 @@ export async function getCurrentBranch() {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
export async function getStatusOutput() {
|
|
155
|
+
try {
|
|
156
|
+
const { stdout } = await exec("git status --porcelain");
|
|
157
|
+
return stdout;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return "";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
154
163
|
export async function getPrimaryBranch() {
|
|
155
164
|
try {
|
|
156
165
|
// Check if 'main' exists on origin
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@programinglive/dev-workflow-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "MCP server to enforce development workflow discipline",
|
|
5
5
|
"homepage": "https://devworkflow.programinglive.com/",
|
|
6
6
|
"mcpName": "io.github.programinglive/dev-workflow-mcp-server",
|
|
@@ -62,11 +62,13 @@
|
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@commitlint/cli": "^20.2.0",
|
|
64
64
|
"@commitlint/config-conventional": "^20.2.0",
|
|
65
|
-
"@programinglive/commiter": "^1.2.
|
|
65
|
+
"@programinglive/commiter": "^1.2.17",
|
|
66
66
|
"autoprefixer": "^10.4.22",
|
|
67
|
+
"cross-env": "^10.1.0",
|
|
67
68
|
"husky": "^9.1.7",
|
|
68
69
|
"postcss": "^8.5.6",
|
|
69
70
|
"standard-version": "^9.5.0",
|
|
71
|
+
"supertest": "^7.1.4",
|
|
70
72
|
"tailwindcss": "^3.4.18"
|
|
71
73
|
},
|
|
72
74
|
"standard-version": {
|
package/scripts/release.cjs
CHANGED
|
@@ -11,6 +11,9 @@ const FS_FOK_PRELOAD_FLAG = buildPreloadFlag(fsFokPreloadPath);
|
|
|
11
11
|
|
|
12
12
|
require('./preload/fs-f-ok.cjs');
|
|
13
13
|
|
|
14
|
+
const DEFAULT_PRD_RELATIVE_PATH = path.join('docs', 'PRD.md');
|
|
15
|
+
const DEFAULT_PRD_DISPLAY_PATH = 'docs/PRD.md';
|
|
16
|
+
|
|
14
17
|
const VALID_RELEASE_TYPES = new Set([
|
|
15
18
|
'major',
|
|
16
19
|
'minor',
|
|
@@ -161,6 +164,8 @@ function runRelease({
|
|
|
161
164
|
throw new Error('Working tree has uncommitted changes. Commit or stash before running release.');
|
|
162
165
|
}
|
|
163
166
|
|
|
167
|
+
ensurePrdPresence({ cwd, dependencies });
|
|
168
|
+
|
|
164
169
|
const testResult = runProjectTests({ spawn, env });
|
|
165
170
|
if (testResult && typeof testResult.status === 'number' && testResult.status !== 0) {
|
|
166
171
|
return testResult;
|
|
@@ -219,9 +224,31 @@ module.exports = {
|
|
|
219
224
|
detectPackageManager,
|
|
220
225
|
runProjectTests,
|
|
221
226
|
runRelease,
|
|
222
|
-
isWorkingTreeClean
|
|
227
|
+
isWorkingTreeClean,
|
|
228
|
+
ensurePrdPresence
|
|
223
229
|
};
|
|
224
230
|
|
|
231
|
+
function ensurePrdPresence({ cwd = process.cwd(), dependencies = {} } = {}) {
|
|
232
|
+
const {
|
|
233
|
+
fsExistsSync = fs.existsSync,
|
|
234
|
+
logger = console,
|
|
235
|
+
prdRelativePath = DEFAULT_PRD_RELATIVE_PATH,
|
|
236
|
+
prdDisplayPath = DEFAULT_PRD_DISPLAY_PATH
|
|
237
|
+
} = dependencies;
|
|
238
|
+
|
|
239
|
+
const resolvedPath = path.isAbsolute(prdRelativePath)
|
|
240
|
+
? prdRelativePath
|
|
241
|
+
: path.join(cwd, prdRelativePath);
|
|
242
|
+
|
|
243
|
+
if (fsExistsSync(resolvedPath)) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const warn = typeof logger?.warn === 'function' ? logger.warn.bind(logger) : console.warn.bind(console);
|
|
248
|
+
warn(`⚠️ Product Requirements Document missing (${prdDisplayPath}). Add ${DEFAULT_PRD_DISPLAY_PATH} so teams understand release expectations.`);
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
225
252
|
function buildPreloadFlag(filePath) {
|
|
226
253
|
const resolved = path.resolve(filePath);
|
|
227
254
|
const escaped = resolved.includes(' ')
|
package/tests/build.test.js
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'path';
|
|
|
8
8
|
const exec = promisify(execCallback);
|
|
9
9
|
|
|
10
10
|
test('build generates dist/index.mjs file', async () => {
|
|
11
|
-
const { stderr } = await exec('NODE_ENV=development npm run build:server');
|
|
11
|
+
const { stderr } = await exec('npx cross-env NODE_ENV=development npm run build:server');
|
|
12
12
|
// Allow the known vite warning about node:process
|
|
13
13
|
const knownWarning = '[plugin vite:resolve] Module "node:process" has been externalized for browser compatibility';
|
|
14
14
|
const tailwindOutput = '≈ tailwindcss';
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { handleToolCall } from "../tools/handlers.js";
|
|
4
|
+
import { withWorkflowState, testUtils, createRequest, createGitMock } from "./test-helpers.js";
|
|
5
|
+
|
|
6
|
+
test("commit_and_push commits changes and pushes", async () => {
|
|
7
|
+
await withWorkflowState(async (workflowState) => {
|
|
8
|
+
workflowState.state.readyToCommit = true;
|
|
9
|
+
workflowState.state.readyCheckCompleted = true;
|
|
10
|
+
workflowState.state.testsSkipped = false;
|
|
11
|
+
workflowState.state.testsCreated = true;
|
|
12
|
+
workflowState.state.testsPassed = true;
|
|
13
|
+
workflowState.state.currentPhase = "commit";
|
|
14
|
+
await workflowState.save();
|
|
15
|
+
|
|
16
|
+
const commands = [];
|
|
17
|
+
const execStub = async (command) => {
|
|
18
|
+
commands.push(command);
|
|
19
|
+
return { stdout: "" };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const response = await handleToolCall({
|
|
23
|
+
request: createRequest("commit_and_push", { commitMessage: "" }),
|
|
24
|
+
normalizeRequestArgs: (args) => ({ args, error: null }),
|
|
25
|
+
workflowState,
|
|
26
|
+
exec: execStub,
|
|
27
|
+
git: createGitMock({
|
|
28
|
+
hasWorkingChanges: async () => true,
|
|
29
|
+
getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
|
|
30
|
+
getStatusOutput: async () => "M tests/handlers.test.js\nM src/index.js",
|
|
31
|
+
containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
|
|
32
|
+
}),
|
|
33
|
+
utils: testUtils,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const pushCommand = commands.find((command) => command.startsWith("git push"));
|
|
37
|
+
assert.ok(pushCommand, "git push should be executed during commit");
|
|
38
|
+
assert.ok(
|
|
39
|
+
pushCommand.startsWith("git push origin"),
|
|
40
|
+
"push command should target origin"
|
|
41
|
+
);
|
|
42
|
+
assert.equal(
|
|
43
|
+
commands.some((command) => command.startsWith("git commit")),
|
|
44
|
+
true,
|
|
45
|
+
"git commit should be executed"
|
|
46
|
+
);
|
|
47
|
+
assert.ok(
|
|
48
|
+
response.content[0].text.includes("Commit and push completed!"),
|
|
49
|
+
"response should confirm commit and push"
|
|
50
|
+
);
|
|
51
|
+
assert.equal(workflowState.state.commitAndPushCompleted, true);
|
|
52
|
+
assert.equal(workflowState.state.lastCommitMessage.length > 0, true);
|
|
53
|
+
assert.equal(workflowState.state.currentPhase, "release");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("commit_and_push uses primary branch when no branch specified", async () => {
|
|
58
|
+
await withWorkflowState(async (workflowState) => {
|
|
59
|
+
workflowState.state.readyToCommit = true;
|
|
60
|
+
workflowState.state.readyCheckCompleted = true;
|
|
61
|
+
workflowState.state.testsSkipped = false;
|
|
62
|
+
workflowState.state.testsCreated = true;
|
|
63
|
+
workflowState.state.testsPassed = true;
|
|
64
|
+
workflowState.state.currentPhase = "commit";
|
|
65
|
+
await workflowState.save();
|
|
66
|
+
|
|
67
|
+
const commands = [];
|
|
68
|
+
const execStub = async (command) => {
|
|
69
|
+
commands.push(command);
|
|
70
|
+
return { stdout: "" };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const response = await handleToolCall({
|
|
74
|
+
request: createRequest("commit_and_push", { commitMessage: "feat: test" }),
|
|
75
|
+
normalizeRequestArgs: (args) => ({ args, error: null }),
|
|
76
|
+
workflowState,
|
|
77
|
+
exec: execStub,
|
|
78
|
+
git: createGitMock({
|
|
79
|
+
hasWorkingChanges: async () => true,
|
|
80
|
+
getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
|
|
81
|
+
getCurrentBranch: async () => "feature/test",
|
|
82
|
+
getStatusOutput: async () => "M tests/handlers.test.js\nM src/index.js",
|
|
83
|
+
containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
|
|
84
|
+
}),
|
|
85
|
+
utils: testUtils,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const pushCommand = commands.find((command) => command.startsWith("git push"));
|
|
89
|
+
assert.ok(pushCommand, "git push should be executed");
|
|
90
|
+
assert.ok(
|
|
91
|
+
/origin\s+['"]?main['"]?/.test(pushCommand),
|
|
92
|
+
"push should use primary branch (main) when no branch specified"
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("commit_and_push falls back to master when main not found", async () => {
|
|
98
|
+
await withWorkflowState(async (workflowState) => {
|
|
99
|
+
workflowState.state.readyToCommit = true;
|
|
100
|
+
workflowState.state.readyCheckCompleted = true;
|
|
101
|
+
workflowState.state.testsSkipped = false;
|
|
102
|
+
workflowState.state.testsCreated = true;
|
|
103
|
+
workflowState.state.testsPassed = true;
|
|
104
|
+
workflowState.state.currentPhase = "commit";
|
|
105
|
+
await workflowState.save();
|
|
106
|
+
|
|
107
|
+
const commands = [];
|
|
108
|
+
const execStub = async (command) => {
|
|
109
|
+
commands.push(command);
|
|
110
|
+
return { stdout: "" };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const response = await handleToolCall({
|
|
114
|
+
request: createRequest("commit_and_push", { commitMessage: "feat: test" }),
|
|
115
|
+
normalizeRequestArgs: (args) => ({ args, error: null }),
|
|
116
|
+
workflowState,
|
|
117
|
+
exec: execStub,
|
|
118
|
+
git: createGitMock({
|
|
119
|
+
hasWorkingChanges: async () => true,
|
|
120
|
+
getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
|
|
121
|
+
getPrimaryBranch: async () => "master",
|
|
122
|
+
getStatusOutput: async () => "M tests/handlers.test.js\nM src/index.js",
|
|
123
|
+
containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
|
|
124
|
+
}),
|
|
125
|
+
utils: testUtils,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const pushCommand = commands.find((command) => command.startsWith("git push"));
|
|
129
|
+
assert.ok(pushCommand, "git push should be executed");
|
|
130
|
+
assert.ok(
|
|
131
|
+
/origin\s+['"]?master['"]?/.test(pushCommand),
|
|
132
|
+
"push should use master when main not found"
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("commit_and_push recognizes already committed work", async () => {
|
|
138
|
+
await withWorkflowState(async (workflowState) => {
|
|
139
|
+
workflowState.state.readyToCommit = true;
|
|
140
|
+
workflowState.state.readyCheckCompleted = true;
|
|
141
|
+
workflowState.state.testsSkipped = false;
|
|
142
|
+
workflowState.state.testsCreated = true;
|
|
143
|
+
workflowState.state.testsPassed = true;
|
|
144
|
+
workflowState.state.currentPhase = "commit";
|
|
145
|
+
await workflowState.save();
|
|
146
|
+
|
|
147
|
+
const commands = [];
|
|
148
|
+
const execStub = async (command) => {
|
|
149
|
+
commands.push(command);
|
|
150
|
+
return { stdout: "" };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const response = await handleToolCall({
|
|
154
|
+
request: createRequest("commit_and_push", { commitMessage: "" }),
|
|
155
|
+
normalizeRequestArgs: (args) => ({ args, error: null }),
|
|
156
|
+
workflowState,
|
|
157
|
+
exec: execStub,
|
|
158
|
+
git: createGitMock({
|
|
159
|
+
hasWorkingChanges: async () => false,
|
|
160
|
+
getLastCommitMessage: async () => "fix: previous work",
|
|
161
|
+
getStatusOutput: async () => "",
|
|
162
|
+
containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
|
|
163
|
+
}),
|
|
164
|
+
utils: testUtils,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
assert.ok(
|
|
168
|
+
response.content[0].text.includes("No changes detected"),
|
|
169
|
+
"response should indicate no changes"
|
|
170
|
+
);
|
|
171
|
+
assert.equal(workflowState.state.commitAndPushCompleted, true);
|
|
172
|
+
assert.equal(workflowState.state.currentPhase, "release");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { handleToolCall } from "../tools/handlers.js";
|
|
4
|
+
import { withWorkflowState, testUtils, createRequest, createGitMock } from "./test-helpers.js";
|
|
5
|
+
|
|
6
|
+
test("perform_release runs release command before pushing with tags", async () => {
|
|
7
|
+
await withWorkflowState(async (workflowState) => {
|
|
8
|
+
workflowState.state.currentPhase = "release";
|
|
9
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
10
|
+
workflowState.state.readyCheckCompleted = true;
|
|
11
|
+
workflowState.state.lastPushBranch = "feature/split";
|
|
12
|
+
workflowState.state.lastCommitMessage = "fix: address issue";
|
|
13
|
+
await workflowState.save();
|
|
14
|
+
|
|
15
|
+
const commands = [];
|
|
16
|
+
const execStub = async (command) => {
|
|
17
|
+
commands.push(command);
|
|
18
|
+
if (command === "git status --porcelain") {
|
|
19
|
+
return { stdout: "" };
|
|
20
|
+
}
|
|
21
|
+
return { stdout: "" };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const response = await handleToolCall({
|
|
25
|
+
request: createRequest("perform_release", { command: "npm run release" }),
|
|
26
|
+
normalizeRequestArgs: (args) => ({ args, error: null }),
|
|
27
|
+
workflowState,
|
|
28
|
+
exec: execStub,
|
|
29
|
+
git: createGitMock(),
|
|
30
|
+
utils: testUtils,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const releaseCommand = commands.find((command) => command.startsWith("npm run release"));
|
|
34
|
+
assert.ok(releaseCommand, "release command should run");
|
|
35
|
+
assert.ok(
|
|
36
|
+
commands.some((command) => command.startsWith("git push --follow-tags")),
|
|
37
|
+
"release should push tags"
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("perform_release accepts shorthand patch command", async () => {
|
|
43
|
+
await withWorkflowState(async (workflowState) => {
|
|
44
|
+
workflowState.state.currentPhase = "release";
|
|
45
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
46
|
+
workflowState.state.readyCheckCompleted = true;
|
|
47
|
+
workflowState.state.lastCommitMessage = "fix: adjust labels";
|
|
48
|
+
await workflowState.save();
|
|
49
|
+
|
|
50
|
+
const commands = [];
|
|
51
|
+
const execStub = async (command) => {
|
|
52
|
+
commands.push(command);
|
|
53
|
+
if (command === "git status --porcelain") {
|
|
54
|
+
return { stdout: "" };
|
|
55
|
+
}
|
|
56
|
+
return { stdout: "" };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
await handleToolCall({
|
|
60
|
+
request: createRequest("perform_release", { command: "patch" }),
|
|
61
|
+
normalizeRequestArgs: (args) => ({ args, error: null }),
|
|
62
|
+
workflowState,
|
|
63
|
+
exec: execStub,
|
|
64
|
+
git: createGitMock(),
|
|
65
|
+
utils: testUtils,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const releaseCommand = commands.find((command) => command.startsWith("npm run release"));
|
|
69
|
+
assert.ok(releaseCommand, "release command should run with patch");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("perform_release respects explicit releaseType option", async () => {
|
|
74
|
+
await withWorkflowState(async (workflowState) => {
|
|
75
|
+
workflowState.state.currentPhase = "release";
|
|
76
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
77
|
+
workflowState.state.readyCheckCompleted = true;
|
|
78
|
+
await workflowState.save();
|
|
79
|
+
|
|
80
|
+
const commands = [];
|
|
81
|
+
const execStub = async (command) => {
|
|
82
|
+
commands.push(command);
|
|
83
|
+
if (command === "git status --porcelain") {
|
|
84
|
+
return { stdout: "" };
|
|
85
|
+
}
|
|
86
|
+
return { stdout: "" };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await handleToolCall({
|
|
90
|
+
request: createRequest("perform_release", {
|
|
91
|
+
command: "npm run release",
|
|
92
|
+
releaseType: "minor",
|
|
93
|
+
}),
|
|
94
|
+
normalizeRequestArgs: (args) => ({ args, error: null }),
|
|
95
|
+
workflowState,
|
|
96
|
+
exec: execStub,
|
|
97
|
+
git: createGitMock(),
|
|
98
|
+
utils: testUtils,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const releaseCommand = commands.find((command) => command.startsWith("npm run release"));
|
|
102
|
+
assert.ok(releaseCommand, "release command should run");
|
|
103
|
+
assert.ok(
|
|
104
|
+
releaseCommand.includes("-- --release-as minor"),
|
|
105
|
+
"release command should append provided releaseType"
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("perform_release blocks when new changes detected", async () => {
|
|
111
|
+
await withWorkflowState(async (workflowState) => {
|
|
112
|
+
workflowState.state.currentPhase = "release";
|
|
113
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
114
|
+
workflowState.state.readyCheckCompleted = true;
|
|
115
|
+
await workflowState.save();
|
|
116
|
+
|
|
117
|
+
const commands = [];
|
|
118
|
+
const execStub = async (command) => {
|
|
119
|
+
commands.push(command);
|
|
120
|
+
if (command === "git status --porcelain") {
|
|
121
|
+
return { stdout: "?? new-file.js" };
|
|
122
|
+
}
|
|
123
|
+
return { stdout: "" };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const response = await handleToolCall({
|
|
127
|
+
request: createRequest("perform_release", { command: "patch" }),
|
|
128
|
+
normalizeRequestArgs: (args) => ({ args, error: null }),
|
|
129
|
+
workflowState,
|
|
130
|
+
exec: execStub,
|
|
131
|
+
git: createGitMock({
|
|
132
|
+
hasWorkingChanges: async () => true,
|
|
133
|
+
getStatusOutput: async () => "?? new-file.js",
|
|
134
|
+
containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
|
|
135
|
+
workingTreeSummary: () => ({ hasChanges: true, lines: ["?? new-file.js"] }),
|
|
136
|
+
}),
|
|
137
|
+
utils: testUtils,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
assert.ok(
|
|
141
|
+
response.content[0].text.includes("run 'commit_and_push' again"),
|
|
142
|
+
"response should instruct to recommit"
|
|
143
|
+
);
|
|
144
|
+
assert.equal(workflowState.state.currentPhase, "commit");
|
|
145
|
+
assert.equal(workflowState.state.commitAndPushCompleted, false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("perform_release requires commit_and_push completion when tree is clean", async () => {
|
|
150
|
+
await withWorkflowState(async (workflowState) => {
|
|
151
|
+
workflowState.state.currentPhase = "release";
|
|
152
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
153
|
+
workflowState.state.readyCheckCompleted = true;
|
|
154
|
+
workflowState.state.lastPushBranch = "";
|
|
155
|
+
workflowState.state.lastCommitMessage = "";
|
|
156
|
+
await workflowState.save();
|
|
157
|
+
|
|
158
|
+
const commands = [];
|
|
159
|
+
const execStub = async (command) => {
|
|
160
|
+
commands.push(command);
|
|
161
|
+
return { stdout: "" };
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const response = await handleToolCall({
|
|
165
|
+
request: createRequest("perform_release", { command: "npm run release" }),
|
|
166
|
+
normalizeRequestArgs: (args) => ({ args, error: null }),
|
|
167
|
+
workflowState,
|
|
168
|
+
exec: execStub,
|
|
169
|
+
git: createGitMock({
|
|
170
|
+
getLastCommitMessage: async () => "chore: already pushed",
|
|
171
|
+
getStatusOutput: async () => "",
|
|
172
|
+
containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
|
|
173
|
+
}),
|
|
174
|
+
utils: testUtils,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
assert.ok(
|
|
178
|
+
response.content[0].text.includes("commit_and_push"),
|
|
179
|
+
"response should mention commit_and_push requirement"
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("skip_release records justification and advances to completion phase", async () => {
|
|
185
|
+
await withWorkflowState(async (workflowState) => {
|
|
186
|
+
workflowState.state.currentPhase = "release";
|
|
187
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
188
|
+
workflowState.state.readyCheckCompleted = true;
|
|
189
|
+
workflowState.state.lastCommitMessage = "chore: docs";
|
|
190
|
+
workflowState.state.releaseSkipped = false;
|
|
191
|
+
workflowState.state.released = false;
|
|
192
|
+
workflowState.state.releaseCommand = "";
|
|
193
|
+
await workflowState.save();
|
|
194
|
+
|
|
195
|
+
const response = await handleToolCall({
|
|
196
|
+
request: createRequest("skip_release", { reason: "Python project – no npm release" }),
|
|
197
|
+
normalizeRequestArgs: (args) => ({ args, error: null }),
|
|
198
|
+
workflowState,
|
|
199
|
+
exec: async () => ({ stdout: "" }),
|
|
200
|
+
git: createGitMock(),
|
|
201
|
+
utils: testUtils,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const text = response.content[0].text;
|
|
205
|
+
assert.ok(text.includes("Release step skipped"), "response should mention release was skipped");
|
|
206
|
+
assert.equal(workflowState.state.releaseSkipped, true);
|
|
207
|
+
assert.equal(workflowState.state.releaseSkippedReason, "Python project – no npm release");
|
|
208
|
+
assert.equal(workflowState.state.releaseCommand, "(skipped)");
|
|
209
|
+
assert.equal(workflowState.state.currentPhase, "ready_to_complete");
|
|
210
|
+
assert.equal(workflowState.state.released, false);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { handleToolCall } from "../tools/handlers.js";
|
|
4
|
+
import { withWorkflowState, testUtils, createRequest, createGitMock } from "./test-helpers.js";
|
|
5
|
+
|
|
6
|
+
test("run_tests guidance mentions core documentation types", async () => {
|
|
7
|
+
await withWorkflowState(async (workflowState) => {
|
|
8
|
+
workflowState.state.bugFixed = true;
|
|
9
|
+
workflowState.state.testsCreated = true;
|
|
10
|
+
await workflowState.save();
|
|
11
|
+
|
|
12
|
+
const response = await handleToolCall({
|
|
13
|
+
request: createRequest("run_tests", {
|
|
14
|
+
passed: true,
|
|
15
|
+
testCommand: "npm test",
|
|
16
|
+
}),
|
|
17
|
+
normalizeRequestArgs: (args) => ({ args, error: null }),
|
|
18
|
+
workflowState,
|
|
19
|
+
exec: async () => ({ stdout: "" }),
|
|
20
|
+
git: createGitMock(),
|
|
21
|
+
utils: testUtils,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const text = response.content[0].text;
|
|
25
|
+
assert.ok(
|
|
26
|
+
text.includes("create or update documentation using 'create_documentation'"),
|
|
27
|
+
"run_tests guidance should mention create or update documentation",
|
|
28
|
+
);
|
|
29
|
+
assert.ok(
|
|
30
|
+
text.includes('"PRD", "README", "RELEASE_NOTES"'),
|
|
31
|
+
"run_tests guidance should list core documentation types",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("create_documentation verifies PRD file exists", async () => {
|
|
37
|
+
await withWorkflowState(async (workflowState) => {
|
|
38
|
+
workflowState.state.testsPassed = true;
|
|
39
|
+
await workflowState.save();
|
|
40
|
+
|
|
41
|
+
const response = await handleToolCall({
|
|
42
|
+
request: createRequest("create_documentation", {
|
|
43
|
+
documentationType: "PRD",
|
|
44
|
+
summary: "Updated PRD for documentation workflow expectations",
|
|
45
|
+
}),
|
|
46
|
+
normalizeRequestArgs: (args) => ({ args, error: null }),
|
|
47
|
+
workflowState,
|
|
48
|
+
exec: async () => ({ stdout: "" }),
|
|
49
|
+
git: createGitMock(),
|
|
50
|
+
utils: testUtils,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
assert.equal(workflowState.state.documentationCreated, true);
|
|
54
|
+
assert.equal(workflowState.state.documentationType, "PRD");
|
|
55
|
+
assert.equal(
|
|
56
|
+
workflowState.state.documentationSummary,
|
|
57
|
+
"Updated PRD for documentation workflow expectations",
|
|
58
|
+
);
|
|
59
|
+
assert.ok(
|
|
60
|
+
response.content[0].text.includes("Documentation created/updated!"),
|
|
61
|
+
"create_documentation response should mention created/updated",
|
|
62
|
+
);
|
|
63
|
+
assert.ok(
|
|
64
|
+
response.content[0].text.includes("PRD verified"),
|
|
65
|
+
"create_documentation response should verify PRD file",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|