@programinglive/dev-workflow-mcp-server 1.5.2 → 1.5.3
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/.agent/workflows/dev-workflow-bugfix.md +250 -250
- package/.agent/workflows/dev-workflow-feature.md +201 -201
- package/.agent/workflows/dev-workflow-full-cycle.md +400 -400
- package/.agent/workflows/dev-workflow-refactor.md +238 -238
- package/.github/ISSUE_TEMPLATE/bug_report.md +34 -34
- package/.github/ISSUE_TEMPLATE/feature_request.md +25 -25
- package/.github/pull_request_template.md +22 -22
- package/.github/workflows/npm-publish.yml +50 -50
- package/.husky/commit-msg +0 -0
- package/CHANGELOG.md +748 -741
- package/CODE_OF_CONDUCT.md +79 -79
- package/CONTRIBUTING.md +60 -60
- package/LICENSE +21 -21
- package/README.md +632 -632
- package/SECURITY.md +20 -20
- package/db/index.js +204 -204
- package/docs/NETLIFY_DEPLOYMENT.md +125 -125
- package/docs/PLESK_DEPLOYMENT.md +170 -170
- package/docs/WORKFLOW-BEST-PRACTICES.md +58 -58
- package/docs/antigravity/EXAMPLES.md +535 -535
- package/docs/antigravity/GETTING-STARTED.md +292 -292
- package/docs/antigravity/TROUBLESHOOTING.md +742 -742
- package/docs/antigravity/WALKTHROUGH.md +25 -25
- package/docs/antigravity/WORKFLOW-TEMPLATES.md +670 -670
- package/docs/product/PRD.md +67 -67
- package/docs/release-notes/RELEASE_NOTES.md +226 -226
- package/docs/web-dashboard.md +175 -175
- package/exec.js +49 -49
- package/index.js +279 -279
- package/install.js +73 -73
- package/package.json +8 -8
- package/release-wrapper.js +192 -192
- package/scripts/build-docs.js +63 -63
- package/scripts/hello-world.js +1 -1
- package/scripts/release.cjs +252 -252
- package/scripts/release.js +252 -0
- package/scripts/update-release-notes.cjs +178 -178
- package/scripts/update-release-notes.js +182 -0
- package/server.json +23 -23
- package/tailwind.config.js +8 -8
- package/tests/build.test.js +83 -83
- package/tests/exec.test.js +35 -35
- package/tests/handlers.test.js +1058 -1058
- package/tests/hello-world.test.js +13 -13
- package/tests/mcpRegistry.test.js +65 -65
- package/tests/patch-release.test.js +7 -0
- package/tests/release-wrapper.test.js +107 -107
- package/tests/workflowState.test.js +505 -505
- package/tools/definitions.js +332 -332
- package/tools/handlers.js +1019 -1019
- package/vite.config.js +43 -43
- package/vite.web.config.js +23 -23
- package/web/README.md +36 -36
- package/web/app/docs/page.tsx +202 -202
- package/web/app/globals.css +90 -90
- package/web/app/layout.tsx +36 -36
- package/web/app/page.tsx +15 -15
- package/web/components/ApiReference.tsx +20 -20
- package/web/components/Features.tsx +61 -61
- package/web/components/Footer.tsx +9 -9
- package/web/components/Hero.tsx +96 -96
- package/web/components/History.tsx +143 -143
- package/web/components/Navbar.tsx +64 -64
- package/web/components/Terminal.tsx +114 -114
- package/web/eslint.config.mjs +18 -18
- package/web/lib/db.js +131 -131
- package/web/lib/docs.js +73 -73
- package/web/netlify.toml +3 -3
- package/web/next.config.ts +12 -12
- package/web/package-lock.json +6560 -6560
- package/web/package.json +28 -28
- package/web/postcss.config.mjs +9 -9
- package/web/server.js +31 -31
- package/web/tailwind.config.ts +19 -19
- package/web/tsconfig.json +42 -42
- package/workflow-state.js +612 -612
package/tests/handlers.test.js
CHANGED
|
@@ -1,1058 +1,1058 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
|
|
7
|
-
import { WorkflowState } from "../workflow-state.js";
|
|
8
|
-
import { handleToolCall } from "../tools/handlers.js";
|
|
9
|
-
import { normalizeRequestArgs, shellEscape } from "../utils.js";
|
|
10
|
-
import {
|
|
11
|
-
createCommitMessageParts,
|
|
12
|
-
determineReleaseTypeFromCommit,
|
|
13
|
-
} from "../commit-helpers.js";
|
|
14
|
-
|
|
15
|
-
async function withWorkflowState(callback) {
|
|
16
|
-
const tempDir = await mkdtemp(path.join(tmpdir(), "workflow-handlers-"));
|
|
17
|
-
const stateFile = path.join(tempDir, "state.json");
|
|
18
|
-
const state = new WorkflowState(stateFile);
|
|
19
|
-
await state.load();
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
await callback(state);
|
|
23
|
-
} finally {
|
|
24
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const utils = {
|
|
29
|
-
shellEscape,
|
|
30
|
-
determineReleaseTypeFromCommit,
|
|
31
|
-
createCommitMessageParts,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function createRequest(name, args) {
|
|
35
|
-
return {
|
|
36
|
-
params: {
|
|
37
|
-
name,
|
|
38
|
-
arguments: args,
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
test("run_tests guidance mentions core documentation types", async () => {
|
|
44
|
-
await withWorkflowState(async (workflowState) => {
|
|
45
|
-
workflowState.state.bugFixed = true;
|
|
46
|
-
workflowState.state.testsCreated = true;
|
|
47
|
-
await workflowState.save();
|
|
48
|
-
|
|
49
|
-
const response = await handleToolCall({
|
|
50
|
-
request: createRequest("run_tests", {
|
|
51
|
-
passed: true,
|
|
52
|
-
testCommand: "npm test",
|
|
53
|
-
}),
|
|
54
|
-
normalizeRequestArgs,
|
|
55
|
-
workflowState,
|
|
56
|
-
exec: async () => ({ stdout: "" }),
|
|
57
|
-
git: {
|
|
58
|
-
hasWorkingChanges: async () => false,
|
|
59
|
-
hasStagedChanges: async () => false,
|
|
60
|
-
hasTestChanges: async () => true,
|
|
61
|
-
getStagedChanges: async () => [],
|
|
62
|
-
getCurrentBranch: async () => "main",
|
|
63
|
-
getPrimaryBranch: async () => "main",
|
|
64
|
-
getLastCommitMessage: async () => "",
|
|
65
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
66
|
-
},
|
|
67
|
-
utils,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const text = response.content[0].text;
|
|
71
|
-
assert.ok(
|
|
72
|
-
text.includes("create or update documentation using 'create_documentation'"),
|
|
73
|
-
"run_tests guidance should mention create or update documentation",
|
|
74
|
-
);
|
|
75
|
-
assert.ok(
|
|
76
|
-
text.includes('"PRD", "README", "RELEASE_NOTES"'),
|
|
77
|
-
"run_tests guidance should list core documentation types",
|
|
78
|
-
);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("create_documentation accepts PRD and records created/updated message", async () => {
|
|
83
|
-
await withWorkflowState(async (workflowState) => {
|
|
84
|
-
workflowState.state.testsPassed = true;
|
|
85
|
-
await workflowState.save();
|
|
86
|
-
|
|
87
|
-
const response = await handleToolCall({
|
|
88
|
-
request: createRequest("create_documentation", {
|
|
89
|
-
documentationType: "PRD",
|
|
90
|
-
summary: "Updated PRD for documentation workflow expectations",
|
|
91
|
-
}),
|
|
92
|
-
normalizeRequestArgs,
|
|
93
|
-
workflowState,
|
|
94
|
-
exec: async () => ({ stdout: "" }),
|
|
95
|
-
git: {
|
|
96
|
-
hasWorkingChanges: async () => false,
|
|
97
|
-
hasStagedChanges: async () => false,
|
|
98
|
-
hasTestChanges: async () => true,
|
|
99
|
-
getStagedChanges: async () => [],
|
|
100
|
-
getCurrentBranch: async () => "main",
|
|
101
|
-
getPrimaryBranch: async () => "main",
|
|
102
|
-
getLastCommitMessage: async () => "",
|
|
103
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
104
|
-
},
|
|
105
|
-
utils,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
assert.equal(workflowState.state.documentationCreated, true);
|
|
109
|
-
assert.equal(workflowState.state.documentationType, "PRD");
|
|
110
|
-
assert.equal(
|
|
111
|
-
workflowState.state.documentationSummary,
|
|
112
|
-
"Updated PRD for documentation workflow expectations",
|
|
113
|
-
);
|
|
114
|
-
assert.ok(
|
|
115
|
-
response.content[0].text.includes("Documentation created/updated!"),
|
|
116
|
-
"create_documentation response should mention created/updated",
|
|
117
|
-
);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("commit_and_push commits changes and pushes", async () => {
|
|
122
|
-
await withWorkflowState(async (workflowState) => {
|
|
123
|
-
workflowState.state.readyToCommit = true;
|
|
124
|
-
workflowState.state.readyCheckCompleted = true;
|
|
125
|
-
workflowState.state.testsSkipped = false;
|
|
126
|
-
workflowState.state.testsCreated = true;
|
|
127
|
-
workflowState.state.testsPassed = true;
|
|
128
|
-
workflowState.state.currentPhase = "commit";
|
|
129
|
-
await workflowState.save();
|
|
130
|
-
|
|
131
|
-
const commands = [];
|
|
132
|
-
const execStub = async (command) => {
|
|
133
|
-
commands.push(command);
|
|
134
|
-
return { stdout: "" };
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const git = {
|
|
138
|
-
hasWorkingChanges: async () => true,
|
|
139
|
-
hasStagedChanges: async () => false,
|
|
140
|
-
hasTestChanges: async () => true,
|
|
141
|
-
getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
|
|
142
|
-
getCurrentBranch: async () => "main",
|
|
143
|
-
getPrimaryBranch: async () => "main",
|
|
144
|
-
getLastCommitMessage: async () => "",
|
|
145
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const response = await handleToolCall({
|
|
149
|
-
request: createRequest("commit_and_push", { commitMessage: "" }),
|
|
150
|
-
normalizeRequestArgs,
|
|
151
|
-
workflowState,
|
|
152
|
-
exec: execStub,
|
|
153
|
-
git,
|
|
154
|
-
utils,
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const pushCommand = commands.find((command) => command.startsWith("git push"));
|
|
158
|
-
assert.ok(pushCommand, "git push should be executed during commit");
|
|
159
|
-
assert.ok(
|
|
160
|
-
pushCommand.startsWith("git push origin"),
|
|
161
|
-
"push command should target origin"
|
|
162
|
-
);
|
|
163
|
-
assert.equal(
|
|
164
|
-
commands.some((command) => command.startsWith("git commit")),
|
|
165
|
-
true,
|
|
166
|
-
"git commit should be executed"
|
|
167
|
-
);
|
|
168
|
-
assert.ok(
|
|
169
|
-
response.content[0].text.includes("Commit and push completed!"),
|
|
170
|
-
"response should confirm commit and push"
|
|
171
|
-
);
|
|
172
|
-
assert.equal(workflowState.state.commitAndPushCompleted, true);
|
|
173
|
-
assert.equal(workflowState.state.lastCommitMessage.length > 0, true);
|
|
174
|
-
assert.equal(workflowState.state.currentPhase, "release");
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("run_full_workflow iterates until commit/release complete with clean tree", async () => {
|
|
179
|
-
await withWorkflowState(async (workflowState) => {
|
|
180
|
-
workflowState.state.taskDescription = "Ship feature";
|
|
181
|
-
workflowState.state.taskType = "feature";
|
|
182
|
-
workflowState.state.currentPhase = "commit";
|
|
183
|
-
workflowState.state.bugFixed = true;
|
|
184
|
-
workflowState.state.testsCreated = true;
|
|
185
|
-
workflowState.state.testsPassed = true;
|
|
186
|
-
workflowState.state.documentationCreated = true;
|
|
187
|
-
workflowState.state.readyToCommit = true;
|
|
188
|
-
workflowState.state.readyCheckCompleted = true;
|
|
189
|
-
workflowState.state.commitAndPushCompleted = false;
|
|
190
|
-
workflowState.state.released = false;
|
|
191
|
-
workflowState.state.documentationType = "README";
|
|
192
|
-
workflowState.state.documentationSummary = "Docs";
|
|
193
|
-
workflowState.state.fixSummary = "Feature done";
|
|
194
|
-
await workflowState.save();
|
|
195
|
-
|
|
196
|
-
const commands = [];
|
|
197
|
-
const execStub = async (command) => {
|
|
198
|
-
commands.push(command);
|
|
199
|
-
if (command === "git status --porcelain") {
|
|
200
|
-
return { stdout: "" };
|
|
201
|
-
}
|
|
202
|
-
return { stdout: "" };
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
const git = {
|
|
206
|
-
hasWorkingChanges: async () => false,
|
|
207
|
-
hasStagedChanges: async () => false,
|
|
208
|
-
hasTestChanges: async () => true,
|
|
209
|
-
getStagedChanges: async () => [],
|
|
210
|
-
getCurrentBranch: async () => "main",
|
|
211
|
-
getPrimaryBranch: async () => "main",
|
|
212
|
-
getLastCommitMessage: async () => "chore: existing",
|
|
213
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const response = await handleToolCall({
|
|
217
|
-
request: createRequest("run_full_workflow", {
|
|
218
|
-
summary: "Feature done",
|
|
219
|
-
testCommand: "npm test",
|
|
220
|
-
documentationType: "README",
|
|
221
|
-
documentationSummary: "Docs",
|
|
222
|
-
commitMessage: "feat: ship feature",
|
|
223
|
-
releaseCommand: "npm run release:patch",
|
|
224
|
-
}),
|
|
225
|
-
normalizeRequestArgs,
|
|
226
|
-
workflowState,
|
|
227
|
-
exec: execStub,
|
|
228
|
-
git,
|
|
229
|
-
utils,
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
assert.ok(
|
|
233
|
-
response.content[0].text.includes("✅ Full workflow completed successfully"),
|
|
234
|
-
"run_full_workflow should finish even when commit is already clean"
|
|
235
|
-
);
|
|
236
|
-
assert.ok(
|
|
237
|
-
response.content[0].text.includes("🎉 Cheers!"),
|
|
238
|
-
"run_full_workflow should include celebration message"
|
|
239
|
-
);
|
|
240
|
-
assert.equal(commands.includes("npm run release:patch"), true, "release command should run");
|
|
241
|
-
assert.equal(workflowState.state.currentPhase, "idle");
|
|
242
|
-
assert.equal(workflowState.state.history.length, 1);
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test("commit_and_push uses primary branch when no branch specified", async () => {
|
|
247
|
-
await withWorkflowState(async (workflowState) => {
|
|
248
|
-
workflowState.state.readyToCommit = true;
|
|
249
|
-
workflowState.state.readyCheckCompleted = true;
|
|
250
|
-
workflowState.state.testsSkipped = false;
|
|
251
|
-
workflowState.state.testsCreated = true;
|
|
252
|
-
workflowState.state.testsPassed = true;
|
|
253
|
-
workflowState.state.currentPhase = "commit";
|
|
254
|
-
await workflowState.save();
|
|
255
|
-
|
|
256
|
-
const commands = [];
|
|
257
|
-
const execStub = async (command) => {
|
|
258
|
-
commands.push(command);
|
|
259
|
-
return { stdout: "" };
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const git = {
|
|
263
|
-
hasWorkingChanges: async () => true,
|
|
264
|
-
hasStagedChanges: async () => false,
|
|
265
|
-
hasTestChanges: async () => true,
|
|
266
|
-
getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
|
|
267
|
-
getCurrentBranch: async () => "feature/test",
|
|
268
|
-
getPrimaryBranch: async () => "main",
|
|
269
|
-
getLastCommitMessage: async () => "",
|
|
270
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const response = await handleToolCall({
|
|
274
|
-
request: createRequest("commit_and_push", { commitMessage: "feat: test" }),
|
|
275
|
-
normalizeRequestArgs,
|
|
276
|
-
workflowState,
|
|
277
|
-
exec: execStub,
|
|
278
|
-
git,
|
|
279
|
-
utils,
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
const pushCommand = commands.find((command) => command.startsWith("git push"));
|
|
283
|
-
assert.ok(pushCommand, "git push should be executed");
|
|
284
|
-
assert.ok(
|
|
285
|
-
pushCommand.includes("main"),
|
|
286
|
-
"push command should use detected primary branch (main)"
|
|
287
|
-
);
|
|
288
|
-
assert.ok(
|
|
289
|
-
response.content[0].text.includes("Commit and push completed!"),
|
|
290
|
-
"response should confirm commit and push"
|
|
291
|
-
);
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test("commit_and_push falls back to master when main not found", async () => {
|
|
296
|
-
await withWorkflowState(async (workflowState) => {
|
|
297
|
-
workflowState.state.readyToCommit = true;
|
|
298
|
-
workflowState.state.readyCheckCompleted = true;
|
|
299
|
-
workflowState.state.testsSkipped = false;
|
|
300
|
-
workflowState.state.testsCreated = true;
|
|
301
|
-
workflowState.state.testsPassed = true;
|
|
302
|
-
workflowState.state.currentPhase = "commit";
|
|
303
|
-
await workflowState.save();
|
|
304
|
-
|
|
305
|
-
const commands = [];
|
|
306
|
-
const execStub = async (command) => {
|
|
307
|
-
commands.push(command);
|
|
308
|
-
return { stdout: "" };
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
const git = {
|
|
312
|
-
hasWorkingChanges: async () => true,
|
|
313
|
-
hasStagedChanges: async () => false,
|
|
314
|
-
hasTestChanges: async () => true,
|
|
315
|
-
getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
|
|
316
|
-
getCurrentBranch: async () => "feature/test",
|
|
317
|
-
getPrimaryBranch: async () => "master",
|
|
318
|
-
getLastCommitMessage: async () => "",
|
|
319
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
const response = await handleToolCall({
|
|
323
|
-
request: createRequest("commit_and_push", { commitMessage: "feat: test" }),
|
|
324
|
-
normalizeRequestArgs,
|
|
325
|
-
workflowState,
|
|
326
|
-
exec: execStub,
|
|
327
|
-
git,
|
|
328
|
-
utils,
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
const pushCommand = commands.find((command) => command.startsWith("git push"));
|
|
332
|
-
assert.ok(pushCommand, "git push should be executed");
|
|
333
|
-
assert.ok(
|
|
334
|
-
pushCommand.includes("master"),
|
|
335
|
-
"push command should use detected primary branch (master)"
|
|
336
|
-
);
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test("run_full_workflow uses provided branch for release push when commit is clean", async () => {
|
|
341
|
-
await withWorkflowState(async (workflowState) => {
|
|
342
|
-
workflowState.state.taskDescription = "Ship feature";
|
|
343
|
-
workflowState.state.taskType = "feature";
|
|
344
|
-
workflowState.state.currentPhase = "commit";
|
|
345
|
-
workflowState.state.bugFixed = true;
|
|
346
|
-
workflowState.state.testsCreated = true;
|
|
347
|
-
workflowState.state.testsPassed = true;
|
|
348
|
-
workflowState.state.documentationCreated = true;
|
|
349
|
-
workflowState.state.readyToCommit = true;
|
|
350
|
-
workflowState.state.readyCheckCompleted = true;
|
|
351
|
-
workflowState.state.commitAndPushCompleted = false;
|
|
352
|
-
workflowState.state.released = false;
|
|
353
|
-
workflowState.state.documentationType = "README";
|
|
354
|
-
workflowState.state.documentationSummary = "Docs";
|
|
355
|
-
workflowState.state.fixSummary = "Feature done";
|
|
356
|
-
await workflowState.save();
|
|
357
|
-
|
|
358
|
-
const commands = [];
|
|
359
|
-
const execStub = async (command) => {
|
|
360
|
-
commands.push(command);
|
|
361
|
-
if (command === "git status --porcelain") {
|
|
362
|
-
return { stdout: "" };
|
|
363
|
-
}
|
|
364
|
-
return { stdout: "" };
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
const git = {
|
|
368
|
-
hasWorkingChanges: async () => false,
|
|
369
|
-
hasStagedChanges: async () => false,
|
|
370
|
-
hasTestChanges: async () => true,
|
|
371
|
-
getStagedChanges: async () => [],
|
|
372
|
-
getCurrentBranch: async () => "feature/local",
|
|
373
|
-
getPrimaryBranch: async () => "main",
|
|
374
|
-
getLastCommitMessage: async () => "chore: existing",
|
|
375
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
const branchName = "master";
|
|
379
|
-
const response = await handleToolCall({
|
|
380
|
-
request: createRequest("run_full_workflow", {
|
|
381
|
-
summary: "Feature done",
|
|
382
|
-
testCommand: "npm test",
|
|
383
|
-
documentationType: "README",
|
|
384
|
-
documentationSummary: "Docs",
|
|
385
|
-
commitMessage: "feat: ship feature",
|
|
386
|
-
branch: branchName,
|
|
387
|
-
releaseCommand: "npm run release:patch",
|
|
388
|
-
}),
|
|
389
|
-
normalizeRequestArgs,
|
|
390
|
-
workflowState,
|
|
391
|
-
exec: execStub,
|
|
392
|
-
git,
|
|
393
|
-
utils,
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
assert.ok(
|
|
397
|
-
response.content[0].text.includes("✅ Full workflow completed successfully"),
|
|
398
|
-
"run_full_workflow should finish successfully"
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
const tagPushCommand = commands.find((command) => command.startsWith("git push --follow-tags"));
|
|
402
|
-
assert.ok(tagPushCommand, "release should push tags");
|
|
403
|
-
assert.ok(
|
|
404
|
-
/origin\s+['"]?master['"]?/.test(tagPushCommand),
|
|
405
|
-
`release push should target the provided branch (${branchName})`
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
assert.equal(workflowState.state.currentPhase, "idle");
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
test("project_summary_data reads persisted file and falls back", async () => {
|
|
413
|
-
await withWorkflowState(async (workflowState) => {
|
|
414
|
-
// Seed some history
|
|
415
|
-
workflowState.addToHistory({ taskDescription: "Add login feature", taskType: "feature", commitMessage: "feat: login" });
|
|
416
|
-
await workflowState.save(); // This should create project-summary.json
|
|
417
|
-
|
|
418
|
-
const response = await handleToolCall({
|
|
419
|
-
request: createRequest("project_summary_data", {}),
|
|
420
|
-
normalizeRequestArgs,
|
|
421
|
-
workflowState,
|
|
422
|
-
exec: async () => ({ stdout: "" }),
|
|
423
|
-
git: {
|
|
424
|
-
hasWorkingChanges: async () => false,
|
|
425
|
-
hasStagedChanges: async () => false,
|
|
426
|
-
hasTestChanges: async () => true,
|
|
427
|
-
getStagedChanges: async () => [],
|
|
428
|
-
getCurrentBranch: async () => "main",
|
|
429
|
-
getPrimaryBranch: async () => "main",
|
|
430
|
-
getLastCommitMessage: async () => "",
|
|
431
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
432
|
-
},
|
|
433
|
-
utils,
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
const text = response.content[0].text;
|
|
437
|
-
assert.ok(text.includes("from persisted data"), "should indicate persisted source");
|
|
438
|
-
assert.ok(text.includes("Total tasks completed: 1"), "should count tasks");
|
|
439
|
-
assert.ok(text.includes("feature: 1"), "should show feature count");
|
|
440
|
-
assert.ok(text.includes("Updated:"), "should show updated timestamp");
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
test("project_summary aggregates task types and recent activity", async () => {
|
|
445
|
-
await withWorkflowState(async (workflowState) => {
|
|
446
|
-
// Seed some history
|
|
447
|
-
workflowState.addToHistory({ taskDescription: "Add login feature", taskType: "feature", commitMessage: "feat: login" });
|
|
448
|
-
workflowState.addToHistory({ taskDescription: "Fix auth bug", taskType: "bugfix", commitMessage: "fix: auth" });
|
|
449
|
-
workflowState.addToHistory({ taskDescription: "Refactor utils", taskType: "refactor", commitMessage: "refactor: utils" });
|
|
450
|
-
await workflowState.save();
|
|
451
|
-
|
|
452
|
-
const response = await handleToolCall({
|
|
453
|
-
request: createRequest("project_summary", {}),
|
|
454
|
-
normalizeRequestArgs,
|
|
455
|
-
workflowState,
|
|
456
|
-
exec: async () => ({ stdout: "" }),
|
|
457
|
-
git: {
|
|
458
|
-
hasWorkingChanges: async () => false,
|
|
459
|
-
hasStagedChanges: async () => false,
|
|
460
|
-
hasTestChanges: async () => true,
|
|
461
|
-
getStagedChanges: async () => [],
|
|
462
|
-
getCurrentBranch: async () => "main",
|
|
463
|
-
getPrimaryBranch: async () => "main",
|
|
464
|
-
getLastCommitMessage: async () => "",
|
|
465
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
466
|
-
},
|
|
467
|
-
utils,
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
const text = response.content[0].text;
|
|
471
|
-
assert.ok(text.includes("Project Knowledge Summary"), "should include summary title");
|
|
472
|
-
assert.ok(text.includes("Total tasks completed: 3"), "should count tasks");
|
|
473
|
-
assert.ok(text.includes("feature: 1"), "should show feature count");
|
|
474
|
-
assert.ok(text.includes("bugfix: 1"), "should show bugfix count");
|
|
475
|
-
assert.ok(text.includes("refactor: 1"), "should show refactor count");
|
|
476
|
-
assert.ok(text.includes("Add login feature"), "should list recent tasks");
|
|
477
|
-
});
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
test("run_full_workflow executes all steps successfully", async () => {
|
|
481
|
-
await withWorkflowState(async (workflowState) => {
|
|
482
|
-
workflowState.state.taskDescription = "Ship feature";
|
|
483
|
-
workflowState.state.taskType = "feature";
|
|
484
|
-
workflowState.state.currentPhase = "coding";
|
|
485
|
-
await workflowState.save();
|
|
486
|
-
|
|
487
|
-
const commands = [];
|
|
488
|
-
let workingChanges = true;
|
|
489
|
-
const execStub = async (command) => {
|
|
490
|
-
commands.push(command);
|
|
491
|
-
if (command.startsWith("git commit") || command.startsWith("git push")) {
|
|
492
|
-
workingChanges = false;
|
|
493
|
-
}
|
|
494
|
-
if (command === "git status --porcelain") {
|
|
495
|
-
return { stdout: workingChanges ? " M src/app.js" : "" };
|
|
496
|
-
}
|
|
497
|
-
return { stdout: "" };
|
|
498
|
-
};
|
|
499
|
-
|
|
500
|
-
const git = {
|
|
501
|
-
hasWorkingChanges: async () => workingChanges,
|
|
502
|
-
hasStagedChanges: async () => false,
|
|
503
|
-
hasTestChanges: async () => true,
|
|
504
|
-
getStagedChanges: async () => [{ status: "M", path: "src/app.js" }],
|
|
505
|
-
getCurrentBranch: async () => "main",
|
|
506
|
-
getPrimaryBranch: async () => "main",
|
|
507
|
-
getLastCommitMessage: async () => "",
|
|
508
|
-
workingTreeSummary: () =>
|
|
509
|
-
workingChanges
|
|
510
|
-
? { hasChanges: true, lines: ["M src/app.js"] }
|
|
511
|
-
: { hasChanges: false, lines: [] },
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
const response = await handleToolCall({
|
|
515
|
-
request: createRequest("run_full_workflow", {
|
|
516
|
-
summary: "Implement feature",
|
|
517
|
-
testCommand: "npm test",
|
|
518
|
-
documentationType: "README",
|
|
519
|
-
documentationSummary: "Document feature",
|
|
520
|
-
commitMessage: "feat: add feature",
|
|
521
|
-
releaseCommand: "npm run release:patch",
|
|
522
|
-
releaseNotes: "Automated release",
|
|
523
|
-
}),
|
|
524
|
-
normalizeRequestArgs,
|
|
525
|
-
workflowState,
|
|
526
|
-
exec: execStub,
|
|
527
|
-
git,
|
|
528
|
-
utils,
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
assert.ok(
|
|
532
|
-
response.content[0].text.includes("✅ Full workflow completed successfully"),
|
|
533
|
-
"run_full_workflow should report success"
|
|
534
|
-
);
|
|
535
|
-
assert.ok(
|
|
536
|
-
commands.some((command) => command.startsWith("git add")),
|
|
537
|
-
"git add should be executed"
|
|
538
|
-
);
|
|
539
|
-
assert.ok(
|
|
540
|
-
commands.some((command) => command.startsWith("git commit")),
|
|
541
|
-
"git commit should be executed"
|
|
542
|
-
);
|
|
543
|
-
assert.ok(
|
|
544
|
-
commands.some((command) => command.startsWith("git push")),
|
|
545
|
-
"git push should be executed"
|
|
546
|
-
);
|
|
547
|
-
assert.ok(
|
|
548
|
-
commands.some((command) => command.startsWith("npm run release")),
|
|
549
|
-
"release command should be executed"
|
|
550
|
-
);
|
|
551
|
-
assert.equal(workflowState.state.currentPhase, "idle");
|
|
552
|
-
});
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
test("run_full_workflow resumes from current phase when steps are already completed", async () => {
|
|
556
|
-
await withWorkflowState(async (workflowState) => {
|
|
557
|
-
workflowState.state.taskDescription = "Ship feature";
|
|
558
|
-
workflowState.state.taskType = "feature";
|
|
559
|
-
workflowState.state.currentPhase = "release";
|
|
560
|
-
workflowState.state.bugFixed = true;
|
|
561
|
-
workflowState.state.testsCreated = true;
|
|
562
|
-
workflowState.state.testsPassed = true;
|
|
563
|
-
workflowState.state.documentationCreated = true;
|
|
564
|
-
workflowState.state.readyCheckCompleted = true;
|
|
565
|
-
workflowState.state.commitAndPushCompleted = true;
|
|
566
|
-
await workflowState.save();
|
|
567
|
-
|
|
568
|
-
const commands = [];
|
|
569
|
-
const execStub = async (command) => {
|
|
570
|
-
commands.push(command);
|
|
571
|
-
if (command === "git status --porcelain") {
|
|
572
|
-
return { stdout: "" };
|
|
573
|
-
}
|
|
574
|
-
return { stdout: "" };
|
|
575
|
-
};
|
|
576
|
-
|
|
577
|
-
const git = {
|
|
578
|
-
hasWorkingChanges: async () => false,
|
|
579
|
-
hasStagedChanges: async () => false,
|
|
580
|
-
hasTestChanges: async () => true,
|
|
581
|
-
getStagedChanges: async () => [],
|
|
582
|
-
getCurrentBranch: async () => "main",
|
|
583
|
-
getPrimaryBranch: async () => "main",
|
|
584
|
-
getLastCommitMessage: async () => "",
|
|
585
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
const response = await handleToolCall({
|
|
589
|
-
request: createRequest("run_full_workflow", {
|
|
590
|
-
summary: "Implement feature",
|
|
591
|
-
testCommand: "npm test",
|
|
592
|
-
documentationType: "README",
|
|
593
|
-
documentationSummary: "Document feature",
|
|
594
|
-
commitMessage: "feat: add feature",
|
|
595
|
-
releaseCommand: "npm run release:patch",
|
|
596
|
-
releaseNotes: "Automated release",
|
|
597
|
-
}),
|
|
598
|
-
normalizeRequestArgs,
|
|
599
|
-
workflowState,
|
|
600
|
-
exec: execStub,
|
|
601
|
-
git,
|
|
602
|
-
utils,
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
assert.ok(
|
|
606
|
-
response.content[0].text.includes("✅ Full workflow completed successfully"),
|
|
607
|
-
"run_full_workflow should report success"
|
|
608
|
-
);
|
|
609
|
-
// Should only run release and complete since earlier steps are done
|
|
610
|
-
assert.ok(
|
|
611
|
-
commands.some((command) => command.startsWith("npm run release")),
|
|
612
|
-
"release command should be executed"
|
|
613
|
-
);
|
|
614
|
-
assert.equal(
|
|
615
|
-
commands.filter((c) => c.startsWith("git commit")).length,
|
|
616
|
-
0,
|
|
617
|
-
"git commit should not be executed when already completed"
|
|
618
|
-
);
|
|
619
|
-
assert.equal(workflowState.state.currentPhase, "idle");
|
|
620
|
-
});
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
test("run_full_workflow validates required arguments", async () => {
|
|
624
|
-
await withWorkflowState(async (workflowState) => {
|
|
625
|
-
workflowState.state.taskDescription = "Ship feature";
|
|
626
|
-
workflowState.state.taskType = "feature";
|
|
627
|
-
workflowState.state.currentPhase = "coding";
|
|
628
|
-
await workflowState.save();
|
|
629
|
-
|
|
630
|
-
const response = await handleToolCall({
|
|
631
|
-
request: createRequest("run_full_workflow", {
|
|
632
|
-
summary: "",
|
|
633
|
-
testCommand: "npm test",
|
|
634
|
-
documentationType: "README",
|
|
635
|
-
documentationSummary: "Doc",
|
|
636
|
-
commitMessage: "feat: add feature",
|
|
637
|
-
releaseCommand: "npm run release:patch",
|
|
638
|
-
}),
|
|
639
|
-
normalizeRequestArgs,
|
|
640
|
-
workflowState,
|
|
641
|
-
exec: async () => ({ stdout: "" }),
|
|
642
|
-
git: {
|
|
643
|
-
hasWorkingChanges: async () => false,
|
|
644
|
-
hasStagedChanges: async () => false,
|
|
645
|
-
hasTestChanges: async () => true,
|
|
646
|
-
getStagedChanges: async () => [],
|
|
647
|
-
getCurrentBranch: async () => "main",
|
|
648
|
-
getPrimaryBranch: async () => "main",
|
|
649
|
-
getLastCommitMessage: async () => "",
|
|
650
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
651
|
-
},
|
|
652
|
-
utils,
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
assert.ok(
|
|
656
|
-
response.content[0].text.includes("Provide a non-empty 'summary'"),
|
|
657
|
-
"run_full_workflow should validate required fields"
|
|
658
|
-
);
|
|
659
|
-
});
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
test("force_complete_task records entry and resets state", async () => {
|
|
663
|
-
await withWorkflowState(async (workflowState) => {
|
|
664
|
-
workflowState.state.taskDescription = "Improve caching";
|
|
665
|
-
workflowState.state.taskType = "feature";
|
|
666
|
-
workflowState.state.lastCommitMessage = "chore: wip";
|
|
667
|
-
workflowState.state.currentPhase = "release";
|
|
668
|
-
await workflowState.save();
|
|
669
|
-
|
|
670
|
-
const response = await handleToolCall({
|
|
671
|
-
request: createRequest("force_complete_task", {
|
|
672
|
-
commitMessage: "chore: force finish",
|
|
673
|
-
reason: "Deadline hit",
|
|
674
|
-
}),
|
|
675
|
-
normalizeRequestArgs,
|
|
676
|
-
workflowState,
|
|
677
|
-
exec: async () => ({ stdout: "" }),
|
|
678
|
-
git: {
|
|
679
|
-
hasWorkingChanges: async () => false,
|
|
680
|
-
hasStagedChanges: async () => false,
|
|
681
|
-
hasTestChanges: async () => true,
|
|
682
|
-
getStagedChanges: async () => [],
|
|
683
|
-
getCurrentBranch: async () => "main",
|
|
684
|
-
getPrimaryBranch: async () => "main",
|
|
685
|
-
getLastCommitMessage: async () => "",
|
|
686
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
687
|
-
},
|
|
688
|
-
utils,
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
assert.ok(response.content[0].text.includes("Task force-completed."));
|
|
692
|
-
assert.equal(workflowState.state.currentPhase, "idle");
|
|
693
|
-
assert.equal(workflowState.state.history.length, 1);
|
|
694
|
-
assert.equal(workflowState.state.history[0].commitMessage, "chore: force finish");
|
|
695
|
-
assert.equal(workflowState.state.history[0].forced, true);
|
|
696
|
-
assert.equal(workflowState.state.history[0].forceReason, "Deadline hit");
|
|
697
|
-
});
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
test("perform_release blocks when new changes detected", async () => {
|
|
701
|
-
await withWorkflowState(async (workflowState) => {
|
|
702
|
-
workflowState.state.currentPhase = "release";
|
|
703
|
-
workflowState.state.commitAndPushCompleted = true;
|
|
704
|
-
workflowState.state.readyCheckCompleted = true;
|
|
705
|
-
await workflowState.save();
|
|
706
|
-
|
|
707
|
-
const commands = [];
|
|
708
|
-
const execStub = async (command) => {
|
|
709
|
-
commands.push(command);
|
|
710
|
-
if (command === "git status --porcelain") {
|
|
711
|
-
return { stdout: "?? new-file.js" };
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
return { stdout: "" };
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
const git = {
|
|
718
|
-
hasWorkingChanges: async () => true,
|
|
719
|
-
hasStagedChanges: async () => false,
|
|
720
|
-
hasTestChanges: async () => true,
|
|
721
|
-
getStagedChanges: async () => [],
|
|
722
|
-
getCurrentBranch: async () => "main",
|
|
723
|
-
getPrimaryBranch: async () => "main",
|
|
724
|
-
getLastCommitMessage: async () => "",
|
|
725
|
-
workingTreeSummary: () => ({ hasChanges: true, lines: ["?? new-file.js"] }),
|
|
726
|
-
};
|
|
727
|
-
|
|
728
|
-
const response = await handleToolCall({
|
|
729
|
-
request: createRequest("perform_release", { command: "patch" }),
|
|
730
|
-
normalizeRequestArgs,
|
|
731
|
-
workflowState,
|
|
732
|
-
exec: execStub,
|
|
733
|
-
git,
|
|
734
|
-
utils,
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
assert.ok(
|
|
738
|
-
response.content[0].text.includes("run 'commit_and_push' again"),
|
|
739
|
-
"response should instruct to recommit"
|
|
740
|
-
);
|
|
741
|
-
assert.equal(workflowState.state.currentPhase, "commit");
|
|
742
|
-
assert.equal(workflowState.state.commitAndPushCompleted, false);
|
|
743
|
-
});
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
test("continue_workflow warns when workflow is idle", async () => {
|
|
747
|
-
await withWorkflowState(async (workflowState) => {
|
|
748
|
-
const git = {
|
|
749
|
-
hasWorkingChanges: async () => false,
|
|
750
|
-
hasStagedChanges: async () => false,
|
|
751
|
-
hasTestChanges: async () => false,
|
|
752
|
-
getStagedChanges: async () => [],
|
|
753
|
-
getCurrentBranch: async () => "main",
|
|
754
|
-
getPrimaryBranch: async () => "main",
|
|
755
|
-
getLastCommitMessage: async () => "",
|
|
756
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
const response = await handleToolCall({
|
|
760
|
-
request: createRequest("continue_workflow", {}),
|
|
761
|
-
normalizeRequestArgs,
|
|
762
|
-
workflowState,
|
|
763
|
-
exec: async () => ({ stdout: "" }),
|
|
764
|
-
git,
|
|
765
|
-
utils,
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
assert.ok(
|
|
769
|
-
response.content[0].text.includes("No active workflow"),
|
|
770
|
-
"Idle workflow should prompt user to start a task"
|
|
771
|
-
);
|
|
772
|
-
});
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
test("continue_workflow resets to commit when new changes detected", async () => {
|
|
776
|
-
await withWorkflowState(async (workflowState) => {
|
|
777
|
-
workflowState.state.currentPhase = "release";
|
|
778
|
-
workflowState.state.commitAndPushCompleted = true;
|
|
779
|
-
workflowState.state.readyCheckCompleted = true;
|
|
780
|
-
await workflowState.save();
|
|
781
|
-
|
|
782
|
-
const git = {
|
|
783
|
-
hasWorkingChanges: async () => true,
|
|
784
|
-
hasStagedChanges: async () => false,
|
|
785
|
-
hasTestChanges: async () => true,
|
|
786
|
-
getStagedChanges: async () => [],
|
|
787
|
-
getCurrentBranch: async () => "main",
|
|
788
|
-
getPrimaryBranch: async () => "main",
|
|
789
|
-
getLastCommitMessage: async () => "",
|
|
790
|
-
workingTreeSummary: () => ({ hasChanges: true, lines: ["?? new-file.js"] }),
|
|
791
|
-
};
|
|
792
|
-
|
|
793
|
-
const response = await handleToolCall({
|
|
794
|
-
request: createRequest("continue_workflow", {}),
|
|
795
|
-
normalizeRequestArgs,
|
|
796
|
-
workflowState,
|
|
797
|
-
exec: async () => ({ stdout: "" }),
|
|
798
|
-
git,
|
|
799
|
-
utils,
|
|
800
|
-
});
|
|
801
|
-
|
|
802
|
-
assert.ok(
|
|
803
|
-
response.content[0].text.includes("Workflow moved back to the commit phase"),
|
|
804
|
-
"response should warn that workflow reset to commit"
|
|
805
|
-
);
|
|
806
|
-
assert.equal(workflowState.state.currentPhase, "commit");
|
|
807
|
-
assert.equal(workflowState.state.commitAndPushCompleted, false);
|
|
808
|
-
});
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
test("commit_and_push recognizes already committed work", async () => {
|
|
812
|
-
await withWorkflowState(async (workflowState) => {
|
|
813
|
-
workflowState.state.readyToCommit = true;
|
|
814
|
-
workflowState.state.readyCheckCompleted = true;
|
|
815
|
-
workflowState.state.testsSkipped = false;
|
|
816
|
-
workflowState.state.testsCreated = true;
|
|
817
|
-
workflowState.state.testsPassed = true;
|
|
818
|
-
workflowState.state.currentPhase = "commit";
|
|
819
|
-
await workflowState.save();
|
|
820
|
-
|
|
821
|
-
const commands = [];
|
|
822
|
-
const execStub = async (command) => {
|
|
823
|
-
commands.push(command);
|
|
824
|
-
return { stdout: "" };
|
|
825
|
-
};
|
|
826
|
-
|
|
827
|
-
const git = {
|
|
828
|
-
hasWorkingChanges: async () => false,
|
|
829
|
-
hasStagedChanges: async () => false,
|
|
830
|
-
hasTestChanges: async () => true,
|
|
831
|
-
getStagedChanges: async () => [],
|
|
832
|
-
getCurrentBranch: async () => "main",
|
|
833
|
-
getPrimaryBranch: async () => "main",
|
|
834
|
-
getLastCommitMessage: async () => "fix: previous work",
|
|
835
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
836
|
-
};
|
|
837
|
-
|
|
838
|
-
const response = await handleToolCall({
|
|
839
|
-
request: createRequest("commit_and_push", { commitMessage: "" }),
|
|
840
|
-
normalizeRequestArgs,
|
|
841
|
-
workflowState,
|
|
842
|
-
exec: execStub,
|
|
843
|
-
git,
|
|
844
|
-
utils,
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
assert.equal(commands.length, 0, "no git commands should run when work is already committed");
|
|
848
|
-
assert.ok(
|
|
849
|
-
response.content[0].text.includes("No changes detected"),
|
|
850
|
-
"response should note that no changes were detected"
|
|
851
|
-
);
|
|
852
|
-
assert.equal(workflowState.state.commitAndPushCompleted, true);
|
|
853
|
-
assert.equal(workflowState.state.lastCommitMessage, "fix: previous work");
|
|
854
|
-
assert.equal(workflowState.state.currentPhase, "release");
|
|
855
|
-
});
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
test("perform_release runs release command before pushing with tags", async () => {
|
|
859
|
-
await withWorkflowState(async (workflowState) => {
|
|
860
|
-
workflowState.state.currentPhase = "release";
|
|
861
|
-
workflowState.state.commitAndPushCompleted = true;
|
|
862
|
-
workflowState.state.readyCheckCompleted = true;
|
|
863
|
-
workflowState.state.lastPushBranch = "feature/split";
|
|
864
|
-
workflowState.state.lastCommitMessage = "fix: address issue";
|
|
865
|
-
await workflowState.save();
|
|
866
|
-
|
|
867
|
-
const commands = [];
|
|
868
|
-
const execStub = async (command) => {
|
|
869
|
-
commands.push(command);
|
|
870
|
-
if (command === "git status --porcelain") {
|
|
871
|
-
return { stdout: "" };
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
return { stdout: "" };
|
|
875
|
-
};
|
|
876
|
-
|
|
877
|
-
const git = {
|
|
878
|
-
hasWorkingChanges: async () => false,
|
|
879
|
-
hasStagedChanges: async () => false,
|
|
880
|
-
hasTestChanges: async () => true,
|
|
881
|
-
getStagedChanges: async () => [],
|
|
882
|
-
getCurrentBranch: async () => "main",
|
|
883
|
-
getLastCommitMessage: async () => "",
|
|
884
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
885
|
-
};
|
|
886
|
-
|
|
887
|
-
const response = await handleToolCall({
|
|
888
|
-
request: createRequest("perform_release", { command: "npm run release" }),
|
|
889
|
-
normalizeRequestArgs,
|
|
890
|
-
workflowState,
|
|
891
|
-
exec: execStub,
|
|
892
|
-
git,
|
|
893
|
-
utils,
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
const releaseCommand = commands.find((command) => command.startsWith("npm run release"));
|
|
897
|
-
assert.ok(releaseCommand, "release command should be executed");
|
|
898
|
-
assert.ok(
|
|
899
|
-
releaseCommand.includes("-- --release-as patch"),
|
|
900
|
-
"release command should append release type"
|
|
901
|
-
);
|
|
902
|
-
|
|
903
|
-
const pushCommand = commands.find((command) => command.startsWith("git push"));
|
|
904
|
-
assert.ok(pushCommand, "git push with tags should be executed after release");
|
|
905
|
-
assert.ok(
|
|
906
|
-
pushCommand.startsWith("git push --follow-tags"),
|
|
907
|
-
"push command should include --follow-tags"
|
|
908
|
-
);
|
|
909
|
-
assert.ok(
|
|
910
|
-
pushCommand.includes("feature/split"),
|
|
911
|
-
"push command should target last recorded branch"
|
|
912
|
-
);
|
|
913
|
-
|
|
914
|
-
assert.equal(workflowState.state.released, true);
|
|
915
|
-
assert.equal(workflowState.state.currentPhase, "ready_to_complete");
|
|
916
|
-
assert.ok(
|
|
917
|
-
response.content[0].text.includes("No workflow history yet") ||
|
|
918
|
-
response.content[0].text.includes("Workflow History"),
|
|
919
|
-
"response should describe workflow history or absence thereof"
|
|
920
|
-
);
|
|
921
|
-
});
|
|
922
|
-
});
|
|
923
|
-
|
|
924
|
-
test("perform_release accepts shorthand patch command", async () => {
|
|
925
|
-
await withWorkflowState(async (workflowState) => {
|
|
926
|
-
workflowState.state.currentPhase = "release";
|
|
927
|
-
workflowState.state.commitAndPushCompleted = true;
|
|
928
|
-
workflowState.state.readyCheckCompleted = true;
|
|
929
|
-
workflowState.state.lastCommitMessage = "fix: adjust labels";
|
|
930
|
-
await workflowState.save();
|
|
931
|
-
|
|
932
|
-
const commands = [];
|
|
933
|
-
const execStub = async (command) => {
|
|
934
|
-
commands.push(command);
|
|
935
|
-
if (command === "git status --porcelain") {
|
|
936
|
-
return { stdout: "" };
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
return { stdout: "" };
|
|
940
|
-
};
|
|
941
|
-
|
|
942
|
-
const git = {
|
|
943
|
-
hasWorkingChanges: async () => false,
|
|
944
|
-
hasStagedChanges: async () => false,
|
|
945
|
-
hasTestChanges: async () => true,
|
|
946
|
-
getStagedChanges: async () => [],
|
|
947
|
-
getCurrentBranch: async () => "main",
|
|
948
|
-
getLastCommitMessage: async () => "fix: adjust labels",
|
|
949
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
950
|
-
};
|
|
951
|
-
|
|
952
|
-
await handleToolCall({
|
|
953
|
-
request: createRequest("perform_release", { command: "patch" }),
|
|
954
|
-
normalizeRequestArgs,
|
|
955
|
-
workflowState,
|
|
956
|
-
exec: execStub,
|
|
957
|
-
git,
|
|
958
|
-
utils,
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
const releaseCommand = commands.find((command) => command.startsWith("npm run release:patch"));
|
|
962
|
-
assert.ok(releaseCommand, "shorthand patch command should map to release:patch script");
|
|
963
|
-
const pushCommand = commands.find((command) => command.startsWith("git push --follow-tags"));
|
|
964
|
-
assert.ok(pushCommand, "git push with tags should run after shorthand release");
|
|
965
|
-
});
|
|
966
|
-
});
|
|
967
|
-
|
|
968
|
-
test("perform_release respects explicit releaseType option", async () => {
|
|
969
|
-
await withWorkflowState(async (workflowState) => {
|
|
970
|
-
workflowState.state.currentPhase = "release";
|
|
971
|
-
workflowState.state.commitAndPushCompleted = true;
|
|
972
|
-
workflowState.state.readyCheckCompleted = true;
|
|
973
|
-
await workflowState.save();
|
|
974
|
-
|
|
975
|
-
const commands = [];
|
|
976
|
-
const execStub = async (command) => {
|
|
977
|
-
commands.push(command);
|
|
978
|
-
if (command === "git status --porcelain") {
|
|
979
|
-
return { stdout: "" };
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
return { stdout: "" };
|
|
983
|
-
};
|
|
984
|
-
|
|
985
|
-
const git = {
|
|
986
|
-
hasWorkingChanges: async () => false,
|
|
987
|
-
hasStagedChanges: async () => false,
|
|
988
|
-
hasTestChanges: async () => true,
|
|
989
|
-
getStagedChanges: async () => [],
|
|
990
|
-
getCurrentBranch: async () => "main",
|
|
991
|
-
getLastCommitMessage: async () => "feat: import improvements",
|
|
992
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
993
|
-
};
|
|
994
|
-
|
|
995
|
-
await handleToolCall({
|
|
996
|
-
request: createRequest("perform_release", {
|
|
997
|
-
command: "npm run release",
|
|
998
|
-
releaseType: "minor",
|
|
999
|
-
}),
|
|
1000
|
-
normalizeRequestArgs,
|
|
1001
|
-
workflowState,
|
|
1002
|
-
exec: execStub,
|
|
1003
|
-
git,
|
|
1004
|
-
utils,
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
const releaseCommand = commands.find((command) => command.startsWith("npm run release"));
|
|
1008
|
-
assert.ok(releaseCommand, "release command should run");
|
|
1009
|
-
assert.ok(
|
|
1010
|
-
releaseCommand.includes("-- --release-as minor"),
|
|
1011
|
-
"release command should append provided releaseType"
|
|
1012
|
-
);
|
|
1013
|
-
});
|
|
1014
|
-
});
|
|
1015
|
-
|
|
1016
|
-
test("perform_release requires commit_and_push completion when tree is clean", async () => {
|
|
1017
|
-
await withWorkflowState(async (workflowState) => {
|
|
1018
|
-
workflowState.state.currentPhase = "release";
|
|
1019
|
-
workflowState.state.commitAndPushCompleted = false;
|
|
1020
|
-
workflowState.state.readyCheckCompleted = true;
|
|
1021
|
-
workflowState.state.lastPushBranch = "";
|
|
1022
|
-
workflowState.state.lastCommitMessage = "";
|
|
1023
|
-
await workflowState.save();
|
|
1024
|
-
|
|
1025
|
-
const commands = [];
|
|
1026
|
-
const execStub = async (command) => {
|
|
1027
|
-
commands.push(command);
|
|
1028
|
-
return { stdout: "" };
|
|
1029
|
-
};
|
|
1030
|
-
|
|
1031
|
-
const git = {
|
|
1032
|
-
hasWorkingChanges: async () => false,
|
|
1033
|
-
hasStagedChanges: async () => false,
|
|
1034
|
-
hasTestChanges: async () => true,
|
|
1035
|
-
getStagedChanges: async () => [],
|
|
1036
|
-
getCurrentBranch: async () => "main",
|
|
1037
|
-
getLastCommitMessage: async () => "chore: already pushed",
|
|
1038
|
-
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
1039
|
-
};
|
|
1040
|
-
|
|
1041
|
-
const response = await handleToolCall({
|
|
1042
|
-
request: createRequest("perform_release", { command: "npm run release" }),
|
|
1043
|
-
normalizeRequestArgs,
|
|
1044
|
-
workflowState,
|
|
1045
|
-
exec: execStub,
|
|
1046
|
-
git,
|
|
1047
|
-
utils,
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
assert.equal(commands.length, 0, "no release commands should run when commit_and_push not completed");
|
|
1051
|
-
assert.equal(workflowState.state.currentPhase, "commit");
|
|
1052
|
-
assert.equal(workflowState.state.commitAndPushCompleted, false);
|
|
1053
|
-
assert.ok(
|
|
1054
|
-
response.content[0].text.includes("Please run 'commit_and_push' after the ready check"),
|
|
1055
|
-
"response should instruct user to run commit_and_push"
|
|
1056
|
-
);
|
|
1057
|
-
});
|
|
1058
|
-
});
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { WorkflowState } from "../workflow-state.js";
|
|
8
|
+
import { handleToolCall } from "../tools/handlers.js";
|
|
9
|
+
import { normalizeRequestArgs, shellEscape } from "../utils.js";
|
|
10
|
+
import {
|
|
11
|
+
createCommitMessageParts,
|
|
12
|
+
determineReleaseTypeFromCommit,
|
|
13
|
+
} from "../commit-helpers.js";
|
|
14
|
+
|
|
15
|
+
async function withWorkflowState(callback) {
|
|
16
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "workflow-handlers-"));
|
|
17
|
+
const stateFile = path.join(tempDir, "state.json");
|
|
18
|
+
const state = new WorkflowState(stateFile);
|
|
19
|
+
await state.load();
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await callback(state);
|
|
23
|
+
} finally {
|
|
24
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const utils = {
|
|
29
|
+
shellEscape,
|
|
30
|
+
determineReleaseTypeFromCommit,
|
|
31
|
+
createCommitMessageParts,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function createRequest(name, args) {
|
|
35
|
+
return {
|
|
36
|
+
params: {
|
|
37
|
+
name,
|
|
38
|
+
arguments: args,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test("run_tests guidance mentions core documentation types", async () => {
|
|
44
|
+
await withWorkflowState(async (workflowState) => {
|
|
45
|
+
workflowState.state.bugFixed = true;
|
|
46
|
+
workflowState.state.testsCreated = true;
|
|
47
|
+
await workflowState.save();
|
|
48
|
+
|
|
49
|
+
const response = await handleToolCall({
|
|
50
|
+
request: createRequest("run_tests", {
|
|
51
|
+
passed: true,
|
|
52
|
+
testCommand: "npm test",
|
|
53
|
+
}),
|
|
54
|
+
normalizeRequestArgs,
|
|
55
|
+
workflowState,
|
|
56
|
+
exec: async () => ({ stdout: "" }),
|
|
57
|
+
git: {
|
|
58
|
+
hasWorkingChanges: async () => false,
|
|
59
|
+
hasStagedChanges: async () => false,
|
|
60
|
+
hasTestChanges: async () => true,
|
|
61
|
+
getStagedChanges: async () => [],
|
|
62
|
+
getCurrentBranch: async () => "main",
|
|
63
|
+
getPrimaryBranch: async () => "main",
|
|
64
|
+
getLastCommitMessage: async () => "",
|
|
65
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
66
|
+
},
|
|
67
|
+
utils,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const text = response.content[0].text;
|
|
71
|
+
assert.ok(
|
|
72
|
+
text.includes("create or update documentation using 'create_documentation'"),
|
|
73
|
+
"run_tests guidance should mention create or update documentation",
|
|
74
|
+
);
|
|
75
|
+
assert.ok(
|
|
76
|
+
text.includes('"PRD", "README", "RELEASE_NOTES"'),
|
|
77
|
+
"run_tests guidance should list core documentation types",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("create_documentation accepts PRD and records created/updated message", async () => {
|
|
83
|
+
await withWorkflowState(async (workflowState) => {
|
|
84
|
+
workflowState.state.testsPassed = true;
|
|
85
|
+
await workflowState.save();
|
|
86
|
+
|
|
87
|
+
const response = await handleToolCall({
|
|
88
|
+
request: createRequest("create_documentation", {
|
|
89
|
+
documentationType: "PRD",
|
|
90
|
+
summary: "Updated PRD for documentation workflow expectations",
|
|
91
|
+
}),
|
|
92
|
+
normalizeRequestArgs,
|
|
93
|
+
workflowState,
|
|
94
|
+
exec: async () => ({ stdout: "" }),
|
|
95
|
+
git: {
|
|
96
|
+
hasWorkingChanges: async () => false,
|
|
97
|
+
hasStagedChanges: async () => false,
|
|
98
|
+
hasTestChanges: async () => true,
|
|
99
|
+
getStagedChanges: async () => [],
|
|
100
|
+
getCurrentBranch: async () => "main",
|
|
101
|
+
getPrimaryBranch: async () => "main",
|
|
102
|
+
getLastCommitMessage: async () => "",
|
|
103
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
104
|
+
},
|
|
105
|
+
utils,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
assert.equal(workflowState.state.documentationCreated, true);
|
|
109
|
+
assert.equal(workflowState.state.documentationType, "PRD");
|
|
110
|
+
assert.equal(
|
|
111
|
+
workflowState.state.documentationSummary,
|
|
112
|
+
"Updated PRD for documentation workflow expectations",
|
|
113
|
+
);
|
|
114
|
+
assert.ok(
|
|
115
|
+
response.content[0].text.includes("Documentation created/updated!"),
|
|
116
|
+
"create_documentation response should mention created/updated",
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("commit_and_push commits changes and pushes", async () => {
|
|
122
|
+
await withWorkflowState(async (workflowState) => {
|
|
123
|
+
workflowState.state.readyToCommit = true;
|
|
124
|
+
workflowState.state.readyCheckCompleted = true;
|
|
125
|
+
workflowState.state.testsSkipped = false;
|
|
126
|
+
workflowState.state.testsCreated = true;
|
|
127
|
+
workflowState.state.testsPassed = true;
|
|
128
|
+
workflowState.state.currentPhase = "commit";
|
|
129
|
+
await workflowState.save();
|
|
130
|
+
|
|
131
|
+
const commands = [];
|
|
132
|
+
const execStub = async (command) => {
|
|
133
|
+
commands.push(command);
|
|
134
|
+
return { stdout: "" };
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const git = {
|
|
138
|
+
hasWorkingChanges: async () => true,
|
|
139
|
+
hasStagedChanges: async () => false,
|
|
140
|
+
hasTestChanges: async () => true,
|
|
141
|
+
getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
|
|
142
|
+
getCurrentBranch: async () => "main",
|
|
143
|
+
getPrimaryBranch: async () => "main",
|
|
144
|
+
getLastCommitMessage: async () => "",
|
|
145
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const response = await handleToolCall({
|
|
149
|
+
request: createRequest("commit_and_push", { commitMessage: "" }),
|
|
150
|
+
normalizeRequestArgs,
|
|
151
|
+
workflowState,
|
|
152
|
+
exec: execStub,
|
|
153
|
+
git,
|
|
154
|
+
utils,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const pushCommand = commands.find((command) => command.startsWith("git push"));
|
|
158
|
+
assert.ok(pushCommand, "git push should be executed during commit");
|
|
159
|
+
assert.ok(
|
|
160
|
+
pushCommand.startsWith("git push origin"),
|
|
161
|
+
"push command should target origin"
|
|
162
|
+
);
|
|
163
|
+
assert.equal(
|
|
164
|
+
commands.some((command) => command.startsWith("git commit")),
|
|
165
|
+
true,
|
|
166
|
+
"git commit should be executed"
|
|
167
|
+
);
|
|
168
|
+
assert.ok(
|
|
169
|
+
response.content[0].text.includes("Commit and push completed!"),
|
|
170
|
+
"response should confirm commit and push"
|
|
171
|
+
);
|
|
172
|
+
assert.equal(workflowState.state.commitAndPushCompleted, true);
|
|
173
|
+
assert.equal(workflowState.state.lastCommitMessage.length > 0, true);
|
|
174
|
+
assert.equal(workflowState.state.currentPhase, "release");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("run_full_workflow iterates until commit/release complete with clean tree", async () => {
|
|
179
|
+
await withWorkflowState(async (workflowState) => {
|
|
180
|
+
workflowState.state.taskDescription = "Ship feature";
|
|
181
|
+
workflowState.state.taskType = "feature";
|
|
182
|
+
workflowState.state.currentPhase = "commit";
|
|
183
|
+
workflowState.state.bugFixed = true;
|
|
184
|
+
workflowState.state.testsCreated = true;
|
|
185
|
+
workflowState.state.testsPassed = true;
|
|
186
|
+
workflowState.state.documentationCreated = true;
|
|
187
|
+
workflowState.state.readyToCommit = true;
|
|
188
|
+
workflowState.state.readyCheckCompleted = true;
|
|
189
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
190
|
+
workflowState.state.released = false;
|
|
191
|
+
workflowState.state.documentationType = "README";
|
|
192
|
+
workflowState.state.documentationSummary = "Docs";
|
|
193
|
+
workflowState.state.fixSummary = "Feature done";
|
|
194
|
+
await workflowState.save();
|
|
195
|
+
|
|
196
|
+
const commands = [];
|
|
197
|
+
const execStub = async (command) => {
|
|
198
|
+
commands.push(command);
|
|
199
|
+
if (command === "git status --porcelain") {
|
|
200
|
+
return { stdout: "" };
|
|
201
|
+
}
|
|
202
|
+
return { stdout: "" };
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const git = {
|
|
206
|
+
hasWorkingChanges: async () => false,
|
|
207
|
+
hasStagedChanges: async () => false,
|
|
208
|
+
hasTestChanges: async () => true,
|
|
209
|
+
getStagedChanges: async () => [],
|
|
210
|
+
getCurrentBranch: async () => "main",
|
|
211
|
+
getPrimaryBranch: async () => "main",
|
|
212
|
+
getLastCommitMessage: async () => "chore: existing",
|
|
213
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const response = await handleToolCall({
|
|
217
|
+
request: createRequest("run_full_workflow", {
|
|
218
|
+
summary: "Feature done",
|
|
219
|
+
testCommand: "npm test",
|
|
220
|
+
documentationType: "README",
|
|
221
|
+
documentationSummary: "Docs",
|
|
222
|
+
commitMessage: "feat: ship feature",
|
|
223
|
+
releaseCommand: "npm run release:patch",
|
|
224
|
+
}),
|
|
225
|
+
normalizeRequestArgs,
|
|
226
|
+
workflowState,
|
|
227
|
+
exec: execStub,
|
|
228
|
+
git,
|
|
229
|
+
utils,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
assert.ok(
|
|
233
|
+
response.content[0].text.includes("✅ Full workflow completed successfully"),
|
|
234
|
+
"run_full_workflow should finish even when commit is already clean"
|
|
235
|
+
);
|
|
236
|
+
assert.ok(
|
|
237
|
+
response.content[0].text.includes("🎉 Cheers!"),
|
|
238
|
+
"run_full_workflow should include celebration message"
|
|
239
|
+
);
|
|
240
|
+
assert.equal(commands.includes("npm run release:patch"), true, "release command should run");
|
|
241
|
+
assert.equal(workflowState.state.currentPhase, "idle");
|
|
242
|
+
assert.equal(workflowState.state.history.length, 1);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("commit_and_push uses primary branch when no branch specified", async () => {
|
|
247
|
+
await withWorkflowState(async (workflowState) => {
|
|
248
|
+
workflowState.state.readyToCommit = true;
|
|
249
|
+
workflowState.state.readyCheckCompleted = true;
|
|
250
|
+
workflowState.state.testsSkipped = false;
|
|
251
|
+
workflowState.state.testsCreated = true;
|
|
252
|
+
workflowState.state.testsPassed = true;
|
|
253
|
+
workflowState.state.currentPhase = "commit";
|
|
254
|
+
await workflowState.save();
|
|
255
|
+
|
|
256
|
+
const commands = [];
|
|
257
|
+
const execStub = async (command) => {
|
|
258
|
+
commands.push(command);
|
|
259
|
+
return { stdout: "" };
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const git = {
|
|
263
|
+
hasWorkingChanges: async () => true,
|
|
264
|
+
hasStagedChanges: async () => false,
|
|
265
|
+
hasTestChanges: async () => true,
|
|
266
|
+
getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
|
|
267
|
+
getCurrentBranch: async () => "feature/test",
|
|
268
|
+
getPrimaryBranch: async () => "main",
|
|
269
|
+
getLastCommitMessage: async () => "",
|
|
270
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const response = await handleToolCall({
|
|
274
|
+
request: createRequest("commit_and_push", { commitMessage: "feat: test" }),
|
|
275
|
+
normalizeRequestArgs,
|
|
276
|
+
workflowState,
|
|
277
|
+
exec: execStub,
|
|
278
|
+
git,
|
|
279
|
+
utils,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const pushCommand = commands.find((command) => command.startsWith("git push"));
|
|
283
|
+
assert.ok(pushCommand, "git push should be executed");
|
|
284
|
+
assert.ok(
|
|
285
|
+
pushCommand.includes("main"),
|
|
286
|
+
"push command should use detected primary branch (main)"
|
|
287
|
+
);
|
|
288
|
+
assert.ok(
|
|
289
|
+
response.content[0].text.includes("Commit and push completed!"),
|
|
290
|
+
"response should confirm commit and push"
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("commit_and_push falls back to master when main not found", async () => {
|
|
296
|
+
await withWorkflowState(async (workflowState) => {
|
|
297
|
+
workflowState.state.readyToCommit = true;
|
|
298
|
+
workflowState.state.readyCheckCompleted = true;
|
|
299
|
+
workflowState.state.testsSkipped = false;
|
|
300
|
+
workflowState.state.testsCreated = true;
|
|
301
|
+
workflowState.state.testsPassed = true;
|
|
302
|
+
workflowState.state.currentPhase = "commit";
|
|
303
|
+
await workflowState.save();
|
|
304
|
+
|
|
305
|
+
const commands = [];
|
|
306
|
+
const execStub = async (command) => {
|
|
307
|
+
commands.push(command);
|
|
308
|
+
return { stdout: "" };
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const git = {
|
|
312
|
+
hasWorkingChanges: async () => true,
|
|
313
|
+
hasStagedChanges: async () => false,
|
|
314
|
+
hasTestChanges: async () => true,
|
|
315
|
+
getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
|
|
316
|
+
getCurrentBranch: async () => "feature/test",
|
|
317
|
+
getPrimaryBranch: async () => "master",
|
|
318
|
+
getLastCommitMessage: async () => "",
|
|
319
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const response = await handleToolCall({
|
|
323
|
+
request: createRequest("commit_and_push", { commitMessage: "feat: test" }),
|
|
324
|
+
normalizeRequestArgs,
|
|
325
|
+
workflowState,
|
|
326
|
+
exec: execStub,
|
|
327
|
+
git,
|
|
328
|
+
utils,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const pushCommand = commands.find((command) => command.startsWith("git push"));
|
|
332
|
+
assert.ok(pushCommand, "git push should be executed");
|
|
333
|
+
assert.ok(
|
|
334
|
+
pushCommand.includes("master"),
|
|
335
|
+
"push command should use detected primary branch (master)"
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("run_full_workflow uses provided branch for release push when commit is clean", async () => {
|
|
341
|
+
await withWorkflowState(async (workflowState) => {
|
|
342
|
+
workflowState.state.taskDescription = "Ship feature";
|
|
343
|
+
workflowState.state.taskType = "feature";
|
|
344
|
+
workflowState.state.currentPhase = "commit";
|
|
345
|
+
workflowState.state.bugFixed = true;
|
|
346
|
+
workflowState.state.testsCreated = true;
|
|
347
|
+
workflowState.state.testsPassed = true;
|
|
348
|
+
workflowState.state.documentationCreated = true;
|
|
349
|
+
workflowState.state.readyToCommit = true;
|
|
350
|
+
workflowState.state.readyCheckCompleted = true;
|
|
351
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
352
|
+
workflowState.state.released = false;
|
|
353
|
+
workflowState.state.documentationType = "README";
|
|
354
|
+
workflowState.state.documentationSummary = "Docs";
|
|
355
|
+
workflowState.state.fixSummary = "Feature done";
|
|
356
|
+
await workflowState.save();
|
|
357
|
+
|
|
358
|
+
const commands = [];
|
|
359
|
+
const execStub = async (command) => {
|
|
360
|
+
commands.push(command);
|
|
361
|
+
if (command === "git status --porcelain") {
|
|
362
|
+
return { stdout: "" };
|
|
363
|
+
}
|
|
364
|
+
return { stdout: "" };
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const git = {
|
|
368
|
+
hasWorkingChanges: async () => false,
|
|
369
|
+
hasStagedChanges: async () => false,
|
|
370
|
+
hasTestChanges: async () => true,
|
|
371
|
+
getStagedChanges: async () => [],
|
|
372
|
+
getCurrentBranch: async () => "feature/local",
|
|
373
|
+
getPrimaryBranch: async () => "main",
|
|
374
|
+
getLastCommitMessage: async () => "chore: existing",
|
|
375
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const branchName = "master";
|
|
379
|
+
const response = await handleToolCall({
|
|
380
|
+
request: createRequest("run_full_workflow", {
|
|
381
|
+
summary: "Feature done",
|
|
382
|
+
testCommand: "npm test",
|
|
383
|
+
documentationType: "README",
|
|
384
|
+
documentationSummary: "Docs",
|
|
385
|
+
commitMessage: "feat: ship feature",
|
|
386
|
+
branch: branchName,
|
|
387
|
+
releaseCommand: "npm run release:patch",
|
|
388
|
+
}),
|
|
389
|
+
normalizeRequestArgs,
|
|
390
|
+
workflowState,
|
|
391
|
+
exec: execStub,
|
|
392
|
+
git,
|
|
393
|
+
utils,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
assert.ok(
|
|
397
|
+
response.content[0].text.includes("✅ Full workflow completed successfully"),
|
|
398
|
+
"run_full_workflow should finish successfully"
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const tagPushCommand = commands.find((command) => command.startsWith("git push --follow-tags"));
|
|
402
|
+
assert.ok(tagPushCommand, "release should push tags");
|
|
403
|
+
assert.ok(
|
|
404
|
+
/origin\s+['"]?master['"]?/.test(tagPushCommand),
|
|
405
|
+
`release push should target the provided branch (${branchName})`
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
assert.equal(workflowState.state.currentPhase, "idle");
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("project_summary_data reads persisted file and falls back", async () => {
|
|
413
|
+
await withWorkflowState(async (workflowState) => {
|
|
414
|
+
// Seed some history
|
|
415
|
+
workflowState.addToHistory({ taskDescription: "Add login feature", taskType: "feature", commitMessage: "feat: login" });
|
|
416
|
+
await workflowState.save(); // This should create project-summary.json
|
|
417
|
+
|
|
418
|
+
const response = await handleToolCall({
|
|
419
|
+
request: createRequest("project_summary_data", {}),
|
|
420
|
+
normalizeRequestArgs,
|
|
421
|
+
workflowState,
|
|
422
|
+
exec: async () => ({ stdout: "" }),
|
|
423
|
+
git: {
|
|
424
|
+
hasWorkingChanges: async () => false,
|
|
425
|
+
hasStagedChanges: async () => false,
|
|
426
|
+
hasTestChanges: async () => true,
|
|
427
|
+
getStagedChanges: async () => [],
|
|
428
|
+
getCurrentBranch: async () => "main",
|
|
429
|
+
getPrimaryBranch: async () => "main",
|
|
430
|
+
getLastCommitMessage: async () => "",
|
|
431
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
432
|
+
},
|
|
433
|
+
utils,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const text = response.content[0].text;
|
|
437
|
+
assert.ok(text.includes("from persisted data"), "should indicate persisted source");
|
|
438
|
+
assert.ok(text.includes("Total tasks completed: 1"), "should count tasks");
|
|
439
|
+
assert.ok(text.includes("feature: 1"), "should show feature count");
|
|
440
|
+
assert.ok(text.includes("Updated:"), "should show updated timestamp");
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("project_summary aggregates task types and recent activity", async () => {
|
|
445
|
+
await withWorkflowState(async (workflowState) => {
|
|
446
|
+
// Seed some history
|
|
447
|
+
workflowState.addToHistory({ taskDescription: "Add login feature", taskType: "feature", commitMessage: "feat: login" });
|
|
448
|
+
workflowState.addToHistory({ taskDescription: "Fix auth bug", taskType: "bugfix", commitMessage: "fix: auth" });
|
|
449
|
+
workflowState.addToHistory({ taskDescription: "Refactor utils", taskType: "refactor", commitMessage: "refactor: utils" });
|
|
450
|
+
await workflowState.save();
|
|
451
|
+
|
|
452
|
+
const response = await handleToolCall({
|
|
453
|
+
request: createRequest("project_summary", {}),
|
|
454
|
+
normalizeRequestArgs,
|
|
455
|
+
workflowState,
|
|
456
|
+
exec: async () => ({ stdout: "" }),
|
|
457
|
+
git: {
|
|
458
|
+
hasWorkingChanges: async () => false,
|
|
459
|
+
hasStagedChanges: async () => false,
|
|
460
|
+
hasTestChanges: async () => true,
|
|
461
|
+
getStagedChanges: async () => [],
|
|
462
|
+
getCurrentBranch: async () => "main",
|
|
463
|
+
getPrimaryBranch: async () => "main",
|
|
464
|
+
getLastCommitMessage: async () => "",
|
|
465
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
466
|
+
},
|
|
467
|
+
utils,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const text = response.content[0].text;
|
|
471
|
+
assert.ok(text.includes("Project Knowledge Summary"), "should include summary title");
|
|
472
|
+
assert.ok(text.includes("Total tasks completed: 3"), "should count tasks");
|
|
473
|
+
assert.ok(text.includes("feature: 1"), "should show feature count");
|
|
474
|
+
assert.ok(text.includes("bugfix: 1"), "should show bugfix count");
|
|
475
|
+
assert.ok(text.includes("refactor: 1"), "should show refactor count");
|
|
476
|
+
assert.ok(text.includes("Add login feature"), "should list recent tasks");
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("run_full_workflow executes all steps successfully", async () => {
|
|
481
|
+
await withWorkflowState(async (workflowState) => {
|
|
482
|
+
workflowState.state.taskDescription = "Ship feature";
|
|
483
|
+
workflowState.state.taskType = "feature";
|
|
484
|
+
workflowState.state.currentPhase = "coding";
|
|
485
|
+
await workflowState.save();
|
|
486
|
+
|
|
487
|
+
const commands = [];
|
|
488
|
+
let workingChanges = true;
|
|
489
|
+
const execStub = async (command) => {
|
|
490
|
+
commands.push(command);
|
|
491
|
+
if (command.startsWith("git commit") || command.startsWith("git push")) {
|
|
492
|
+
workingChanges = false;
|
|
493
|
+
}
|
|
494
|
+
if (command === "git status --porcelain") {
|
|
495
|
+
return { stdout: workingChanges ? " M src/app.js" : "" };
|
|
496
|
+
}
|
|
497
|
+
return { stdout: "" };
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const git = {
|
|
501
|
+
hasWorkingChanges: async () => workingChanges,
|
|
502
|
+
hasStagedChanges: async () => false,
|
|
503
|
+
hasTestChanges: async () => true,
|
|
504
|
+
getStagedChanges: async () => [{ status: "M", path: "src/app.js" }],
|
|
505
|
+
getCurrentBranch: async () => "main",
|
|
506
|
+
getPrimaryBranch: async () => "main",
|
|
507
|
+
getLastCommitMessage: async () => "",
|
|
508
|
+
workingTreeSummary: () =>
|
|
509
|
+
workingChanges
|
|
510
|
+
? { hasChanges: true, lines: ["M src/app.js"] }
|
|
511
|
+
: { hasChanges: false, lines: [] },
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const response = await handleToolCall({
|
|
515
|
+
request: createRequest("run_full_workflow", {
|
|
516
|
+
summary: "Implement feature",
|
|
517
|
+
testCommand: "npm test",
|
|
518
|
+
documentationType: "README",
|
|
519
|
+
documentationSummary: "Document feature",
|
|
520
|
+
commitMessage: "feat: add feature",
|
|
521
|
+
releaseCommand: "npm run release:patch",
|
|
522
|
+
releaseNotes: "Automated release",
|
|
523
|
+
}),
|
|
524
|
+
normalizeRequestArgs,
|
|
525
|
+
workflowState,
|
|
526
|
+
exec: execStub,
|
|
527
|
+
git,
|
|
528
|
+
utils,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
assert.ok(
|
|
532
|
+
response.content[0].text.includes("✅ Full workflow completed successfully"),
|
|
533
|
+
"run_full_workflow should report success"
|
|
534
|
+
);
|
|
535
|
+
assert.ok(
|
|
536
|
+
commands.some((command) => command.startsWith("git add")),
|
|
537
|
+
"git add should be executed"
|
|
538
|
+
);
|
|
539
|
+
assert.ok(
|
|
540
|
+
commands.some((command) => command.startsWith("git commit")),
|
|
541
|
+
"git commit should be executed"
|
|
542
|
+
);
|
|
543
|
+
assert.ok(
|
|
544
|
+
commands.some((command) => command.startsWith("git push")),
|
|
545
|
+
"git push should be executed"
|
|
546
|
+
);
|
|
547
|
+
assert.ok(
|
|
548
|
+
commands.some((command) => command.startsWith("npm run release")),
|
|
549
|
+
"release command should be executed"
|
|
550
|
+
);
|
|
551
|
+
assert.equal(workflowState.state.currentPhase, "idle");
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test("run_full_workflow resumes from current phase when steps are already completed", async () => {
|
|
556
|
+
await withWorkflowState(async (workflowState) => {
|
|
557
|
+
workflowState.state.taskDescription = "Ship feature";
|
|
558
|
+
workflowState.state.taskType = "feature";
|
|
559
|
+
workflowState.state.currentPhase = "release";
|
|
560
|
+
workflowState.state.bugFixed = true;
|
|
561
|
+
workflowState.state.testsCreated = true;
|
|
562
|
+
workflowState.state.testsPassed = true;
|
|
563
|
+
workflowState.state.documentationCreated = true;
|
|
564
|
+
workflowState.state.readyCheckCompleted = true;
|
|
565
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
566
|
+
await workflowState.save();
|
|
567
|
+
|
|
568
|
+
const commands = [];
|
|
569
|
+
const execStub = async (command) => {
|
|
570
|
+
commands.push(command);
|
|
571
|
+
if (command === "git status --porcelain") {
|
|
572
|
+
return { stdout: "" };
|
|
573
|
+
}
|
|
574
|
+
return { stdout: "" };
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const git = {
|
|
578
|
+
hasWorkingChanges: async () => false,
|
|
579
|
+
hasStagedChanges: async () => false,
|
|
580
|
+
hasTestChanges: async () => true,
|
|
581
|
+
getStagedChanges: async () => [],
|
|
582
|
+
getCurrentBranch: async () => "main",
|
|
583
|
+
getPrimaryBranch: async () => "main",
|
|
584
|
+
getLastCommitMessage: async () => "",
|
|
585
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const response = await handleToolCall({
|
|
589
|
+
request: createRequest("run_full_workflow", {
|
|
590
|
+
summary: "Implement feature",
|
|
591
|
+
testCommand: "npm test",
|
|
592
|
+
documentationType: "README",
|
|
593
|
+
documentationSummary: "Document feature",
|
|
594
|
+
commitMessage: "feat: add feature",
|
|
595
|
+
releaseCommand: "npm run release:patch",
|
|
596
|
+
releaseNotes: "Automated release",
|
|
597
|
+
}),
|
|
598
|
+
normalizeRequestArgs,
|
|
599
|
+
workflowState,
|
|
600
|
+
exec: execStub,
|
|
601
|
+
git,
|
|
602
|
+
utils,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
assert.ok(
|
|
606
|
+
response.content[0].text.includes("✅ Full workflow completed successfully"),
|
|
607
|
+
"run_full_workflow should report success"
|
|
608
|
+
);
|
|
609
|
+
// Should only run release and complete since earlier steps are done
|
|
610
|
+
assert.ok(
|
|
611
|
+
commands.some((command) => command.startsWith("npm run release")),
|
|
612
|
+
"release command should be executed"
|
|
613
|
+
);
|
|
614
|
+
assert.equal(
|
|
615
|
+
commands.filter((c) => c.startsWith("git commit")).length,
|
|
616
|
+
0,
|
|
617
|
+
"git commit should not be executed when already completed"
|
|
618
|
+
);
|
|
619
|
+
assert.equal(workflowState.state.currentPhase, "idle");
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test("run_full_workflow validates required arguments", async () => {
|
|
624
|
+
await withWorkflowState(async (workflowState) => {
|
|
625
|
+
workflowState.state.taskDescription = "Ship feature";
|
|
626
|
+
workflowState.state.taskType = "feature";
|
|
627
|
+
workflowState.state.currentPhase = "coding";
|
|
628
|
+
await workflowState.save();
|
|
629
|
+
|
|
630
|
+
const response = await handleToolCall({
|
|
631
|
+
request: createRequest("run_full_workflow", {
|
|
632
|
+
summary: "",
|
|
633
|
+
testCommand: "npm test",
|
|
634
|
+
documentationType: "README",
|
|
635
|
+
documentationSummary: "Doc",
|
|
636
|
+
commitMessage: "feat: add feature",
|
|
637
|
+
releaseCommand: "npm run release:patch",
|
|
638
|
+
}),
|
|
639
|
+
normalizeRequestArgs,
|
|
640
|
+
workflowState,
|
|
641
|
+
exec: async () => ({ stdout: "" }),
|
|
642
|
+
git: {
|
|
643
|
+
hasWorkingChanges: async () => false,
|
|
644
|
+
hasStagedChanges: async () => false,
|
|
645
|
+
hasTestChanges: async () => true,
|
|
646
|
+
getStagedChanges: async () => [],
|
|
647
|
+
getCurrentBranch: async () => "main",
|
|
648
|
+
getPrimaryBranch: async () => "main",
|
|
649
|
+
getLastCommitMessage: async () => "",
|
|
650
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
651
|
+
},
|
|
652
|
+
utils,
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
assert.ok(
|
|
656
|
+
response.content[0].text.includes("Provide a non-empty 'summary'"),
|
|
657
|
+
"run_full_workflow should validate required fields"
|
|
658
|
+
);
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("force_complete_task records entry and resets state", async () => {
|
|
663
|
+
await withWorkflowState(async (workflowState) => {
|
|
664
|
+
workflowState.state.taskDescription = "Improve caching";
|
|
665
|
+
workflowState.state.taskType = "feature";
|
|
666
|
+
workflowState.state.lastCommitMessage = "chore: wip";
|
|
667
|
+
workflowState.state.currentPhase = "release";
|
|
668
|
+
await workflowState.save();
|
|
669
|
+
|
|
670
|
+
const response = await handleToolCall({
|
|
671
|
+
request: createRequest("force_complete_task", {
|
|
672
|
+
commitMessage: "chore: force finish",
|
|
673
|
+
reason: "Deadline hit",
|
|
674
|
+
}),
|
|
675
|
+
normalizeRequestArgs,
|
|
676
|
+
workflowState,
|
|
677
|
+
exec: async () => ({ stdout: "" }),
|
|
678
|
+
git: {
|
|
679
|
+
hasWorkingChanges: async () => false,
|
|
680
|
+
hasStagedChanges: async () => false,
|
|
681
|
+
hasTestChanges: async () => true,
|
|
682
|
+
getStagedChanges: async () => [],
|
|
683
|
+
getCurrentBranch: async () => "main",
|
|
684
|
+
getPrimaryBranch: async () => "main",
|
|
685
|
+
getLastCommitMessage: async () => "",
|
|
686
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
687
|
+
},
|
|
688
|
+
utils,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
assert.ok(response.content[0].text.includes("Task force-completed."));
|
|
692
|
+
assert.equal(workflowState.state.currentPhase, "idle");
|
|
693
|
+
assert.equal(workflowState.state.history.length, 1);
|
|
694
|
+
assert.equal(workflowState.state.history[0].commitMessage, "chore: force finish");
|
|
695
|
+
assert.equal(workflowState.state.history[0].forced, true);
|
|
696
|
+
assert.equal(workflowState.state.history[0].forceReason, "Deadline hit");
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
test("perform_release blocks when new changes detected", async () => {
|
|
701
|
+
await withWorkflowState(async (workflowState) => {
|
|
702
|
+
workflowState.state.currentPhase = "release";
|
|
703
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
704
|
+
workflowState.state.readyCheckCompleted = true;
|
|
705
|
+
await workflowState.save();
|
|
706
|
+
|
|
707
|
+
const commands = [];
|
|
708
|
+
const execStub = async (command) => {
|
|
709
|
+
commands.push(command);
|
|
710
|
+
if (command === "git status --porcelain") {
|
|
711
|
+
return { stdout: "?? new-file.js" };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return { stdout: "" };
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const git = {
|
|
718
|
+
hasWorkingChanges: async () => true,
|
|
719
|
+
hasStagedChanges: async () => false,
|
|
720
|
+
hasTestChanges: async () => true,
|
|
721
|
+
getStagedChanges: async () => [],
|
|
722
|
+
getCurrentBranch: async () => "main",
|
|
723
|
+
getPrimaryBranch: async () => "main",
|
|
724
|
+
getLastCommitMessage: async () => "",
|
|
725
|
+
workingTreeSummary: () => ({ hasChanges: true, lines: ["?? new-file.js"] }),
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const response = await handleToolCall({
|
|
729
|
+
request: createRequest("perform_release", { command: "patch" }),
|
|
730
|
+
normalizeRequestArgs,
|
|
731
|
+
workflowState,
|
|
732
|
+
exec: execStub,
|
|
733
|
+
git,
|
|
734
|
+
utils,
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
assert.ok(
|
|
738
|
+
response.content[0].text.includes("run 'commit_and_push' again"),
|
|
739
|
+
"response should instruct to recommit"
|
|
740
|
+
);
|
|
741
|
+
assert.equal(workflowState.state.currentPhase, "commit");
|
|
742
|
+
assert.equal(workflowState.state.commitAndPushCompleted, false);
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test("continue_workflow warns when workflow is idle", async () => {
|
|
747
|
+
await withWorkflowState(async (workflowState) => {
|
|
748
|
+
const git = {
|
|
749
|
+
hasWorkingChanges: async () => false,
|
|
750
|
+
hasStagedChanges: async () => false,
|
|
751
|
+
hasTestChanges: async () => false,
|
|
752
|
+
getStagedChanges: async () => [],
|
|
753
|
+
getCurrentBranch: async () => "main",
|
|
754
|
+
getPrimaryBranch: async () => "main",
|
|
755
|
+
getLastCommitMessage: async () => "",
|
|
756
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
const response = await handleToolCall({
|
|
760
|
+
request: createRequest("continue_workflow", {}),
|
|
761
|
+
normalizeRequestArgs,
|
|
762
|
+
workflowState,
|
|
763
|
+
exec: async () => ({ stdout: "" }),
|
|
764
|
+
git,
|
|
765
|
+
utils,
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
assert.ok(
|
|
769
|
+
response.content[0].text.includes("No active workflow"),
|
|
770
|
+
"Idle workflow should prompt user to start a task"
|
|
771
|
+
);
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test("continue_workflow resets to commit when new changes detected", async () => {
|
|
776
|
+
await withWorkflowState(async (workflowState) => {
|
|
777
|
+
workflowState.state.currentPhase = "release";
|
|
778
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
779
|
+
workflowState.state.readyCheckCompleted = true;
|
|
780
|
+
await workflowState.save();
|
|
781
|
+
|
|
782
|
+
const git = {
|
|
783
|
+
hasWorkingChanges: async () => true,
|
|
784
|
+
hasStagedChanges: async () => false,
|
|
785
|
+
hasTestChanges: async () => true,
|
|
786
|
+
getStagedChanges: async () => [],
|
|
787
|
+
getCurrentBranch: async () => "main",
|
|
788
|
+
getPrimaryBranch: async () => "main",
|
|
789
|
+
getLastCommitMessage: async () => "",
|
|
790
|
+
workingTreeSummary: () => ({ hasChanges: true, lines: ["?? new-file.js"] }),
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const response = await handleToolCall({
|
|
794
|
+
request: createRequest("continue_workflow", {}),
|
|
795
|
+
normalizeRequestArgs,
|
|
796
|
+
workflowState,
|
|
797
|
+
exec: async () => ({ stdout: "" }),
|
|
798
|
+
git,
|
|
799
|
+
utils,
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
assert.ok(
|
|
803
|
+
response.content[0].text.includes("Workflow moved back to the commit phase"),
|
|
804
|
+
"response should warn that workflow reset to commit"
|
|
805
|
+
);
|
|
806
|
+
assert.equal(workflowState.state.currentPhase, "commit");
|
|
807
|
+
assert.equal(workflowState.state.commitAndPushCompleted, false);
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
test("commit_and_push recognizes already committed work", async () => {
|
|
812
|
+
await withWorkflowState(async (workflowState) => {
|
|
813
|
+
workflowState.state.readyToCommit = true;
|
|
814
|
+
workflowState.state.readyCheckCompleted = true;
|
|
815
|
+
workflowState.state.testsSkipped = false;
|
|
816
|
+
workflowState.state.testsCreated = true;
|
|
817
|
+
workflowState.state.testsPassed = true;
|
|
818
|
+
workflowState.state.currentPhase = "commit";
|
|
819
|
+
await workflowState.save();
|
|
820
|
+
|
|
821
|
+
const commands = [];
|
|
822
|
+
const execStub = async (command) => {
|
|
823
|
+
commands.push(command);
|
|
824
|
+
return { stdout: "" };
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
const git = {
|
|
828
|
+
hasWorkingChanges: async () => false,
|
|
829
|
+
hasStagedChanges: async () => false,
|
|
830
|
+
hasTestChanges: async () => true,
|
|
831
|
+
getStagedChanges: async () => [],
|
|
832
|
+
getCurrentBranch: async () => "main",
|
|
833
|
+
getPrimaryBranch: async () => "main",
|
|
834
|
+
getLastCommitMessage: async () => "fix: previous work",
|
|
835
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
const response = await handleToolCall({
|
|
839
|
+
request: createRequest("commit_and_push", { commitMessage: "" }),
|
|
840
|
+
normalizeRequestArgs,
|
|
841
|
+
workflowState,
|
|
842
|
+
exec: execStub,
|
|
843
|
+
git,
|
|
844
|
+
utils,
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
assert.equal(commands.length, 0, "no git commands should run when work is already committed");
|
|
848
|
+
assert.ok(
|
|
849
|
+
response.content[0].text.includes("No changes detected"),
|
|
850
|
+
"response should note that no changes were detected"
|
|
851
|
+
);
|
|
852
|
+
assert.equal(workflowState.state.commitAndPushCompleted, true);
|
|
853
|
+
assert.equal(workflowState.state.lastCommitMessage, "fix: previous work");
|
|
854
|
+
assert.equal(workflowState.state.currentPhase, "release");
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test("perform_release runs release command before pushing with tags", async () => {
|
|
859
|
+
await withWorkflowState(async (workflowState) => {
|
|
860
|
+
workflowState.state.currentPhase = "release";
|
|
861
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
862
|
+
workflowState.state.readyCheckCompleted = true;
|
|
863
|
+
workflowState.state.lastPushBranch = "feature/split";
|
|
864
|
+
workflowState.state.lastCommitMessage = "fix: address issue";
|
|
865
|
+
await workflowState.save();
|
|
866
|
+
|
|
867
|
+
const commands = [];
|
|
868
|
+
const execStub = async (command) => {
|
|
869
|
+
commands.push(command);
|
|
870
|
+
if (command === "git status --porcelain") {
|
|
871
|
+
return { stdout: "" };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return { stdout: "" };
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const git = {
|
|
878
|
+
hasWorkingChanges: async () => false,
|
|
879
|
+
hasStagedChanges: async () => false,
|
|
880
|
+
hasTestChanges: async () => true,
|
|
881
|
+
getStagedChanges: async () => [],
|
|
882
|
+
getCurrentBranch: async () => "main",
|
|
883
|
+
getLastCommitMessage: async () => "",
|
|
884
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
const response = await handleToolCall({
|
|
888
|
+
request: createRequest("perform_release", { command: "npm run release" }),
|
|
889
|
+
normalizeRequestArgs,
|
|
890
|
+
workflowState,
|
|
891
|
+
exec: execStub,
|
|
892
|
+
git,
|
|
893
|
+
utils,
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
const releaseCommand = commands.find((command) => command.startsWith("npm run release"));
|
|
897
|
+
assert.ok(releaseCommand, "release command should be executed");
|
|
898
|
+
assert.ok(
|
|
899
|
+
releaseCommand.includes("-- --release-as patch"),
|
|
900
|
+
"release command should append release type"
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
const pushCommand = commands.find((command) => command.startsWith("git push"));
|
|
904
|
+
assert.ok(pushCommand, "git push with tags should be executed after release");
|
|
905
|
+
assert.ok(
|
|
906
|
+
pushCommand.startsWith("git push --follow-tags"),
|
|
907
|
+
"push command should include --follow-tags"
|
|
908
|
+
);
|
|
909
|
+
assert.ok(
|
|
910
|
+
pushCommand.includes("feature/split"),
|
|
911
|
+
"push command should target last recorded branch"
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
assert.equal(workflowState.state.released, true);
|
|
915
|
+
assert.equal(workflowState.state.currentPhase, "ready_to_complete");
|
|
916
|
+
assert.ok(
|
|
917
|
+
response.content[0].text.includes("No workflow history yet") ||
|
|
918
|
+
response.content[0].text.includes("Workflow History"),
|
|
919
|
+
"response should describe workflow history or absence thereof"
|
|
920
|
+
);
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
test("perform_release accepts shorthand patch command", async () => {
|
|
925
|
+
await withWorkflowState(async (workflowState) => {
|
|
926
|
+
workflowState.state.currentPhase = "release";
|
|
927
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
928
|
+
workflowState.state.readyCheckCompleted = true;
|
|
929
|
+
workflowState.state.lastCommitMessage = "fix: adjust labels";
|
|
930
|
+
await workflowState.save();
|
|
931
|
+
|
|
932
|
+
const commands = [];
|
|
933
|
+
const execStub = async (command) => {
|
|
934
|
+
commands.push(command);
|
|
935
|
+
if (command === "git status --porcelain") {
|
|
936
|
+
return { stdout: "" };
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return { stdout: "" };
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
const git = {
|
|
943
|
+
hasWorkingChanges: async () => false,
|
|
944
|
+
hasStagedChanges: async () => false,
|
|
945
|
+
hasTestChanges: async () => true,
|
|
946
|
+
getStagedChanges: async () => [],
|
|
947
|
+
getCurrentBranch: async () => "main",
|
|
948
|
+
getLastCommitMessage: async () => "fix: adjust labels",
|
|
949
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
await handleToolCall({
|
|
953
|
+
request: createRequest("perform_release", { command: "patch" }),
|
|
954
|
+
normalizeRequestArgs,
|
|
955
|
+
workflowState,
|
|
956
|
+
exec: execStub,
|
|
957
|
+
git,
|
|
958
|
+
utils,
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
const releaseCommand = commands.find((command) => command.startsWith("npm run release:patch"));
|
|
962
|
+
assert.ok(releaseCommand, "shorthand patch command should map to release:patch script");
|
|
963
|
+
const pushCommand = commands.find((command) => command.startsWith("git push --follow-tags"));
|
|
964
|
+
assert.ok(pushCommand, "git push with tags should run after shorthand release");
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
test("perform_release respects explicit releaseType option", async () => {
|
|
969
|
+
await withWorkflowState(async (workflowState) => {
|
|
970
|
+
workflowState.state.currentPhase = "release";
|
|
971
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
972
|
+
workflowState.state.readyCheckCompleted = true;
|
|
973
|
+
await workflowState.save();
|
|
974
|
+
|
|
975
|
+
const commands = [];
|
|
976
|
+
const execStub = async (command) => {
|
|
977
|
+
commands.push(command);
|
|
978
|
+
if (command === "git status --porcelain") {
|
|
979
|
+
return { stdout: "" };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return { stdout: "" };
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
const git = {
|
|
986
|
+
hasWorkingChanges: async () => false,
|
|
987
|
+
hasStagedChanges: async () => false,
|
|
988
|
+
hasTestChanges: async () => true,
|
|
989
|
+
getStagedChanges: async () => [],
|
|
990
|
+
getCurrentBranch: async () => "main",
|
|
991
|
+
getLastCommitMessage: async () => "feat: import improvements",
|
|
992
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
await handleToolCall({
|
|
996
|
+
request: createRequest("perform_release", {
|
|
997
|
+
command: "npm run release",
|
|
998
|
+
releaseType: "minor",
|
|
999
|
+
}),
|
|
1000
|
+
normalizeRequestArgs,
|
|
1001
|
+
workflowState,
|
|
1002
|
+
exec: execStub,
|
|
1003
|
+
git,
|
|
1004
|
+
utils,
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
const releaseCommand = commands.find((command) => command.startsWith("npm run release"));
|
|
1008
|
+
assert.ok(releaseCommand, "release command should run");
|
|
1009
|
+
assert.ok(
|
|
1010
|
+
releaseCommand.includes("-- --release-as minor"),
|
|
1011
|
+
"release command should append provided releaseType"
|
|
1012
|
+
);
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
test("perform_release requires commit_and_push completion when tree is clean", async () => {
|
|
1017
|
+
await withWorkflowState(async (workflowState) => {
|
|
1018
|
+
workflowState.state.currentPhase = "release";
|
|
1019
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
1020
|
+
workflowState.state.readyCheckCompleted = true;
|
|
1021
|
+
workflowState.state.lastPushBranch = "";
|
|
1022
|
+
workflowState.state.lastCommitMessage = "";
|
|
1023
|
+
await workflowState.save();
|
|
1024
|
+
|
|
1025
|
+
const commands = [];
|
|
1026
|
+
const execStub = async (command) => {
|
|
1027
|
+
commands.push(command);
|
|
1028
|
+
return { stdout: "" };
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
const git = {
|
|
1032
|
+
hasWorkingChanges: async () => false,
|
|
1033
|
+
hasStagedChanges: async () => false,
|
|
1034
|
+
hasTestChanges: async () => true,
|
|
1035
|
+
getStagedChanges: async () => [],
|
|
1036
|
+
getCurrentBranch: async () => "main",
|
|
1037
|
+
getLastCommitMessage: async () => "chore: already pushed",
|
|
1038
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
const response = await handleToolCall({
|
|
1042
|
+
request: createRequest("perform_release", { command: "npm run release" }),
|
|
1043
|
+
normalizeRequestArgs,
|
|
1044
|
+
workflowState,
|
|
1045
|
+
exec: execStub,
|
|
1046
|
+
git,
|
|
1047
|
+
utils,
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
assert.equal(commands.length, 0, "no release commands should run when commit_and_push not completed");
|
|
1051
|
+
assert.equal(workflowState.state.currentPhase, "commit");
|
|
1052
|
+
assert.equal(workflowState.state.commitAndPushCompleted, false);
|
|
1053
|
+
assert.ok(
|
|
1054
|
+
response.content[0].text.includes("Please run 'commit_and_push' after the ready check"),
|
|
1055
|
+
"response should instruct user to run commit_and_push"
|
|
1056
|
+
);
|
|
1057
|
+
});
|
|
1058
|
+
});
|