@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.
- package/.env.example +21 -1
- package/AGENTS.md +25 -0
- package/README.md +372 -649
- package/factory-client.js +554 -0
- package/git-facade.js +35 -0
- package/index.js +9 -34
- package/package.json +21 -20
- package/scripts/bootstrap-unix.sh +55 -0
- package/scripts/bootstrap-windows.ps1 +75 -0
- package/scripts/install-husky.js +43 -0
- package/scripts/setup-stack.js +436 -0
- package/scripts/setup-test-dbs.sh +61 -0
- package/scripts/test-db-adapters.sh +71 -0
- package/scripts/verify-bundle.mjs +92 -0
- package/tools/definitions.js +143 -0
- package/tools/handler-modules/commit-handlers.js +2 -1
- package/tools/handler-modules/factory-handlers.js +276 -0
- package/tools/handler-modules/index.js +24 -7
- package/tools/handler-modules/shared.js +7 -41
- package/tools/handler-modules/status-handlers.js +25 -3
- package/tools/handler-modules/workflow-handlers.js +57 -10
- package/vite.config.js +6 -0
- package/workflow-state.js +176 -2
- package/.agent/workflows/dev-workflow-bugfix.md +0 -250
- package/.agent/workflows/dev-workflow-feature.md +0 -201
- package/.agent/workflows/dev-workflow-full-cycle.md +0 -400
- package/.agent/workflows/dev-workflow-refactor.md +0 -238
- package/.env.production +0 -16
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -34
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
- package/.github/pull_request_template.md +0 -22
- package/.github/workflows/github-release.yml +0 -131
- package/.husky/commit-msg +0 -3
- package/.husky/pre-commit +0 -2
- package/CHANGELOG.md +0 -1145
- package/docs/GCP_DEPLOYMENT.md +0 -298
- package/docs/NETLIFY_DEPLOYMENT.md +0 -125
- package/docs/PLESK_DEPLOYMENT.md +0 -170
- package/docs/PRD.md +0 -86
- package/docs/WORKFLOW-BEST-PRACTICES.md +0 -58
- package/docs/antigravity/EXAMPLES.md +0 -535
- package/docs/antigravity/GETTING-STARTED.md +0 -292
- package/docs/antigravity/TROUBLESHOOTING.md +0 -742
- package/docs/antigravity/WALKTHROUGH.md +0 -25
- package/docs/antigravity/WORKFLOW-TEMPLATES.md +0 -670
- package/docs/release-notes/RELEASE_NOTES.md +0 -524
- package/docs/web-dashboard.md +0 -175
- package/scripts/test-auth-flow.js +0 -75
- package/tailwind.config.js +0 -8
- package/tests/build.test.js +0 -74
- package/tests/db-adapters.test.js +0 -108
- package/tests/env-support.test.js +0 -46
- package/tests/exec.test.js +0 -35
- package/tests/handlers-commit.test.js +0 -174
- package/tests/handlers-release.test.js +0 -212
- package/tests/handlers-workflow.test.js +0 -68
- package/tests/handlers.test.js +0 -1142
- package/tests/hello-world.test.js +0 -13
- package/tests/mcpRegistry.test.js +0 -65
- package/tests/patch-release.test.js +0 -7
- package/tests/release-wrapper.test.js +0 -107
- package/tests/startup-resilience.test.js +0 -73
- package/tests/test-helpers.js +0 -50
- package/tests/workflowState.test.js +0 -520
- package/web/Dockerfile.api +0 -25
- package/web/README.md +0 -133
- package/web/__tests__/api/workflow-history.test.js +0 -63
- package/web/api-server.js +0 -211
- package/web/app/docs/page.tsx +0 -202
- package/web/app/favicon.ico +0 -0
- package/web/app/globals.css +0 -91
- package/web/app/history/page.tsx +0 -220
- package/web/app/layout.tsx +0 -39
- package/web/app/login/page.tsx +0 -142
- package/web/app/page.tsx +0 -13
- package/web/components/ApiReference.tsx +0 -20
- package/web/components/Features.tsx +0 -61
- package/web/components/Footer.tsx +0 -9
- package/web/components/Hero.tsx +0 -96
- package/web/components/History.tsx +0 -175
- package/web/components/Navbar.tsx +0 -208
- package/web/components/Terminal.tsx +0 -114
- package/web/components/ThemeProvider.tsx +0 -58
- package/web/eslint.config.mjs +0 -18
- package/web/lib/api-client.ts +0 -123
- package/web/lib/auth.js +0 -75
- package/web/lib/db.js +0 -131
- package/web/lib/docs.js +0 -73
- package/web/middleware/auth.js +0 -26
- package/web/netlify.toml +0 -8
- package/web/next.config.ts +0 -19
- package/web/package-lock.json +0 -10911
- package/web/package.json +0 -42
- package/web/postcss.config.mjs +0 -9
- package/web/public/assets/android-chrome-192x192.png +0 -0
- package/web/public/assets/android-chrome-512x512.png +0 -0
- package/web/public/assets/apple-touch-icon.png +0 -0
- package/web/public/assets/favicon-16x16.png +0 -0
- package/web/public/assets/favicon-32x32.png +0 -0
- package/web/public/assets/favicon.ico +0 -0
- package/web/public/assets/favicon.png +0 -0
- package/web/public/assets/logo.png +0 -0
- package/web/public/assets/og-preview.png +0 -0
- package/web/public/assets/site.webmanifest +0 -1
- package/web/public/file.svg +0 -1
- package/web/public/globe.svg +0 -1
- package/web/public/next.svg +0 -1
- package/web/public/vercel.svg +0 -1
- package/web/public/window.svg +0 -1
- package/web/scripts/db-query-history.js +0 -120
- package/web/scripts/smoke-test.js +0 -30
- package/web/server.js +0 -184
- package/web/tailwind.config.ts +0 -20
- 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();
|
package/tailwind.config.js
DELETED
package/tests/build.test.js
DELETED
|
@@ -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
|
-
});
|
package/tests/exec.test.js
DELETED
|
@@ -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
|
-
});
|