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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.env.example +21 -1
  2. package/AGENTS.md +25 -0
  3. package/README.md +372 -649
  4. package/factory-client.js +554 -0
  5. package/git-facade.js +35 -0
  6. package/index.js +9 -34
  7. package/package.json +21 -20
  8. package/scripts/bootstrap-unix.sh +55 -0
  9. package/scripts/bootstrap-windows.ps1 +75 -0
  10. package/scripts/install-husky.js +43 -0
  11. package/scripts/setup-stack.js +436 -0
  12. package/scripts/setup-test-dbs.sh +61 -0
  13. package/scripts/test-db-adapters.sh +71 -0
  14. package/scripts/verify-bundle.mjs +92 -0
  15. package/tools/definitions.js +143 -0
  16. package/tools/handler-modules/commit-handlers.js +2 -1
  17. package/tools/handler-modules/factory-handlers.js +276 -0
  18. package/tools/handler-modules/index.js +24 -7
  19. package/tools/handler-modules/shared.js +7 -41
  20. package/tools/handler-modules/status-handlers.js +25 -3
  21. package/tools/handler-modules/workflow-handlers.js +57 -10
  22. package/vite.config.js +6 -0
  23. package/workflow-state.js +176 -2
  24. package/.agent/workflows/dev-workflow-bugfix.md +0 -250
  25. package/.agent/workflows/dev-workflow-feature.md +0 -201
  26. package/.agent/workflows/dev-workflow-full-cycle.md +0 -400
  27. package/.agent/workflows/dev-workflow-refactor.md +0 -238
  28. package/.env.production +0 -16
  29. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -34
  30. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  31. package/.github/pull_request_template.md +0 -22
  32. package/.github/workflows/github-release.yml +0 -131
  33. package/.husky/commit-msg +0 -3
  34. package/.husky/pre-commit +0 -2
  35. package/CHANGELOG.md +0 -1145
  36. package/docs/GCP_DEPLOYMENT.md +0 -298
  37. package/docs/NETLIFY_DEPLOYMENT.md +0 -125
  38. package/docs/PLESK_DEPLOYMENT.md +0 -170
  39. package/docs/PRD.md +0 -86
  40. package/docs/WORKFLOW-BEST-PRACTICES.md +0 -58
  41. package/docs/antigravity/EXAMPLES.md +0 -535
  42. package/docs/antigravity/GETTING-STARTED.md +0 -292
  43. package/docs/antigravity/TROUBLESHOOTING.md +0 -742
  44. package/docs/antigravity/WALKTHROUGH.md +0 -25
  45. package/docs/antigravity/WORKFLOW-TEMPLATES.md +0 -670
  46. package/docs/release-notes/RELEASE_NOTES.md +0 -524
  47. package/docs/web-dashboard.md +0 -175
  48. package/scripts/test-auth-flow.js +0 -75
  49. package/tailwind.config.js +0 -8
  50. package/tests/build.test.js +0 -74
  51. package/tests/db-adapters.test.js +0 -108
  52. package/tests/env-support.test.js +0 -46
  53. package/tests/exec.test.js +0 -35
  54. package/tests/handlers-commit.test.js +0 -174
  55. package/tests/handlers-release.test.js +0 -212
  56. package/tests/handlers-workflow.test.js +0 -68
  57. package/tests/handlers.test.js +0 -1142
  58. package/tests/hello-world.test.js +0 -13
  59. package/tests/mcpRegistry.test.js +0 -65
  60. package/tests/patch-release.test.js +0 -7
  61. package/tests/release-wrapper.test.js +0 -107
  62. package/tests/startup-resilience.test.js +0 -73
  63. package/tests/test-helpers.js +0 -50
  64. package/tests/workflowState.test.js +0 -520
  65. package/web/Dockerfile.api +0 -25
  66. package/web/README.md +0 -133
  67. package/web/__tests__/api/workflow-history.test.js +0 -63
  68. package/web/api-server.js +0 -211
  69. package/web/app/docs/page.tsx +0 -202
  70. package/web/app/favicon.ico +0 -0
  71. package/web/app/globals.css +0 -91
  72. package/web/app/history/page.tsx +0 -220
  73. package/web/app/layout.tsx +0 -39
  74. package/web/app/login/page.tsx +0 -142
  75. package/web/app/page.tsx +0 -13
  76. package/web/components/ApiReference.tsx +0 -20
  77. package/web/components/Features.tsx +0 -61
  78. package/web/components/Footer.tsx +0 -9
  79. package/web/components/Hero.tsx +0 -96
  80. package/web/components/History.tsx +0 -175
  81. package/web/components/Navbar.tsx +0 -208
  82. package/web/components/Terminal.tsx +0 -114
  83. package/web/components/ThemeProvider.tsx +0 -58
  84. package/web/eslint.config.mjs +0 -18
  85. package/web/lib/api-client.ts +0 -123
  86. package/web/lib/auth.js +0 -75
  87. package/web/lib/db.js +0 -131
  88. package/web/lib/docs.js +0 -73
  89. package/web/middleware/auth.js +0 -26
  90. package/web/netlify.toml +0 -8
  91. package/web/next.config.ts +0 -19
  92. package/web/package-lock.json +0 -10911
  93. package/web/package.json +0 -42
  94. package/web/postcss.config.mjs +0 -9
  95. package/web/public/assets/android-chrome-192x192.png +0 -0
  96. package/web/public/assets/android-chrome-512x512.png +0 -0
  97. package/web/public/assets/apple-touch-icon.png +0 -0
  98. package/web/public/assets/favicon-16x16.png +0 -0
  99. package/web/public/assets/favicon-32x32.png +0 -0
  100. package/web/public/assets/favicon.ico +0 -0
  101. package/web/public/assets/favicon.png +0 -0
  102. package/web/public/assets/logo.png +0 -0
  103. package/web/public/assets/og-preview.png +0 -0
  104. package/web/public/assets/site.webmanifest +0 -1
  105. package/web/public/file.svg +0 -1
  106. package/web/public/globe.svg +0 -1
  107. package/web/public/next.svg +0 -1
  108. package/web/public/vercel.svg +0 -1
  109. package/web/public/window.svg +0 -1
  110. package/web/scripts/db-query-history.js +0 -120
  111. package/web/scripts/smoke-test.js +0 -30
  112. package/web/server.js +0 -184
  113. package/web/tailwind.config.ts +0 -20
  114. package/web/tsconfig.json +0 -42
