@programinglive/dev-workflow-mcp-server 1.8.1 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.env.example +21 -1
  2. package/AGENTS.md +25 -0
  3. package/README.md +257 -672
  4. package/index.js +5 -4
  5. package/package.json +11 -6
  6. package/scripts/bootstrap-unix.sh +55 -0
  7. package/scripts/bootstrap-windows.ps1 +75 -0
  8. package/scripts/install-husky.js +43 -0
  9. package/scripts/setup-stack.js +436 -0
  10. package/tools/definitions.js +19 -0
  11. package/tools/handler-modules/commit-handlers.js +2 -1
  12. package/tools/handler-modules/index.js +13 -7
  13. package/tools/handler-modules/shared.js +7 -41
  14. package/tools/handler-modules/status-handlers.js +3 -1
  15. package/tools/handler-modules/workflow-handlers.js +26 -8
  16. package/workflow-state.js +10 -0
  17. package/.agent/workflows/dev-workflow-bugfix.md +0 -250
  18. package/.agent/workflows/dev-workflow-feature.md +0 -201
  19. package/.agent/workflows/dev-workflow-full-cycle.md +0 -400
  20. package/.agent/workflows/dev-workflow-refactor.md +0 -238
  21. package/.env.production +0 -16
  22. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -34
  23. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  24. package/.github/pull_request_template.md +0 -22
  25. package/.github/workflows/github-release.yml +0 -131
  26. package/.husky/commit-msg +0 -3
  27. package/.husky/pre-commit +0 -2
  28. package/CHANGELOG.md +0 -1145
  29. package/docs/GCP_DEPLOYMENT.md +0 -298
  30. package/docs/NETLIFY_DEPLOYMENT.md +0 -125
  31. package/docs/PLESK_DEPLOYMENT.md +0 -170
  32. package/docs/PRD.md +0 -86
  33. package/docs/WORKFLOW-BEST-PRACTICES.md +0 -58
  34. package/docs/antigravity/EXAMPLES.md +0 -535
  35. package/docs/antigravity/GETTING-STARTED.md +0 -292
  36. package/docs/antigravity/TROUBLESHOOTING.md +0 -742
  37. package/docs/antigravity/WALKTHROUGH.md +0 -25
  38. package/docs/antigravity/WORKFLOW-TEMPLATES.md +0 -670
  39. package/docs/release-notes/RELEASE_NOTES.md +0 -524
  40. package/docs/web-dashboard.md +0 -175
  41. package/tailwind.config.js +0 -8
  42. package/tests/build.test.js +0 -74
  43. package/tests/db-adapters.test.js +0 -108
  44. package/tests/env-support.test.js +0 -46
  45. package/tests/exec.test.js +0 -35
  46. package/tests/handlers-commit.test.js +0 -174
  47. package/tests/handlers-release.test.js +0 -212
  48. package/tests/handlers-workflow.test.js +0 -68
  49. package/tests/handlers.test.js +0 -1142
  50. package/tests/hello-world.test.js +0 -13
  51. package/tests/mcpRegistry.test.js +0 -65
  52. package/tests/patch-release.test.js +0 -7
  53. package/tests/release-wrapper.test.js +0 -107
  54. package/tests/startup-resilience.test.js +0 -73
  55. package/tests/test-helpers.js +0 -50
  56. package/tests/workflowState.test.js +0 -520
  57. package/web/Dockerfile.api +0 -25
  58. package/web/README.md +0 -133
  59. package/web/__tests__/api/workflow-history.test.js +0 -63
  60. package/web/api-server.js +0 -211
  61. package/web/app/docs/page.tsx +0 -202
  62. package/web/app/favicon.ico +0 -0
  63. package/web/app/globals.css +0 -91
  64. package/web/app/history/page.tsx +0 -220
  65. package/web/app/layout.tsx +0 -39
  66. package/web/app/login/page.tsx +0 -142
  67. package/web/app/page.tsx +0 -13
  68. package/web/components/ApiReference.tsx +0 -20
  69. package/web/components/Features.tsx +0 -61
  70. package/web/components/Footer.tsx +0 -9
  71. package/web/components/Hero.tsx +0 -96
  72. package/web/components/History.tsx +0 -175
  73. package/web/components/Navbar.tsx +0 -208
  74. package/web/components/Terminal.tsx +0 -114
  75. package/web/components/ThemeProvider.tsx +0 -58
  76. package/web/eslint.config.mjs +0 -18
  77. package/web/lib/api-client.ts +0 -123
  78. package/web/lib/auth.js +0 -75
  79. package/web/lib/db.js +0 -131
  80. package/web/lib/docs.js +0 -73
  81. package/web/middleware/auth.js +0 -26
  82. package/web/netlify.toml +0 -8
  83. package/web/next.config.ts +0 -19
  84. package/web/package-lock.json +0 -10911
  85. package/web/package.json +0 -42
  86. package/web/postcss.config.mjs +0 -9
  87. package/web/public/assets/android-chrome-192x192.png +0 -0
  88. package/web/public/assets/android-chrome-512x512.png +0 -0
  89. package/web/public/assets/apple-touch-icon.png +0 -0
  90. package/web/public/assets/favicon-16x16.png +0 -0
  91. package/web/public/assets/favicon-32x32.png +0 -0
  92. package/web/public/assets/favicon.ico +0 -0
  93. package/web/public/assets/favicon.png +0 -0
  94. package/web/public/assets/logo.png +0 -0
  95. package/web/public/assets/og-preview.png +0 -0
  96. package/web/public/assets/site.webmanifest +0 -1
  97. package/web/public/file.svg +0 -1
  98. package/web/public/globe.svg +0 -1
  99. package/web/public/next.svg +0 -1
  100. package/web/public/vercel.svg +0 -1
  101. package/web/public/window.svg +0 -1
  102. package/web/scripts/db-query-history.js +0 -120
  103. package/web/scripts/smoke-test.js +0 -30
  104. package/web/server.js +0 -184
  105. package/web/tailwind.config.ts +0 -20
  106. package/web/tsconfig.json +0 -42
