fraim-framework 2.0.26 → 2.0.30
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/.github/workflows/deploy-fraim.yml +1 -1
- package/dist/registry/scripts/build-scripts-generator.js +205 -0
- package/dist/registry/scripts/cleanup-branch.js +258 -0
- package/dist/registry/scripts/evaluate-code-quality.js +66 -0
- package/dist/registry/scripts/exec-with-timeout.js +142 -0
- package/dist/registry/scripts/fraim-config.js +61 -0
- package/dist/registry/scripts/generate-engagement-emails.js +630 -0
- package/dist/registry/scripts/generic-issues-api.js +100 -0
- package/dist/registry/scripts/newsletter-helpers.js +731 -0
- package/dist/registry/scripts/openapi-generator.js +664 -0
- package/dist/registry/scripts/performance/profile-server.js +390 -0
- package/dist/registry/scripts/run-thank-you-workflow.js +92 -0
- package/dist/registry/scripts/send-newsletter-simple.js +85 -0
- package/dist/registry/scripts/send-thank-you-emails.js +54 -0
- package/dist/registry/scripts/validate-openapi-limits.js +311 -0
- package/dist/registry/scripts/validate-test-coverage.js +262 -0
- package/dist/registry/scripts/verify-test-coverage.js +66 -0
- package/dist/src/cli/commands/init.js +14 -12
- package/dist/src/cli/commands/sync.js +19 -2
- package/dist/src/cli/fraim.js +24 -22
- package/dist/src/cli/setup/first-run.js +13 -6
- package/dist/src/fraim/config-loader.js +0 -8
- package/dist/src/fraim/db-service.js +26 -15
- package/dist/src/fraim/issues.js +67 -0
- package/dist/src/fraim/setup-wizard.js +1 -69
- package/dist/src/fraim/types.js +0 -11
- package/dist/src/fraim-mcp-server.js +272 -18
- package/dist/src/utils/git-utils.js +1 -1
- package/dist/src/utils/version-utils.js +32 -0
- package/dist/tests/debug-tools.js +79 -0
- package/dist/tests/esm-compat.js +11 -0
- package/dist/tests/test-chalk-esm-issue.js +159 -0
- package/dist/tests/test-chalk-real-world.js +265 -0
- package/dist/tests/test-chalk-regression.js +327 -0
- package/dist/tests/test-chalk-resolution-issue.js +304 -0
- package/dist/tests/test-cli.js +0 -2
- package/dist/tests/test-fraim-install-chalk-issue.js +254 -0
- package/dist/tests/test-fraim-issues.js +59 -0
- package/dist/tests/test-genericization.js +1 -3
- package/dist/tests/test-mcp-connection.js +166 -0
- package/dist/tests/test-mcp-issue-integration.js +144 -0
- package/dist/tests/test-mcp-lifecycle-methods.js +312 -0
- package/dist/tests/test-node-compatibility.js +71 -0
- package/dist/tests/test-npm-install.js +66 -0
- package/dist/tests/test-npm-resolution-diagnostic.js +140 -0
- package/dist/tests/test-session-rehydration.js +145 -0
- package/dist/tests/test-standalone.js +2 -8
- package/dist/tests/test-sync-version-update.js +93 -0
- package/dist/tests/test-telemetry.js +190 -0
- package/package.json +10 -8
- package/registry/agent-guardrails.md +62 -54
- package/registry/rules/agent-success-criteria.md +52 -0
- package/registry/rules/agent-testing-guidelines.md +502 -502
- package/registry/rules/communication.md +121 -121
- package/registry/rules/continuous-learning.md +54 -54
- package/registry/rules/ephemeral-execution.md +10 -5
- package/registry/rules/hitl-ppe-record-analysis.md +302 -302
- package/registry/rules/local-development.md +251 -251
- package/registry/rules/software-development-lifecycle.md +104 -104
- package/registry/rules/successful-debugging-patterns.md +482 -478
- package/registry/rules/telemetry.md +67 -0
- package/registry/scripts/build-scripts-generator.ts +216 -215
- package/registry/scripts/cleanup-branch.ts +303 -284
- package/registry/scripts/code-quality-check.sh +559 -559
- package/registry/scripts/detect-tautological-tests.sh +38 -38
- package/registry/scripts/evaluate-code-quality.ts +1 -1
- package/registry/scripts/generate-engagement-emails.ts +744 -744
- package/registry/scripts/generic-issues-api.ts +110 -150
- package/registry/scripts/newsletter-helpers.ts +874 -874
- package/registry/scripts/openapi-generator.ts +695 -693
- package/registry/scripts/performance/profile-server.ts +5 -3
- package/registry/scripts/prep-issue.sh +468 -455
- package/registry/scripts/validate-openapi-limits.ts +366 -365
- package/registry/scripts/validate-test-coverage.ts +280 -280
- package/registry/scripts/verify-pr-comments.sh +70 -70
- package/registry/scripts/verify-test-coverage.ts +1 -1
- package/registry/templates/bootstrap/ARCHITECTURE-TEMPLATE.md +53 -53
- package/registry/templates/evidence/Implementation-BugEvidence.md +85 -85
- package/registry/templates/evidence/Implementation-FeatureEvidence.md +120 -120
- package/registry/templates/marketing/HBR-ARTICLE-TEMPLATE.md +66 -0
- package/registry/workflows/bootstrap/create-architecture.md +2 -2
- package/registry/workflows/bootstrap/evaluate-code-quality.md +3 -3
- package/registry/workflows/bootstrap/verify-test-coverage.md +2 -2
- package/registry/workflows/customer-development/insight-analysis.md +156 -156
- package/registry/workflows/customer-development/interview-preparation.md +421 -421
- package/registry/workflows/customer-development/strategic-brainstorming.md +146 -146
- package/registry/workflows/customer-development/thank-customers.md +193 -191
- package/registry/workflows/customer-development/weekly-newsletter.md +362 -352
- package/registry/workflows/improve-fraim/contribute.md +32 -0
- package/registry/workflows/improve-fraim/file-issue.md +32 -0
- package/registry/workflows/marketing/hbr-article.md +73 -0
- package/registry/workflows/performance/analyze-performance.md +63 -59
- package/registry/workflows/product-building/design.md +3 -2
- package/registry/workflows/product-building/implement.md +4 -3
- package/registry/workflows/product-building/prep-issue.md +28 -17
- package/registry/workflows/product-building/resolve.md +3 -2
- package/registry/workflows/product-building/retrospect.md +3 -2
- package/registry/workflows/product-building/spec.md +5 -4
- package/registry/workflows/product-building/test.md +3 -2
- package/registry/workflows/quality-assurance/iterative-improvement-cycle.md +562 -562
- package/registry/workflows/replicate/website-discovery-analysis.md +3 -3
- package/registry/workflows/reviewer/review-implementation-vs-design-spec.md +632 -632
- package/registry/workflows/reviewer/review-implementation-vs-feature-spec.md +669 -669
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const test_utils_1 = require("./test-utils");
|
|
9
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
10
|
+
const tree_kill_1 = __importDefault(require("tree-kill"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const db_service_js_1 = require("../src/fraim/db-service.js");
|
|
13
|
+
async function testMcpConnection() {
|
|
14
|
+
console.log(' 🚀 Testing MCP Connection Reliability...');
|
|
15
|
+
let fraimProcess;
|
|
16
|
+
let dbService;
|
|
17
|
+
const PORT = Math.floor(Math.random() * 1000) + 12000;
|
|
18
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
19
|
+
const TEST_API_KEY = 'test-fraim-key-mcp-conn';
|
|
20
|
+
const TEST_ADMIN_KEY = 'test-admin-key-mcp-conn';
|
|
21
|
+
try {
|
|
22
|
+
// 0. Setup DB and Key
|
|
23
|
+
dbService = new db_service_js_1.FraimDbService();
|
|
24
|
+
await dbService.connect();
|
|
25
|
+
const db = dbService.db;
|
|
26
|
+
await db.collection('fraim_api_keys').deleteOne({ key: TEST_API_KEY });
|
|
27
|
+
await db.collection('fraim_api_keys').insertOne({
|
|
28
|
+
key: TEST_API_KEY,
|
|
29
|
+
userId: 'test-user-mcp',
|
|
30
|
+
orgId: 'test-org',
|
|
31
|
+
isActive: true,
|
|
32
|
+
createdAt: new Date()
|
|
33
|
+
});
|
|
34
|
+
// 1. Start Server
|
|
35
|
+
console.log(` Starting server on port ${PORT}...`);
|
|
36
|
+
const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
37
|
+
const serverScript = path_1.default.resolve(__dirname, '../src/fraim-mcp-server.ts');
|
|
38
|
+
fraimProcess = (0, node_child_process_1.spawn)(npxCommand, ['tsx', `"${serverScript}"`], {
|
|
39
|
+
env: {
|
|
40
|
+
...process.env,
|
|
41
|
+
FRAIM_MCP_PORT: PORT.toString(),
|
|
42
|
+
FRAIM_ADMIN_KEY: TEST_ADMIN_KEY,
|
|
43
|
+
FRAIM_SKIP_INDEX_ON_START: 'true'
|
|
44
|
+
},
|
|
45
|
+
stdio: 'pipe',
|
|
46
|
+
shell: true
|
|
47
|
+
});
|
|
48
|
+
if (fraimProcess.stdout)
|
|
49
|
+
fraimProcess.stdout.pipe(process.stdout);
|
|
50
|
+
if (fraimProcess.stderr)
|
|
51
|
+
fraimProcess.stderr.pipe(process.stderr);
|
|
52
|
+
// Wait for start
|
|
53
|
+
let started = false;
|
|
54
|
+
for (let i = 0; i < 15; i++) {
|
|
55
|
+
try {
|
|
56
|
+
await axios_1.default.get(`${BASE_URL}/health`, { timeout: 500 });
|
|
57
|
+
started = true;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!started)
|
|
65
|
+
throw new Error('Server failed to start');
|
|
66
|
+
console.log(' Server started!');
|
|
67
|
+
const authHeaders = { 'x-api-key': TEST_API_KEY };
|
|
68
|
+
// 2. Test notifications/initialized (Fix 1 verification)
|
|
69
|
+
console.log(' Testing notifications/initialized...');
|
|
70
|
+
try {
|
|
71
|
+
const res = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
72
|
+
jsonrpc: '2.0',
|
|
73
|
+
method: 'notifications/initialized',
|
|
74
|
+
params: {}
|
|
75
|
+
}, { headers: authHeaders });
|
|
76
|
+
// Should be 200 OK now, not 500
|
|
77
|
+
node_assert_1.default.strictEqual(res.status, 200, 'Should return 200 OK for notifications/initialized');
|
|
78
|
+
node_assert_1.default.strictEqual(res.data.result, true, 'Should return result: true');
|
|
79
|
+
console.log(' ✅ notifications/initialized handled correctly.');
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.error(' ❌ Notification failed:', error.message);
|
|
83
|
+
if (error.response) {
|
|
84
|
+
console.error(' Response:', JSON.stringify(error.response.data));
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
// 3. Test DELETE request crash (Fix 2 verification)
|
|
89
|
+
console.log(' Testing DELETE request (simulate disconnect crash)...');
|
|
90
|
+
try {
|
|
91
|
+
// DELETE requests often have no body.
|
|
92
|
+
// The bug was reading req.body.id => crash if req.body is undefined/empty
|
|
93
|
+
await axios_1.default.delete(`${BASE_URL}/mcp`, {
|
|
94
|
+
headers: authHeaders,
|
|
95
|
+
validateStatus: (status) => true // Accept any status
|
|
96
|
+
});
|
|
97
|
+
console.log(' ✅ DELETE request completed without network error (Server survived).');
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
console.error(' ❌ Server likely crashed on DELETE:', error.message);
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
// Verify Server is STILL ALIVE after DELETE
|
|
104
|
+
const healthRes = await axios_1.default.get(`${BASE_URL}/health`);
|
|
105
|
+
node_assert_1.default.strictEqual(healthRes.status, 200, 'Server should be alive after DELETE request');
|
|
106
|
+
console.log(' ✅ Server verified alive after malformed request.');
|
|
107
|
+
// 4. Test SSE Endpoint Handshake
|
|
108
|
+
console.log(' Testing SSE Handshake...');
|
|
109
|
+
const sseRes = await axios_1.default.get(`${BASE_URL}/mcp`, {
|
|
110
|
+
headers: {
|
|
111
|
+
'Accept': 'text/event-stream',
|
|
112
|
+
...authHeaders
|
|
113
|
+
},
|
|
114
|
+
responseType: 'stream'
|
|
115
|
+
});
|
|
116
|
+
const stream = sseRes.data;
|
|
117
|
+
let dataReceived = '';
|
|
118
|
+
await new Promise((resolve, reject) => {
|
|
119
|
+
stream.on('data', (chunk) => {
|
|
120
|
+
dataReceived += chunk.toString();
|
|
121
|
+
if (dataReceived.includes('event: endpoint')) {
|
|
122
|
+
stream.destroy();
|
|
123
|
+
resolve();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
stream.on('error', reject);
|
|
127
|
+
setTimeout(() => { stream.destroy(); resolve(); }, 2000);
|
|
128
|
+
});
|
|
129
|
+
node_assert_1.default.ok(dataReceived.includes('event: endpoint'), 'Should receive endpoint event');
|
|
130
|
+
// Verify absolute URL format
|
|
131
|
+
const endpointLine = dataReceived.split('\n').find(l => l.startsWith('data: http'));
|
|
132
|
+
node_assert_1.default.ok(endpointLine, 'Should find data line with absolute URL starting with http');
|
|
133
|
+
node_assert_1.default.ok(endpointLine?.includes(`localhost:${PORT}/mcp`), 'Absolute URL should point to correct port/path');
|
|
134
|
+
console.log(' ✅ SSE Handshake verified with absolute URL.');
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
console.error(' ❌ Test failed:', error.message);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
if (dbService) {
|
|
143
|
+
const db = dbService.db;
|
|
144
|
+
await db.collection('fraim_api_keys').deleteOne({ key: TEST_API_KEY }).catch(() => { });
|
|
145
|
+
await dbService.close();
|
|
146
|
+
}
|
|
147
|
+
if (fraimProcess && fraimProcess.pid) {
|
|
148
|
+
console.log(' Cleanup: Killing server...');
|
|
149
|
+
await new Promise(resolve => (0, tree_kill_1.default)(fraimProcess.pid, 'SIGKILL', () => resolve()));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const testCases = [
|
|
154
|
+
{
|
|
155
|
+
name: 'MCP Connection & Stability',
|
|
156
|
+
description: 'Verifies connection protocol, notification handling, and crash resilience',
|
|
157
|
+
testFunction: testMcpConnection,
|
|
158
|
+
tags: ['mcp', 'connection']
|
|
159
|
+
}
|
|
160
|
+
];
|
|
161
|
+
(0, test_utils_1.runTests)(testCases, async (t) => t.testFunction(), 'MCP Connection Reliability')
|
|
162
|
+
.then(() => process.exit(0))
|
|
163
|
+
.catch((err) => {
|
|
164
|
+
console.error('Test runner failed:', err);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const db_service_js_1 = require("../src/fraim/db-service.js");
|
|
9
|
+
const test_utils_1 = require("./test-utils");
|
|
10
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
11
|
+
const tree_kill_1 = __importDefault(require("tree-kill"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
async function testFileIssueToolAPI() {
|
|
14
|
+
console.log(' 🚀 Testing File Issue Tool via MCP API...');
|
|
15
|
+
let fraimProcess;
|
|
16
|
+
let dbService;
|
|
17
|
+
const PORT = 10002; // Different port
|
|
18
|
+
const TEST_API_KEY = 'test-fraim-key-issues';
|
|
19
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
20
|
+
try {
|
|
21
|
+
// 1. Seed API key
|
|
22
|
+
dbService = new db_service_js_1.FraimDbService();
|
|
23
|
+
await dbService.connect();
|
|
24
|
+
const db = dbService.db;
|
|
25
|
+
await db.collection('fraim_api_keys').updateOne({ key: TEST_API_KEY }, {
|
|
26
|
+
$set: {
|
|
27
|
+
userId: 'test-user@ashley.ai',
|
|
28
|
+
orgId: 'test-org',
|
|
29
|
+
isActive: true,
|
|
30
|
+
createdAt: new Date()
|
|
31
|
+
}
|
|
32
|
+
}, { upsert: true });
|
|
33
|
+
// 2. Start server
|
|
34
|
+
console.log(` Starting server on port ${PORT}...`);
|
|
35
|
+
const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
36
|
+
const serverScript = path_1.default.resolve(__dirname, '../src/fraim-mcp-server.ts');
|
|
37
|
+
fraimProcess = (0, node_child_process_1.spawn)(npxCommand, ['tsx', `"${serverScript}"`], {
|
|
38
|
+
env: {
|
|
39
|
+
...process.env,
|
|
40
|
+
FRAIM_MCP_PORT: PORT.toString(),
|
|
41
|
+
FRAIM_SKIP_INDEX_ON_START: 'true'
|
|
42
|
+
},
|
|
43
|
+
stdio: 'pipe', // Using pipe to keep output cleaner, assuming debug isn't needed unless failure
|
|
44
|
+
shell: true
|
|
45
|
+
});
|
|
46
|
+
// 3. Wait for start
|
|
47
|
+
console.log(' Waiting for server to start...');
|
|
48
|
+
let started = false;
|
|
49
|
+
for (let i = 0; i < 20; i++) {
|
|
50
|
+
try {
|
|
51
|
+
await axios_1.default.get(`${BASE_URL}/health`, { timeout: 1000 });
|
|
52
|
+
started = true;
|
|
53
|
+
console.log(' Server started!');
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!started) {
|
|
61
|
+
console.error(' ❌ Server failed to start');
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
// 4. Perform Handshake (Required for Session)
|
|
65
|
+
console.log(' Performing Handshake (fraim_connect)...');
|
|
66
|
+
await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
67
|
+
jsonrpc: '2.0',
|
|
68
|
+
id: 0,
|
|
69
|
+
method: 'tools/call',
|
|
70
|
+
params: {
|
|
71
|
+
name: 'fraim_connect',
|
|
72
|
+
arguments: {
|
|
73
|
+
machine: { hostname: 'test-host', platform: process.platform },
|
|
74
|
+
repo: { url: 'https://github.com/test/repo' }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}, {
|
|
78
|
+
headers: { 'x-api-key': TEST_API_KEY }
|
|
79
|
+
});
|
|
80
|
+
// 5. Test file_issue tool call (Dry Run)
|
|
81
|
+
console.log(' Testing file_issue tool (dry run)...');
|
|
82
|
+
const response = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
83
|
+
jsonrpc: '2.0',
|
|
84
|
+
id: 1,
|
|
85
|
+
method: 'tools/call',
|
|
86
|
+
params: {
|
|
87
|
+
name: 'file_issue',
|
|
88
|
+
arguments: {
|
|
89
|
+
title: 'Integration Test Issue',
|
|
90
|
+
body: 'This is an integration test',
|
|
91
|
+
dryRun: true
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, {
|
|
95
|
+
headers: { 'x-api-key': TEST_API_KEY },
|
|
96
|
+
timeout: 10000
|
|
97
|
+
});
|
|
98
|
+
node_assert_1.default.strictEqual(response.status, 200);
|
|
99
|
+
node_assert_1.default.ok(response.data.result, 'Should have result');
|
|
100
|
+
node_assert_1.default.ok(response.data.result.content, 'Should have content');
|
|
101
|
+
const contentText = response.data.result.content[0].text;
|
|
102
|
+
const resultJson = JSON.parse(contentText);
|
|
103
|
+
node_assert_1.default.strictEqual(resultJson.success, true);
|
|
104
|
+
node_assert_1.default.strictEqual(resultJson.dryRun, true);
|
|
105
|
+
node_assert_1.default.ok(resultJson.message.includes('[DRY RUN]'));
|
|
106
|
+
node_assert_1.default.ok(resultJson.message.includes('Integration Test Issue'));
|
|
107
|
+
console.log(' ✅ file_issue tool verified successfully');
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.error(' ❌ Test failed:', error);
|
|
112
|
+
if (axios_1.default.isAxiosError(error) && error.response) {
|
|
113
|
+
console.error(' Response data:', error.response.data);
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
console.log(' Cleaning up...');
|
|
119
|
+
if (dbService) {
|
|
120
|
+
const db = dbService.db;
|
|
121
|
+
if (db) {
|
|
122
|
+
await db.collection('fraim_api_keys').deleteOne({ key: TEST_API_KEY }).catch(() => { });
|
|
123
|
+
}
|
|
124
|
+
await dbService.close().catch(() => { });
|
|
125
|
+
}
|
|
126
|
+
if (fraimProcess && fraimProcess.pid) {
|
|
127
|
+
const pid = fraimProcess.pid;
|
|
128
|
+
await new Promise((resolve) => (0, tree_kill_1.default)(pid, 'SIGKILL', () => resolve()));
|
|
129
|
+
console.log(` Terminated server process ${pid}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function runTest(testCase) {
|
|
134
|
+
return await testCase.testFunction();
|
|
135
|
+
}
|
|
136
|
+
const testCases = [
|
|
137
|
+
{
|
|
138
|
+
name: 'File Issue Integration Test',
|
|
139
|
+
description: 'Tests file_issue tool via real MCP server request',
|
|
140
|
+
testFunction: testFileIssueToolAPI,
|
|
141
|
+
tags: ['integration', 'issues']
|
|
142
|
+
}
|
|
143
|
+
];
|
|
144
|
+
(0, test_utils_1.runTests)(testCases, runTest, 'Fraim Issue API Integration');
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const test_utils_1 = require("./test-utils");
|
|
9
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
10
|
+
const tree_kill_1 = __importDefault(require("tree-kill"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const db_service_js_1 = require("../src/fraim/db-service.js");
|
|
13
|
+
/**
|
|
14
|
+
* Test that resources/list and prompts/list work without session
|
|
15
|
+
* This verifies the fix for the MCP telemetry blocking issue
|
|
16
|
+
*/
|
|
17
|
+
async function testMcpLifecycleMethods() {
|
|
18
|
+
console.log(' 🚀 Testing MCP Lifecycle Methods (resources/list, prompts/list)...');
|
|
19
|
+
let fraimProcess;
|
|
20
|
+
let dbService;
|
|
21
|
+
const PORT = Math.floor(Math.random() * 1000) + 13000;
|
|
22
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
23
|
+
const TEST_API_KEY = 'test-fraim-key-lifecycle';
|
|
24
|
+
const TEST_ADMIN_KEY = 'test-admin-key-lifecycle';
|
|
25
|
+
try {
|
|
26
|
+
// 0. Setup DB and Key
|
|
27
|
+
dbService = new db_service_js_1.FraimDbService();
|
|
28
|
+
await dbService.connect();
|
|
29
|
+
const db = dbService.db;
|
|
30
|
+
await db.collection('fraim_api_keys').deleteOne({ key: TEST_API_KEY });
|
|
31
|
+
await db.collection('fraim_api_keys').insertOne({
|
|
32
|
+
key: TEST_API_KEY,
|
|
33
|
+
userId: 'test-user-lifecycle',
|
|
34
|
+
orgId: 'test-org',
|
|
35
|
+
isActive: true,
|
|
36
|
+
createdAt: new Date()
|
|
37
|
+
});
|
|
38
|
+
// 1. Start Server
|
|
39
|
+
console.log(` Starting server on port ${PORT}...`);
|
|
40
|
+
const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
41
|
+
const serverScript = path_1.default.resolve(__dirname, '../src/fraim-mcp-server.ts');
|
|
42
|
+
fraimProcess = (0, node_child_process_1.spawn)(npxCommand, ['tsx', `"${serverScript}"`], {
|
|
43
|
+
env: {
|
|
44
|
+
...process.env,
|
|
45
|
+
FRAIM_MCP_PORT: PORT.toString(),
|
|
46
|
+
FRAIM_ADMIN_KEY: TEST_ADMIN_KEY,
|
|
47
|
+
FRAIM_SKIP_INDEX_ON_START: 'true'
|
|
48
|
+
},
|
|
49
|
+
stdio: 'pipe',
|
|
50
|
+
shell: true
|
|
51
|
+
});
|
|
52
|
+
if (fraimProcess.stdout)
|
|
53
|
+
fraimProcess.stdout.pipe(process.stdout);
|
|
54
|
+
if (fraimProcess.stderr)
|
|
55
|
+
fraimProcess.stderr.pipe(process.stderr);
|
|
56
|
+
// Wait for start
|
|
57
|
+
let started = false;
|
|
58
|
+
for (let i = 0; i < 15; i++) {
|
|
59
|
+
try {
|
|
60
|
+
await axios_1.default.get(`${BASE_URL}/health`, { timeout: 500 });
|
|
61
|
+
started = true;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!started)
|
|
69
|
+
throw new Error('Server failed to start');
|
|
70
|
+
console.log(' Server started!');
|
|
71
|
+
const authHeaders = { 'x-api-key': TEST_API_KEY };
|
|
72
|
+
// 2. Test initialize (should work without session)
|
|
73
|
+
console.log(' Testing initialize...');
|
|
74
|
+
const initRes = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
75
|
+
jsonrpc: '2.0',
|
|
76
|
+
method: 'initialize',
|
|
77
|
+
params: {
|
|
78
|
+
protocolVersion: '2024-11-05',
|
|
79
|
+
capabilities: {},
|
|
80
|
+
clientInfo: { name: 'test-client', version: '1.0.0' }
|
|
81
|
+
},
|
|
82
|
+
id: 1
|
|
83
|
+
}, { headers: authHeaders });
|
|
84
|
+
node_assert_1.default.strictEqual(initRes.status, 200, 'initialize should return 200');
|
|
85
|
+
node_assert_1.default.ok(initRes.data.result, 'initialize should return result');
|
|
86
|
+
node_assert_1.default.ok(initRes.data.result.serverInfo, 'initialize should return serverInfo');
|
|
87
|
+
console.log(' ✅ initialize works without session');
|
|
88
|
+
// 3. Test tools/list (should work without session)
|
|
89
|
+
console.log(' Testing tools/list...');
|
|
90
|
+
const toolsRes = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
91
|
+
jsonrpc: '2.0',
|
|
92
|
+
method: 'tools/list',
|
|
93
|
+
params: {},
|
|
94
|
+
id: 2
|
|
95
|
+
}, { headers: authHeaders });
|
|
96
|
+
node_assert_1.default.strictEqual(toolsRes.status, 200, 'tools/list should return 200');
|
|
97
|
+
node_assert_1.default.ok(toolsRes.data.result, 'tools/list should return result');
|
|
98
|
+
node_assert_1.default.ok(Array.isArray(toolsRes.data.result.tools), 'tools/list should return tools array');
|
|
99
|
+
node_assert_1.default.ok(toolsRes.data.result.tools.length > 0, 'tools/list should return at least one tool');
|
|
100
|
+
console.log(` ✅ tools/list works without session (${toolsRes.data.result.tools.length} tools)`);
|
|
101
|
+
// 4. Test resources/list (THE FIX - should work without session)
|
|
102
|
+
console.log(' Testing resources/list (THE FIX)...');
|
|
103
|
+
const resourcesRes = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
104
|
+
jsonrpc: '2.0',
|
|
105
|
+
method: 'resources/list',
|
|
106
|
+
params: {},
|
|
107
|
+
id: 3
|
|
108
|
+
}, { headers: authHeaders });
|
|
109
|
+
node_assert_1.default.strictEqual(resourcesRes.status, 200, 'resources/list should return 200');
|
|
110
|
+
node_assert_1.default.ok(resourcesRes.data.result, 'resources/list should return result');
|
|
111
|
+
node_assert_1.default.ok(Array.isArray(resourcesRes.data.result.resources), 'resources/list should return resources array');
|
|
112
|
+
node_assert_1.default.strictEqual(resourcesRes.data.result.resources.length, 0, 'resources/list should return empty array');
|
|
113
|
+
console.log(' ✅ resources/list works without session (returns empty array)');
|
|
114
|
+
// 5. Test prompts/list (THE FIX - should work without session)
|
|
115
|
+
console.log(' Testing prompts/list (THE FIX)...');
|
|
116
|
+
const promptsRes = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
117
|
+
jsonrpc: '2.0',
|
|
118
|
+
method: 'prompts/list',
|
|
119
|
+
params: {},
|
|
120
|
+
id: 4
|
|
121
|
+
}, { headers: authHeaders });
|
|
122
|
+
node_assert_1.default.strictEqual(promptsRes.status, 200, 'prompts/list should return 200');
|
|
123
|
+
node_assert_1.default.ok(promptsRes.data.result, 'prompts/list should return result');
|
|
124
|
+
node_assert_1.default.ok(Array.isArray(promptsRes.data.result.prompts), 'prompts/list should return prompts array');
|
|
125
|
+
node_assert_1.default.strictEqual(promptsRes.data.result.prompts.length, 0, 'prompts/list should return empty array');
|
|
126
|
+
console.log(' ✅ prompts/list works without session (returns empty array)');
|
|
127
|
+
// 6. Test that non-lifecycle methods still require session
|
|
128
|
+
console.log(' Testing that non-lifecycle methods require session...');
|
|
129
|
+
try {
|
|
130
|
+
await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
131
|
+
jsonrpc: '2.0',
|
|
132
|
+
method: 'tools/call',
|
|
133
|
+
params: {
|
|
134
|
+
name: 'get_fraim_workflow',
|
|
135
|
+
arguments: { workflow: 'spec' }
|
|
136
|
+
},
|
|
137
|
+
id: 5
|
|
138
|
+
}, { headers: authHeaders, timeout: 2000 });
|
|
139
|
+
console.error(' ❌ Should have failed with 400 (Session Not Started)');
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error.response) {
|
|
144
|
+
node_assert_1.default.strictEqual(error.response.status, 400, 'Should return 400 for tool call without session');
|
|
145
|
+
node_assert_1.default.match(error.response.data?.error?.message, /Session Not Started/, 'Should mention Session Not Started');
|
|
146
|
+
console.log(' ✅ Non-lifecycle methods still require session');
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 7. Test complete MCP initialization sequence
|
|
153
|
+
console.log(' Testing complete MCP initialization sequence...');
|
|
154
|
+
const sequence = [
|
|
155
|
+
{ method: 'initialize', id: 10 },
|
|
156
|
+
{ method: 'notifications/initialized', id: 11 },
|
|
157
|
+
{ method: 'tools/list', id: 12 },
|
|
158
|
+
{ method: 'resources/list', id: 13 },
|
|
159
|
+
{ method: 'prompts/list', id: 14 }
|
|
160
|
+
];
|
|
161
|
+
for (const step of sequence) {
|
|
162
|
+
const res = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
163
|
+
jsonrpc: '2.0',
|
|
164
|
+
method: step.method,
|
|
165
|
+
params: {},
|
|
166
|
+
id: step.id
|
|
167
|
+
}, { headers: authHeaders });
|
|
168
|
+
node_assert_1.default.strictEqual(res.status, 200, `${step.method} should return 200 in sequence`);
|
|
169
|
+
console.log(` ✅ ${step.method} succeeded in sequence`);
|
|
170
|
+
}
|
|
171
|
+
console.log(' ✅ Complete MCP initialization sequence works without session');
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
console.error(' ❌ Test failed:', error.message);
|
|
176
|
+
if (error.response?.data) {
|
|
177
|
+
console.error(' 🔍 Server Error Detail:', JSON.stringify(error.response.data, null, 2));
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
if (dbService) {
|
|
183
|
+
const db = dbService.db;
|
|
184
|
+
await db.collection('fraim_api_keys').deleteOne({ key: TEST_API_KEY }).catch(() => { });
|
|
185
|
+
await dbService.close();
|
|
186
|
+
}
|
|
187
|
+
if (fraimProcess && fraimProcess.pid) {
|
|
188
|
+
console.log(' Cleanup: Killing server...');
|
|
189
|
+
await new Promise(resolve => (0, tree_kill_1.default)(fraimProcess.pid, 'SIGKILL', () => resolve()));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Test that bootstrap tools work without session
|
|
195
|
+
*/
|
|
196
|
+
async function testBootstrapTools() {
|
|
197
|
+
console.log(' 🚀 Testing Bootstrap Tools (without session)...');
|
|
198
|
+
let fraimProcess;
|
|
199
|
+
let dbService;
|
|
200
|
+
const PORT = Math.floor(Math.random() * 1000) + 14000;
|
|
201
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
202
|
+
const TEST_API_KEY = 'test-fraim-key-bootstrap';
|
|
203
|
+
const TEST_ADMIN_KEY = 'test-admin-key-bootstrap';
|
|
204
|
+
try {
|
|
205
|
+
// 0. Setup DB and Key
|
|
206
|
+
dbService = new db_service_js_1.FraimDbService();
|
|
207
|
+
await dbService.connect();
|
|
208
|
+
const db = dbService.db;
|
|
209
|
+
await db.collection('fraim_api_keys').deleteOne({ key: TEST_API_KEY });
|
|
210
|
+
await db.collection('fraim_api_keys').insertOne({
|
|
211
|
+
key: TEST_API_KEY,
|
|
212
|
+
userId: 'test-user-bootstrap',
|
|
213
|
+
orgId: 'test-org',
|
|
214
|
+
isActive: true,
|
|
215
|
+
createdAt: new Date()
|
|
216
|
+
});
|
|
217
|
+
// 1. Start Server
|
|
218
|
+
console.log(` Starting server on port ${PORT}...`);
|
|
219
|
+
const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
220
|
+
const serverScript = path_1.default.resolve(__dirname, '../src/fraim-mcp-server.ts');
|
|
221
|
+
fraimProcess = (0, node_child_process_1.spawn)(npxCommand, ['tsx', `"${serverScript}"`], {
|
|
222
|
+
env: {
|
|
223
|
+
...process.env,
|
|
224
|
+
FRAIM_MCP_PORT: PORT.toString(),
|
|
225
|
+
FRAIM_ADMIN_KEY: TEST_ADMIN_KEY,
|
|
226
|
+
FRAIM_SKIP_INDEX_ON_START: 'true'
|
|
227
|
+
},
|
|
228
|
+
stdio: 'pipe',
|
|
229
|
+
shell: true
|
|
230
|
+
});
|
|
231
|
+
if (fraimProcess.stdout)
|
|
232
|
+
fraimProcess.stdout.pipe(process.stdout);
|
|
233
|
+
if (fraimProcess.stderr)
|
|
234
|
+
fraimProcess.stderr.pipe(process.stderr);
|
|
235
|
+
// Wait for start
|
|
236
|
+
let started = false;
|
|
237
|
+
for (let i = 0; i < 15; i++) {
|
|
238
|
+
try {
|
|
239
|
+
await axios_1.default.get(`${BASE_URL}/health`, { timeout: 500 });
|
|
240
|
+
started = true;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!started)
|
|
248
|
+
throw new Error('Server failed to start');
|
|
249
|
+
console.log(' Server started!');
|
|
250
|
+
const authHeaders = { 'x-api-key': TEST_API_KEY };
|
|
251
|
+
// Test bootstrap tools that should work without session
|
|
252
|
+
const bootstrapTools = [
|
|
253
|
+
{ name: 'get_fraim_init', args: {} },
|
|
254
|
+
{ name: 'list_fraim_workflows', args: {} },
|
|
255
|
+
{ name: 'fraim_get_local_config', args: {} }
|
|
256
|
+
];
|
|
257
|
+
for (const tool of bootstrapTools) {
|
|
258
|
+
console.log(` Testing ${tool.name}...`);
|
|
259
|
+
const res = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
260
|
+
jsonrpc: '2.0',
|
|
261
|
+
method: 'tools/call',
|
|
262
|
+
params: {
|
|
263
|
+
name: tool.name,
|
|
264
|
+
arguments: tool.args
|
|
265
|
+
},
|
|
266
|
+
id: 1
|
|
267
|
+
}, { headers: authHeaders });
|
|
268
|
+
node_assert_1.default.strictEqual(res.status, 200, `${tool.name} should return 200 without session`);
|
|
269
|
+
node_assert_1.default.ok(res.data.result, `${tool.name} should return result`);
|
|
270
|
+
console.log(` ✅ ${tool.name} works without session`);
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
console.error(' ❌ Test failed:', error.message);
|
|
276
|
+
if (error.response?.data) {
|
|
277
|
+
console.error(' 🔍 Server Error Detail:', JSON.stringify(error.response.data, null, 2));
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
if (dbService) {
|
|
283
|
+
const db = dbService.db;
|
|
284
|
+
await db.collection('fraim_api_keys').deleteOne({ key: TEST_API_KEY }).catch(() => { });
|
|
285
|
+
await dbService.close();
|
|
286
|
+
}
|
|
287
|
+
if (fraimProcess && fraimProcess.pid) {
|
|
288
|
+
console.log(' Cleanup: Killing server...');
|
|
289
|
+
await new Promise(resolve => (0, tree_kill_1.default)(fraimProcess.pid, 'SIGKILL', () => resolve()));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const testCases = [
|
|
294
|
+
{
|
|
295
|
+
name: 'MCP Lifecycle Methods',
|
|
296
|
+
description: 'Tests that resources/list and prompts/list work without session (fix for telemetry blocking)',
|
|
297
|
+
testFunction: testMcpLifecycleMethods,
|
|
298
|
+
tags: ['mcp', 'lifecycle', 'telemetry']
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: 'Bootstrap Tools',
|
|
302
|
+
description: 'Tests that bootstrap tools work without session',
|
|
303
|
+
testFunction: testBootstrapTools,
|
|
304
|
+
tags: ['mcp', 'bootstrap', 'telemetry']
|
|
305
|
+
}
|
|
306
|
+
];
|
|
307
|
+
(0, test_utils_1.runTests)(testCases, async (t) => t.testFunction(), 'MCP Lifecycle Methods & Bootstrap Tools')
|
|
308
|
+
.then(() => process.exit(0))
|
|
309
|
+
.catch((err) => {
|
|
310
|
+
console.error('Test runner failed:', err);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
});
|