@@ -1,75 +0,0 @@
1
- import fetch from 'node-fetch';
2
-
3
- const API_BASE_URL = 'https://hollywood-andrew-cycle-genre.trycloudflare.com';
4
- const USERNAME = 'programinglive';
5
- const PASSWORD = 'mcpdevserver';
6
-
7
- async function testAuthFlow() {
8
- console.log('🚀 Starting Auth Flow Test...');
9
- console.log(`Target: ${API_BASE_URL}\n`);
10
-
11
- try {
12
- // 1. Login
13
- console.log('1️⃣ Attempting Login...');
14
- const loginRes = await fetch(`${API_BASE_URL}/api/auth/login`, {
15
- method: 'POST',
16
- headers: { 'Content-Type': 'application/json' },
17
- body: JSON.stringify({ username: USERNAME, password: PASSWORD }),
18
- });
19
-
20
- console.log(` Status: ${loginRes.status}`);
21
- if (!loginRes.ok) {
22
- throw new Error(`Login failed: ${loginRes.statusText}`);
23
- }
24
-
25
- const loginData = await loginRes.json();
26
- const token = loginData.token;
27
-
28
- if (token) {
29
- console.log(' ✅ Login Successful! JWT Token received.');
30
- console.log(` Token: ${token.substring(0, 20)}...`);
31
- } else {
32
- console.log(' ⚠️ Login successful but NO TOKEN returned (Legacy Session mode?)');
33
- }
34
-
35
- // 2. Access Protected Route (GET /me)
36
- console.log('\n2️⃣ Verifying Session/Token (GET /api/auth/me)...');
37
- const headers = {};
38
- if (token) {
39
- headers['Authorization'] = `Bearer ${token}`;
40
- console.log(' Using Authorization: Bearer header');
41
- } else {
42
- console.log(' Using Cookies (if any)');
43
- }
44
-
45
- const meRes = await fetch(`${API_BASE_URL}/api/auth/me`, {
46
- headers,
47
- });
48
-
49
- console.log(` Status: ${meRes.status}`);
50
- if (meRes.ok) {
51
- const meData = await meRes.json();
52
- console.log(` ✅ Authorized! User: ${meData.username} (${meData.email})`);
53
- } else {
54
- console.error(' ❌ Authorization Failed!');
55
- console.error(await meRes.text());
56
- }
57
-
58
- // 3. Access History (GET /api/history)
59
- console.log('\n3️⃣ Fetching History...');
60
- const historyRes = await fetch(`${API_BASE_URL}/api/history`, { headers });
61
- console.log(` Status: ${historyRes.status}`);
62
-
63
- if (historyRes.ok) {
64
- const historyData = await historyRes.json();
65
- console.log(` ✅ History fetched! Count: ${historyData.history ? historyData.history.length : 0}`);
66
- } else {
67
- console.log(' ❌ History fetch failed.');
68
- }
69
-
70
- } catch (error) {
71
- console.error('\n❌ Test Failed:', error.message);
72
- }
73
- }
74
-
75
- testAuthFlow();
@@ -1,8 +0,0 @@
1
- /** @type {import('tailwindcss').Config} */
2
- module.exports = {
3
- content: ["./web/public/**/*.{html,js}"],
4
- theme: {
5
- extend: {},
6
- },
7
- plugins: [],
8
- }
@@ -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
- });