@@ -1,74 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert';
3
- import { readFile, access } from 'fs/promises';
4
- import { exec as execCallback } from 'child_process';
5
- import { promisify } from 'util';
6
- import path from 'path';
7
-
8
- const exec = promisify(execCallback);
9
-
10
- test('build generates dist/index.mjs file', async () => {
11
- const { stderr } = await exec('npx cross-env NODE_ENV=development npm run build:server');
12
- // Allow the known vite warning about node:process
13
- const knownWarning = '[plugin vite:resolve] Module "node:process" has been externalized for browser compatibility';
14
- const tailwindOutput = '≈ tailwindcss';
15
- const browserslistWarning = 'Browserslist: caniuse-lite is outdated';
16
- const actualStderr = stderr.replace(/\x1B\[[0-9;]*m/g, ''); // Remove ANSI codes
17
- assert(
18
- actualStderr === '' || actualStderr.includes(knownWarning) || actualStderr.includes(tailwindOutput) || actualStderr.includes(browserslistWarning),
19
- `Build should not have unexpected errors: ${actualStderr}`
20
- );
21
-
22
- // Verify the file exists
23
- const distPath = path.join(process.cwd(), 'dist', 'index.mjs');
24
- await access(distPath);
25
-
26
- // Verify it contains expected content
27
- const content = await readFile(distPath, 'utf-8');
28
- assert(content.includes('main'), 'Bundle should contain main function');
29
- assert(content.length > 1000, 'Bundle should be substantial');
30
- });
31
-
32
- test('build script exists in package.json', async () => {
33
- const packagePath = path.join(process.cwd(), 'package.json');
34
- const content = await readFile(packagePath, 'utf-8');
35
- const pkg = JSON.parse(content);
36
-
37
- assert.strictEqual(typeof pkg.scripts['build:server'], 'string');
38
- assert(pkg.scripts['build:server'].includes('vite build'));
39
- });
40
-
41
- test('Hero component fetches npm downloads directly', async () => {
42
- const heroPath = path.join(process.cwd(), 'web', 'components', 'Hero.tsx');
43
- const heroContent = await readFile(heroPath, 'utf-8');
44
-
45
- // Verify it fetches npm downloads directly from npm API
46
- assert(heroContent.includes('api.npmjs.org'), 'Hero should fetch downloads from npm API');
47
- assert(heroContent.includes('useState<string | null>(null)'), 'Hero should have version state');
48
- });
49
-
50
- test('Netlify deployment configuration exists for static export', async () => {
51
- const netlifyTomlPath = path.join(process.cwd(), 'web', 'netlify.toml');
52
- await access(netlifyTomlPath);
53
-
54
- const content = await readFile(netlifyTomlPath, 'utf-8');
55
- assert(content.includes('[build]'), 'netlify.toml should have build section');
56
- assert(content.includes('npm run build'), 'netlify.toml should have build command');
57
- assert(content.includes('publish = "out"'), 'netlify.toml should publish out directory for static export');
58
- });
59
-
60
- test('Web package.json does not include better-sqlite3', async () => {
61
- const webPackagePath = path.join(process.cwd(), 'web', 'package.json');
62
- const content = await readFile(webPackagePath, 'utf-8');
63
- const pkg = JSON.parse(content);
64
-
65
- assert(!pkg.dependencies['better-sqlite3'], 'Web should not depend on better-sqlite3 for Netlify compatibility');
66
- });
67
-
68
- test('Next.js config uses static export for Netlify', async () => {
69
- const nextConfigPath = path.join(process.cwd(), 'web', 'next.config.ts');
70
- const content = await readFile(nextConfigPath, 'utf-8');
71
-
72
- assert(content.includes("output: 'export'"), 'next.config.ts should use static export');
73
- assert(content.includes('unoptimized: true'), 'next.config.ts should disable image optimization for static export');
74
- });
@@ -1,108 +0,0 @@
1
- import "dotenv/config";
2
- import test from "node:test";
3
- import assert from "node:assert/strict";
4
- import path from "node:path";
5
- import fs from "node:fs/promises";
6
- import os from "node:os";
7
-
8
- import { SqliteAdapter } from "../db/adapters/sqlite.js";
9
- import { MysqlAdapter } from "../db/adapters/mysql.js";
10
- import { PostgresAdapter } from "../db/adapters/postgres.js";
11
-
12
-
13
- // Helper to run common adapter tests
14
- async function runGenericAdapterTests(t, adapter, testUserId, testProject) {
15
- await t.test("connects and migrates", async () => {
16
- await adapter.connect();
17
- // No error means success
18
- });
19
-
20
- await t.test("inserts and retrieves history", async () => {
21
- const entry = {
22
- taskDescription: "Test Task",
23
- taskType: "feature",
24
- commitMessage: "feat: test",
25
- timestamp: new Date().toISOString(),
26
- };
27
- await adapter.insertHistoryEntry(testUserId, testProject, entry);
28
-
29
- const history = await adapter.getHistoryForUser(testUserId, testProject, { pageSize: 1 });
30
- assert.equal(history.entries.length, 1);
31
- assert.equal(history.entries[0].task_description, "Test Task");
32
- });
33
-
34
- await t.test("saves and retrieves state", async () => {
35
- const state = { phase: "testing", count: 123 };
36
- await adapter.saveState(testUserId, testProject, state);
37
- const retrieved = await adapter.getState(testUserId, testProject);
38
- assert.deepEqual(retrieved, state);
39
- });
40
-
41
- await t.test("history summary aggregation", async () => {
42
- // already inserted one entry
43
- const summary = await adapter.getHistorySummary(testUserId, testProject, { frequency: 'daily' });
44
- assert.ok(summary.length > 0);
45
- });
46
-
47
- await t.test("project summary update", async () => {
48
- const summary = await adapter.getSummaryForUser(testUserId, testProject);
49
- assert.ok(summary);
50
- assert.ok(summary.totalTasks >= 1);
51
- });
52
- }
53
-
54
- test("Database Adapters", async (t) => {
55
- // 1. SQLite (Always Test)
56
- await t.test("SqliteAdapter", async (t) => {
57
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "sqlite-test-"));
58
- const dbPath = path.join(tmpDir, "test.db");
59
-
60
- // Convert to file URI for connection string if needed, or just pass path
61
- // The adapter expects a connection URL for SQLite usually: file:///path/to/db or just path?
62
- // Looking at sqlite.js: uri: this.config.connectionUrl.replace("sqlite://", "")
63
- // So we invoke it with sqlite://<path>
64
- const connectionUrl = `sqlite://${dbPath}`;
65
-
66
- const adapter = new SqliteAdapter({ connectionUrl });
67
-
68
- try {
69
- await runGenericAdapterTests(t, adapter, "user-sqlite", tmpDir);
70
- } finally {
71
- if (adapter.db) adapter.db.close();
72
- await fs.rm(tmpDir, { recursive: true, force: true });
73
- }
74
- });
75
-
76
- // 2. MySQL (Conditional)
77
- // Check specific test variable OR generic one if it maps to mysql
78
- const mysqlUrl = process.env.TEST_MYSQL_URL ||
79
- (process.env.DEV_WORKFLOW_DB_URL && process.env.DEV_WORKFLOW_DB_URL.startsWith("mysql") ? process.env.DEV_WORKFLOW_DB_URL : null);
80
-
81
- await t.test("MysqlAdapter", { skip: !mysqlUrl }, async (t) => {
82
- const adapter = new MysqlAdapter({ connectionUrl: mysqlUrl });
83
- try {
84
- // We use a random user ID to avoid clashing with verification script data
85
- // although verification cleaned up mostly.
86
- const testId = `test-mysql-${Date.now()}`;
87
- const testProj = process.cwd();
88
- await runGenericAdapterTests(t, adapter, testId, testProj);
89
- } finally {
90
- if (adapter.pool) await adapter.pool.end();
91
- }
92
- });
93
-
94
- // 3. PostgreSQL (Conditional)
95
- const pgUrl = process.env.TEST_POSTGRES_URL ||
96
- (process.env.DEV_WORKFLOW_DB_URL && process.env.DEV_WORKFLOW_DB_URL.startsWith("postgres") ? process.env.DEV_WORKFLOW_DB_URL : null);
97
-
98
- await t.test("PostgresAdapter", { skip: !pgUrl }, async (t) => {
99
- const adapter = new PostgresAdapter({ connectionUrl: pgUrl });
100
- try {
101
- const testId = `test-pg-${Date.now()}`;
102
- const testProj = process.cwd();
103
- await runGenericAdapterTests(t, adapter, testId, testProj);
104
- } finally {
105
- if (adapter.pool) await adapter.pool.end();
106
- }
107
- });
108
- });
@@ -1,46 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import path from "node:path";
4
- import fs from "node:fs/promises";
5
- import os from "node:os";
6
- import { exec } from "node:child_process";
7
- import { promisify } from "node:util";
8
- import { pathToFileURL } from "node:url";
9
-
10
- const execAsync = promisify(exec);
11
-
12
- test("Environment Configuration", async (t) => {
13
- await t.test("loads variables from .env file", async () => {
14
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "env-test-"));
15
- const originalCwd = process.cwd();
16
- const indexJsPath = path.resolve(originalCwd, "index.js");
17
- const indexUrl = pathToFileURL(indexJsPath).href;
18
-
19
- try {
20
- // Create a .env file
21
- await fs.writeFile(path.join(tmpDir, ".env"), "TEST_ENV_VAR=loaded_successfully");
22
-
23
- // Verify node can run and load it via index.js import
24
- // We import index.js to trigger the 'dotenv/config' import.
25
- // Since index.js starts a server if run directly, we just import it.
26
- // However, we need to run a script in the NEW CWD.
27
-
28
- const script = `
29
- import '${indexUrl}';
30
- console.log("VAR:" + process.env.TEST_ENV_VAR);
31
- process.exit(0);
32
- `;
33
-
34
- const scriptPath = path.join(tmpDir, "check_env.js");
35
- await fs.writeFile(scriptPath, script);
36
-
37
- // Execute in tmpDir
38
- const { stdout } = await execAsync(`node check_env.js`, { cwd: tmpDir });
39
-
40
- assert.ok(stdout.includes("VAR:loaded_successfully"), "Output should contain loaded variable");
41
-
42
- } finally {
43
- await fs.rm(tmpDir, { recursive: true, force: true });
44
- }
45
- });
46
- });
@@ -1,35 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert';
3
- import { exec } from '../exec.js';
4
- import { tmpdir } from 'os';
5
- import { mkdtemp, rm } from 'fs/promises';
6
- import path from 'path';
7
-
8
- test('exec runs commands successfully', async () => {
9
- const echoCommand = process.platform === 'win32' ? 'echo hello world' : 'echo "hello world"';
10
- const { stdout } = await exec(echoCommand);
11
- const normalized = stdout.trim().replace(/^"(.*)"$/, '$1');
12
- assert.strictEqual(normalized, 'hello world');
13
- });
14
-
15
- test('exec respects explicit cwd option', async () => {
16
- const tempDir = await mkdtemp(path.join(tmpdir(), 'mcp-test-'));
17
-
18
- try {
19
- const dirCommand = process.platform === 'win32' ? 'cd' : 'pwd';
20
- const { stdout } = await exec(dirCommand, { cwd: tempDir });
21
- // Just verify it runs in the specified directory (path may vary by platform)
22
- assert(stdout.trim().toLowerCase().includes('mcp-test-'));
23
- } finally {
24
- await rm(tempDir, { recursive: true, force: true });
25
- }
26
- });
27
-
28
- test('exec handles command errors', async () => {
29
- try {
30
- await exec('command-that-does-not-exist');
31
- assert.fail('Should have thrown an error');
32
- } catch (error) {
33
- assert(error.code !== undefined || error.message.includes('command-not-found'));
34
- }
35
- });
@@ -1,174 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { handleToolCall } from "../tools/handlers.js";
4
- import { withWorkflowState, testUtils, createRequest, createGitMock } from "./test-helpers.js";
5
-
6
- test("commit_and_push commits changes and pushes", async () => {
7
- await withWorkflowState(async (workflowState) => {
8
- workflowState.state.readyToCommit = true;
9
- workflowState.state.readyCheckCompleted = true;
10
- workflowState.state.testsSkipped = false;
11
- workflowState.state.testsCreated = true;
12
- workflowState.state.testsPassed = true;
13
- workflowState.state.currentPhase = "commit";
14
- await workflowState.save();
15
-
16
- const commands = [];
17
- const execStub = async (command) => {
18
- commands.push(command);
19
- return { stdout: "" };
20
- };
21
-
22
- const response = await handleToolCall({
23
- request: createRequest("commit_and_push", { commitMessage: "" }),
24
- normalizeRequestArgs: (args) => ({ args, error: null }),
25
- workflowState,
26
- exec: execStub,
27
- git: createGitMock({
28
- hasWorkingChanges: async () => true,
29
- getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
30
- getStatusOutput: async () => "M tests/handlers.test.js\nM src/index.js",
31
- containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
32
- }),
33
- utils: testUtils,
34
- });
35
-
36
- const pushCommand = commands.find((command) => command.startsWith("git push"));
37
- assert.ok(pushCommand, "git push should be executed during commit");
38
- assert.ok(
39
- pushCommand.startsWith("git push origin"),
40
- "push command should target origin"
41
- );
42
- assert.equal(
43
- commands.some((command) => command.startsWith("git commit")),
44
- true,
45
- "git commit should be executed"
46
- );
47
- assert.ok(
48
- response.content[0].text.includes("Commit and push completed!"),
49
- "response should confirm commit and push"
50
- );
51
- assert.equal(workflowState.state.commitAndPushCompleted, true);
52
- assert.equal(workflowState.state.lastCommitMessage.length > 0, true);
53
- assert.equal(workflowState.state.currentPhase, "release");
54
- });
55
- });
56
-
57
- test("commit_and_push uses primary branch when no branch specified", async () => {
58
- await withWorkflowState(async (workflowState) => {
59
- workflowState.state.readyToCommit = true;
60
- workflowState.state.readyCheckCompleted = true;
61
- workflowState.state.testsSkipped = false;
62
- workflowState.state.testsCreated = true;
63
- workflowState.state.testsPassed = true;
64
- workflowState.state.currentPhase = "commit";
65
- await workflowState.save();
66
-
67
- const commands = [];
68
- const execStub = async (command) => {
69
- commands.push(command);
70
- return { stdout: "" };
71
- };
72
-
73
- const response = await handleToolCall({
74
- request: createRequest("commit_and_push", { commitMessage: "feat: test" }),
75
- normalizeRequestArgs: (args) => ({ args, error: null }),
76
- workflowState,
77
- exec: execStub,
78
- git: createGitMock({
79
- hasWorkingChanges: async () => true,
80
- getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
81
- getCurrentBranch: async () => "feature/test",
82
- getStatusOutput: async () => "M tests/handlers.test.js\nM src/index.js",
83
- containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
84
- }),
85
- utils: testUtils,
86
- });
87
-
88
- const pushCommand = commands.find((command) => command.startsWith("git push"));
89
- assert.ok(pushCommand, "git push should be executed");
90
- assert.ok(
91
- /origin\s+['"]?main['"]?/.test(pushCommand),
92
- "push should use primary branch (main) when no branch specified"
93
- );
94
- });
95
- });
96
-
97
- test("commit_and_push falls back to master when main not found", async () => {
98
- await withWorkflowState(async (workflowState) => {
99
- workflowState.state.readyToCommit = true;
100
- workflowState.state.readyCheckCompleted = true;
101
- workflowState.state.testsSkipped = false;
102
- workflowState.state.testsCreated = true;
103
- workflowState.state.testsPassed = true;
104
- workflowState.state.currentPhase = "commit";
105
- await workflowState.save();
106
-
107
- const commands = [];
108
- const execStub = async (command) => {
109
- commands.push(command);
110
- return { stdout: "" };
111
- };
112
-
113
- const response = await handleToolCall({
114
- request: createRequest("commit_and_push", { commitMessage: "feat: test" }),
115
- normalizeRequestArgs: (args) => ({ args, error: null }),
116
- workflowState,
117
- exec: execStub,
118
- git: createGitMock({
119
- hasWorkingChanges: async () => true,
120
- getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
121
- getPrimaryBranch: async () => "master",
122
- getStatusOutput: async () => "M tests/handlers.test.js\nM src/index.js",
123
- containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
124
- }),
125
- utils: testUtils,
126
- });
127
-
128
- const pushCommand = commands.find((command) => command.startsWith("git push"));
129
- assert.ok(pushCommand, "git push should be executed");
130
- assert.ok(
131
- /origin\s+['"]?master['"]?/.test(pushCommand),
132
- "push should use master when main not found"
133
- );
134
- });
135
- });
136
-
137
- test("commit_and_push recognizes already committed work", async () => {
138
- await withWorkflowState(async (workflowState) => {
139
- workflowState.state.readyToCommit = true;
140
- workflowState.state.readyCheckCompleted = true;
141
- workflowState.state.testsSkipped = false;
142
- workflowState.state.testsCreated = true;
143
- workflowState.state.testsPassed = true;
144
- workflowState.state.currentPhase = "commit";
145
- await workflowState.save();
146
-
147
- const commands = [];
148
- const execStub = async (command) => {
149
- commands.push(command);
150
- return { stdout: "" };
151
- };
152
-
153
- const response = await handleToolCall({
154
- request: createRequest("commit_and_push", { commitMessage: "" }),
155
- normalizeRequestArgs: (args) => ({ args, error: null }),
156
- workflowState,
157
- exec: execStub,
158
- git: createGitMock({
159
- hasWorkingChanges: async () => false,
160
- getLastCommitMessage: async () => "fix: previous work",
161
- getStatusOutput: async () => "",
162
- containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
163
- }),
164
- utils: testUtils,
165
- });
166
-
167
- assert.ok(
168
- response.content[0].text.includes("No changes detected"),
169
- "response should indicate no changes"
170
- );
171
- assert.equal(workflowState.state.commitAndPushCompleted, true);
172
- assert.equal(workflowState.state.currentPhase, "release");
173
- });
174
- });
@@ -1,212 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { handleToolCall } from "../tools/handlers.js";
4
- import { withWorkflowState, testUtils, createRequest, createGitMock } from "./test-helpers.js";
5
-
6
- test("perform_release runs release command before pushing with tags", async () => {
7
- await withWorkflowState(async (workflowState) => {
8
- workflowState.state.currentPhase = "release";
9
- workflowState.state.commitAndPushCompleted = true;
10
- workflowState.state.readyCheckCompleted = true;
11
- workflowState.state.lastPushBranch = "feature/split";
12
- workflowState.state.lastCommitMessage = "fix: address issue";
13
- await workflowState.save();
14
-
15
- const commands = [];
16
- const execStub = async (command) => {
17
- commands.push(command);
18
- if (command === "git status --porcelain") {
19
- return { stdout: "" };
20
- }
21
- return { stdout: "" };
22
- };
23
-
24
- const response = await handleToolCall({
25
- request: createRequest("perform_release", { command: "npm run release" }),
26
- normalizeRequestArgs: (args) => ({ args, error: null }),
27
- workflowState,
28
- exec: execStub,
29
- git: createGitMock(),
30
- utils: testUtils,
31
- });
32
-
33
- const releaseCommand = commands.find((command) => command.startsWith("npm run release"));
34
- assert.ok(releaseCommand, "release command should run");
35
- assert.ok(
36
- commands.some((command) => command.startsWith("git push --follow-tags")),
37
- "release should push tags"
38
- );
39
- });
40
- });
41
-
42
- test("perform_release accepts shorthand patch command", async () => {
43
- await withWorkflowState(async (workflowState) => {
44
- workflowState.state.currentPhase = "release";
45
- workflowState.state.commitAndPushCompleted = true;
46
- workflowState.state.readyCheckCompleted = true;
47
- workflowState.state.lastCommitMessage = "fix: adjust labels";
48
- await workflowState.save();
49
-
50
- const commands = [];
51
- const execStub = async (command) => {
52
- commands.push(command);
53
- if (command === "git status --porcelain") {
54
- return { stdout: "" };
55
- }
56
- return { stdout: "" };
57
- };
58
-
59
- await handleToolCall({
60
- request: createRequest("perform_release", { command: "patch" }),
61
- normalizeRequestArgs: (args) => ({ args, error: null }),
62
- workflowState,
63
- exec: execStub,
64
- git: createGitMock(),
65
- utils: testUtils,
66
- });
67
-
68
- const releaseCommand = commands.find((command) => command.startsWith("npm run release"));
69
- assert.ok(releaseCommand, "release command should run with patch");
70
- });
71
- });
72
-
73
- test("perform_release respects explicit releaseType option", async () => {
74
- await withWorkflowState(async (workflowState) => {
75
- workflowState.state.currentPhase = "release";
76
- workflowState.state.commitAndPushCompleted = true;
77
- workflowState.state.readyCheckCompleted = true;
78
- await workflowState.save();
79
-
80
- const commands = [];
81
- const execStub = async (command) => {
82
- commands.push(command);
83
- if (command === "git status --porcelain") {
84
- return { stdout: "" };
85
- }
86
- return { stdout: "" };
87
- };
88
-
89
- await handleToolCall({
90
- request: createRequest("perform_release", {
91
- command: "npm run release",
92
- releaseType: "minor",
93
- }),
94
- normalizeRequestArgs: (args) => ({ args, error: null }),
95
- workflowState,
96
- exec: execStub,
97
- git: createGitMock(),
98
- utils: testUtils,
99
- });
100
-
101
- const releaseCommand = commands.find((command) => command.startsWith("npm run release"));
102
- assert.ok(releaseCommand, "release command should run");
103
- assert.ok(
104
- releaseCommand.includes("-- --release-as minor"),
105
- "release command should append provided releaseType"
106
- );
107
- });
108
- });
109
-
110
- test("perform_release blocks when new changes detected", async () => {
111
- await withWorkflowState(async (workflowState) => {
112
- workflowState.state.currentPhase = "release";
113
- workflowState.state.commitAndPushCompleted = true;
114
- workflowState.state.readyCheckCompleted = true;
115
- await workflowState.save();
116
-
117
- const commands = [];
118
- const execStub = async (command) => {
119
- commands.push(command);
120
- if (command === "git status --porcelain") {
121
- return { stdout: "?? new-file.js" };
122
- }
123
- return { stdout: "" };
124
- };
125
-
126
- const response = await handleToolCall({
127
- request: createRequest("perform_release", { command: "patch" }),
128
- normalizeRequestArgs: (args) => ({ args, error: null }),
129
- workflowState,
130
- exec: execStub,
131
- git: createGitMock({
132
- hasWorkingChanges: async () => true,
133
- getStatusOutput: async () => "?? new-file.js",
134
- containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
135
- workingTreeSummary: () => ({ hasChanges: true, lines: ["?? new-file.js"] }),
136
- }),
137
- utils: testUtils,
138
- });
139
-
140
- assert.ok(
141
- response.content[0].text.includes("run 'commit_and_push' again"),
142
- "response should instruct to recommit"
143
- );
144
- assert.equal(workflowState.state.currentPhase, "commit");
145
- assert.equal(workflowState.state.commitAndPushCompleted, false);
146
- });
147
- });
148
-
149
- test("perform_release requires commit_and_push completion when tree is clean", async () => {
150
- await withWorkflowState(async (workflowState) => {
151
- workflowState.state.currentPhase = "release";
152
- workflowState.state.commitAndPushCompleted = false;
153
- workflowState.state.readyCheckCompleted = true;
154
- workflowState.state.lastPushBranch = "";
155
- workflowState.state.lastCommitMessage = "";
156
- await workflowState.save();
157
-
158
- const commands = [];
159
- const execStub = async (command) => {
160
- commands.push(command);
161
- return { stdout: "" };
162
- };
163
-
164
- const response = await handleToolCall({
165
- request: createRequest("perform_release", { command: "npm run release" }),
166
- normalizeRequestArgs: (args) => ({ args, error: null }),
167
- workflowState,
168
- exec: execStub,
169
- git: createGitMock({
170
- getLastCommitMessage: async () => "chore: already pushed",
171
- getStatusOutput: async () => "",
172
- containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
173
- }),
174
- utils: testUtils,
175
- });
176
-
177
- assert.ok(
178
- response.content[0].text.includes("commit_and_push"),
179
- "response should mention commit_and_push requirement"
180
- );
181
- });
182
- });
183
-
184
- test("skip_release records justification and advances to completion phase", async () => {
185
- await withWorkflowState(async (workflowState) => {
186
- workflowState.state.currentPhase = "release";
187
- workflowState.state.commitAndPushCompleted = true;
188
- workflowState.state.readyCheckCompleted = true;
189
- workflowState.state.lastCommitMessage = "chore: docs";
190
- workflowState.state.releaseSkipped = false;
191
- workflowState.state.released = false;
192
- workflowState.state.releaseCommand = "";
193
- await workflowState.save();
194
-
195
- const response = await handleToolCall({
196
- request: createRequest("skip_release", { reason: "Python project – no npm release" }),
197
- normalizeRequestArgs: (args) => ({ args, error: null }),
198
- workflowState,
199
- exec: async () => ({ stdout: "" }),
200
- git: createGitMock(),
201
- utils: testUtils,
202
- });
203
-
204
- const text = response.content[0].text;
205
- assert.ok(text.includes("Release step skipped"), "response should mention release was skipped");
206
- assert.equal(workflowState.state.releaseSkipped, true);
207
- assert.equal(workflowState.state.releaseSkippedReason, "Python project – no npm release");
208
- assert.equal(workflowState.state.releaseCommand, "(skipped)");
209
- assert.equal(workflowState.state.currentPhase, "ready_to_complete");
210
- assert.equal(workflowState.state.released, false);
211
- });
212
- });