fraim-framework 2.0.27 → 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 -14
- package/dist/src/cli/commands/sync.js +4 -4
- package/dist/src/cli/fraim.js +24 -22
- package/dist/src/cli/setup/first-run.js +13 -6
- package/dist/src/fraim/db-service.js +26 -15
- package/dist/src/fraim/issues.js +67 -0
- package/dist/src/fraim-mcp-server.js +272 -18
- package/dist/src/utils/git-utils.js +1 -1
- 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-fraim-install-chalk-issue.js +254 -0
- package/dist/tests/test-fraim-issues.js +59 -0
- package/dist/tests/test-genericization.js +1 -1
- 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-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,71 @@
|
|
|
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 test_utils_1 = require("./test-utils");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
11
|
+
async function testInitOnNodeVersion(version) {
|
|
12
|
+
console.log(` 🧪 Testing Node v${version}...`);
|
|
13
|
+
// Create a temp directory for each version test
|
|
14
|
+
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `fraim-node-${version}-`));
|
|
15
|
+
try {
|
|
16
|
+
const platform = process.platform;
|
|
17
|
+
const npx = platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
18
|
+
const binPath = path_1.default.resolve(__dirname, '../bin/fraim.js');
|
|
19
|
+
// Run: npx -y -p node@<version> node bin/fraim.js init
|
|
20
|
+
// We use -y to skip prompt, and -p node@version to ensure that specific node binary is used
|
|
21
|
+
const execNode = (args) => {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const ps = (0, node_child_process_1.spawn)(npx, ['-y', '-p', `node@${version}`, 'node', `"${binPath}"`, ...args], {
|
|
24
|
+
cwd: tempDir,
|
|
25
|
+
env: { ...process.env, TEST_MODE: 'true' },
|
|
26
|
+
shell: true
|
|
27
|
+
});
|
|
28
|
+
let stdout = '';
|
|
29
|
+
let stderr = '';
|
|
30
|
+
ps.stdout.on('data', d => stdout += d.toString());
|
|
31
|
+
ps.stderr.on('data', d => stderr += d.toString());
|
|
32
|
+
ps.on('close', (code) => resolve({ stdout, stderr, code }));
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
console.log(` Running "fraim init" on Node ${version}...`);
|
|
36
|
+
const result = await execNode(['init']);
|
|
37
|
+
if (result.code !== 0) {
|
|
38
|
+
console.error(` ❌ Failed on Node ${version}`);
|
|
39
|
+
console.error(` Error: ${result.stderr}`);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const configPath = path_1.default.join(tempDir, '.fraim', 'config.json');
|
|
43
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
44
|
+
console.log(` ✅ Success on Node ${version}`);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.error(` ❌ Config not found on Node ${version}`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
console.error(` ❌ Test Logic Error on Node ${version}:`, e.message);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
try {
|
|
58
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
catch (e) { }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const nodeVersions = ['16', '18', '20', '22'];
|
|
64
|
+
const testCases = nodeVersions.map(v => ({
|
|
65
|
+
name: `Node v${v} Compatibility`,
|
|
66
|
+
version: v,
|
|
67
|
+
tags: ['node', 'compatibility']
|
|
68
|
+
}));
|
|
69
|
+
(0, test_utils_1.runTests)(testCases, async (t) => await testInitOnNodeVersion(t.version), 'Node Compatibility Matrix')
|
|
70
|
+
.then(() => process.exit(0))
|
|
71
|
+
.catch(() => process.exit(1));
|
|
@@ -0,0 +1,66 @@
|
|
|
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 test_utils_1 = require("./test-utils");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
11
|
+
async function testCleanNpmInstall(version) {
|
|
12
|
+
console.log(` 🧪 Testing NPM Install on Node v${version}...`);
|
|
13
|
+
// Create a temp directory for the install test
|
|
14
|
+
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `fraim-npm-${version}-`));
|
|
15
|
+
try {
|
|
16
|
+
const platform = process.platform;
|
|
17
|
+
const npx = platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
18
|
+
// Copy package.json to temp dir
|
|
19
|
+
const packageJsonPath = path_1.default.resolve(__dirname, '../package.json');
|
|
20
|
+
fs_1.default.copyFileSync(packageJsonPath, path_1.default.join(tempDir, 'package.json'));
|
|
21
|
+
// Run: npx -p node@<version> npm install --package-lock-only
|
|
22
|
+
// This uses the npm bundled with that specific Node version
|
|
23
|
+
const execNpm = () => {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const ps = (0, node_child_process_1.spawn)(npx, ['-y', '-p', `node@${version}`, 'npm', 'install', '--package-lock-only', '--quiet', '--no-audit'], {
|
|
26
|
+
cwd: tempDir,
|
|
27
|
+
env: { ...process.env },
|
|
28
|
+
shell: true
|
|
29
|
+
});
|
|
30
|
+
let stdout = '';
|
|
31
|
+
let stderr = '';
|
|
32
|
+
ps.stdout.on('data', d => stdout += d.toString());
|
|
33
|
+
ps.stderr.on('data', d => stderr += d.toString());
|
|
34
|
+
ps.on('close', (code) => resolve({ stdout, stderr, code }));
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
console.log(` Running "npm install" with Node ${version}...`);
|
|
38
|
+
const result = await execNpm();
|
|
39
|
+
if (result.code !== 0) {
|
|
40
|
+
console.error(` ❌ NPM Install Failed on Node ${version}`);
|
|
41
|
+
console.error(` Stderr: ${result.stderr}`);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
console.log(` ✅ NPM Resolution Success on Node ${version}`);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
console.error(` ❌ Test Logic Error on Node ${version}:`, e.message);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
try {
|
|
53
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
catch (e) { }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const nodeVersions = ['16', '18', '20', '22'];
|
|
59
|
+
const testCases = nodeVersions.map(v => ({
|
|
60
|
+
name: `NPM Compatibility (Node v${v})`,
|
|
61
|
+
version: v,
|
|
62
|
+
tags: ['npm', 'compatibility']
|
|
63
|
+
}));
|
|
64
|
+
(0, test_utils_1.runTests)(testCases, async (t) => await testCleanNpmInstall(t.version), 'NPM Configuration Matrix')
|
|
65
|
+
.then(() => process.exit(0))
|
|
66
|
+
.catch(() => process.exit(1));
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Diagnostic test to understand npm's chalk resolution behavior
|
|
4
|
+
*
|
|
5
|
+
* This test will show us exactly what npm does when there's a version conflict
|
|
6
|
+
*/
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
const node_child_process_1 = require("node:child_process");
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const os_1 = __importDefault(require("os"));
|
|
15
|
+
async function main() {
|
|
16
|
+
console.log('🔍 NPM Chalk Resolution Diagnostic\n');
|
|
17
|
+
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'npm-diagnostic-'));
|
|
18
|
+
console.log(`📂 Temp dir: ${tempDir}\n`);
|
|
19
|
+
try {
|
|
20
|
+
// 1. Pack fraim-framework
|
|
21
|
+
console.log('📦 Packing fraim-framework...');
|
|
22
|
+
const projectRoot = process.cwd();
|
|
23
|
+
const packResult = (0, node_child_process_1.execSync)('npm pack', {
|
|
24
|
+
cwd: projectRoot,
|
|
25
|
+
encoding: 'utf-8'
|
|
26
|
+
});
|
|
27
|
+
const tarballName = packResult.trim().split('\n').pop()?.trim();
|
|
28
|
+
if (!tarballName) {
|
|
29
|
+
console.log('❌ Failed to pack');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const tarballPath = path_1.default.join(projectRoot, tarballName);
|
|
33
|
+
console.log(`✅ Created: ${tarballName}\n`);
|
|
34
|
+
// 2. Create test project with chalk v5
|
|
35
|
+
console.log('📝 Creating test project with chalk v5 dependency...');
|
|
36
|
+
const packageJson = {
|
|
37
|
+
name: 'diagnostic-test',
|
|
38
|
+
version: '1.0.0',
|
|
39
|
+
dependencies: {
|
|
40
|
+
'chalk': '^5.0.0',
|
|
41
|
+
'fraim-framework': `file:${tarballPath}`
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
45
|
+
// 3. Install
|
|
46
|
+
console.log('📥 Running npm install...\n');
|
|
47
|
+
try {
|
|
48
|
+
const installOutput = (0, node_child_process_1.execSync)('npm install', {
|
|
49
|
+
cwd: tempDir,
|
|
50
|
+
encoding: 'utf-8',
|
|
51
|
+
stdio: 'pipe'
|
|
52
|
+
});
|
|
53
|
+
console.log('✅ Install completed\n');
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.log('⚠️ Install had issues:\n', error.stdout || error.message);
|
|
57
|
+
}
|
|
58
|
+
// 4. Check what was installed
|
|
59
|
+
console.log('📋 Checking installed chalk versions...\n');
|
|
60
|
+
// Root chalk
|
|
61
|
+
const rootChalkPath = path_1.default.join(tempDir, 'node_modules', 'chalk', 'package.json');
|
|
62
|
+
if (fs_1.default.existsSync(rootChalkPath)) {
|
|
63
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(rootChalkPath, 'utf-8'));
|
|
64
|
+
console.log(` Root chalk: ${pkg.version}`);
|
|
65
|
+
console.log(` Type: ${pkg.type || 'commonjs'}`);
|
|
66
|
+
console.log(` Main: ${pkg.main || 'N/A'}`);
|
|
67
|
+
console.log(` Exports: ${pkg.exports ? 'Yes' : 'No'}\n`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.log(' ❌ No root chalk found\n');
|
|
71
|
+
}
|
|
72
|
+
// Fraim's chalk
|
|
73
|
+
const fraimChalkPath = path_1.default.join(tempDir, 'node_modules', 'fraim-framework', 'node_modules', 'chalk', 'package.json');
|
|
74
|
+
if (fs_1.default.existsSync(fraimChalkPath)) {
|
|
75
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(fraimChalkPath, 'utf-8'));
|
|
76
|
+
console.log(` fraim-framework's chalk: ${pkg.version}`);
|
|
77
|
+
console.log(` Type: ${pkg.type || 'commonjs'}`);
|
|
78
|
+
console.log(` Main: ${pkg.main || 'N/A'}\n`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
console.log(' ℹ️ fraim-framework has no separate chalk (using root)\n');
|
|
82
|
+
}
|
|
83
|
+
// 5. Run npm ls to see the dependency tree
|
|
84
|
+
console.log('🌳 Dependency tree (npm ls chalk):\n');
|
|
85
|
+
try {
|
|
86
|
+
const lsOutput = (0, node_child_process_1.execSync)('npm ls chalk', {
|
|
87
|
+
cwd: tempDir,
|
|
88
|
+
encoding: 'utf-8'
|
|
89
|
+
});
|
|
90
|
+
console.log(lsOutput);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.log(error.stdout || error.message);
|
|
94
|
+
}
|
|
95
|
+
// 6. Check fraim-framework's package.json in node_modules
|
|
96
|
+
console.log('\n📋 fraim-framework\'s chalk dependency in node_modules:\n');
|
|
97
|
+
const fraimPkgPath = path_1.default.join(tempDir, 'node_modules', 'fraim-framework', 'package.json');
|
|
98
|
+
if (fs_1.default.existsSync(fraimPkgPath)) {
|
|
99
|
+
const fraimPkg = JSON.parse(fs_1.default.readFileSync(fraimPkgPath, 'utf-8'));
|
|
100
|
+
console.log(` Chalk version in package.json: ${fraimPkg.dependencies?.chalk || 'N/A'}\n`);
|
|
101
|
+
}
|
|
102
|
+
// 7. Try to load fraim CLI
|
|
103
|
+
console.log('🚀 Testing fraim CLI load...\n');
|
|
104
|
+
const testScript = `
|
|
105
|
+
try {
|
|
106
|
+
const fraimCli = require('./node_modules/fraim-framework/dist/src/cli/fraim.js');
|
|
107
|
+
console.log('✅ SUCCESS: fraim CLI loaded');
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.log('❌ ERROR:', error.code || 'Unknown');
|
|
110
|
+
console.log('Message:', error.message.substring(0, 200));
|
|
111
|
+
}
|
|
112
|
+
`;
|
|
113
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, 'test.js'), testScript);
|
|
114
|
+
try {
|
|
115
|
+
const testOutput = (0, node_child_process_1.execSync)('node test.js', {
|
|
116
|
+
cwd: tempDir,
|
|
117
|
+
encoding: 'utf-8'
|
|
118
|
+
});
|
|
119
|
+
console.log(testOutput);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
console.log(error.stdout || error.message);
|
|
123
|
+
}
|
|
124
|
+
// Cleanup
|
|
125
|
+
fs_1.default.unlinkSync(tarballPath);
|
|
126
|
+
console.log('\n✅ Diagnostic complete');
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error('\n❌ Diagnostic failed:', error);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
try {
|
|
133
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
console.log('\n⚠️ Could not clean up temp directory');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
main();
|
|
@@ -0,0 +1,145 @@
|
|
|
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 testRehydrationAndExemptions() {
|
|
14
|
+
console.log(' 🚀 Testing Session Re-hydration and Initialization Exemptions...');
|
|
15
|
+
let fraimProcess;
|
|
16
|
+
let dbService;
|
|
17
|
+
const PORT = Math.floor(Math.random() * 1000) + 12000;
|
|
18
|
+
const TEST_API_KEY = 'test-rehydration-key';
|
|
19
|
+
const TEST_ADMIN_KEY = 'test-admin-key';
|
|
20
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
21
|
+
const startServer = async () => {
|
|
22
|
+
console.log(` Starting server on port ${PORT}...`);
|
|
23
|
+
const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
24
|
+
const serverScript = path_1.default.resolve(__dirname, '../src/fraim-mcp-server.ts');
|
|
25
|
+
const proc = (0, node_child_process_1.spawn)(npxCommand, ['tsx', `"${serverScript}"`], {
|
|
26
|
+
env: {
|
|
27
|
+
...process.env,
|
|
28
|
+
FRAIM_MCP_PORT: PORT.toString(),
|
|
29
|
+
FRAIM_ADMIN_KEY: TEST_ADMIN_KEY,
|
|
30
|
+
FRAIM_SKIP_INDEX_ON_START: 'true'
|
|
31
|
+
},
|
|
32
|
+
stdio: 'inherit',
|
|
33
|
+
shell: true
|
|
34
|
+
});
|
|
35
|
+
let started = false;
|
|
36
|
+
for (let i = 0; i < 15; i++) {
|
|
37
|
+
try {
|
|
38
|
+
await axios_1.default.get(`${BASE_URL}/health`, { timeout: 500 });
|
|
39
|
+
started = true;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!started)
|
|
47
|
+
throw new Error('Server failed to start');
|
|
48
|
+
return proc;
|
|
49
|
+
};
|
|
50
|
+
const stopServer = async (proc) => {
|
|
51
|
+
if (proc && proc.pid) {
|
|
52
|
+
await new Promise(resolve => (0, tree_kill_1.default)(proc.pid, 'SIGKILL', () => resolve()));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
// 1. Setup DB and Key
|
|
57
|
+
dbService = new db_service_js_1.FraimDbService();
|
|
58
|
+
await dbService.connect();
|
|
59
|
+
const db = dbService.db;
|
|
60
|
+
await db.collection('fraim_api_keys').deleteOne({ key: TEST_API_KEY });
|
|
61
|
+
await db.collection('fraim_telemetry_sessions').deleteMany({ userId: 'test-user-recalc' });
|
|
62
|
+
await db.collection('fraim_api_keys').insertOne({
|
|
63
|
+
key: TEST_API_KEY,
|
|
64
|
+
userId: 'test-user-recalc',
|
|
65
|
+
orgId: 'test-org',
|
|
66
|
+
isActive: true,
|
|
67
|
+
createdAt: new Date()
|
|
68
|
+
});
|
|
69
|
+
// 2. Start Server (Instance 1)
|
|
70
|
+
fraimProcess = await startServer();
|
|
71
|
+
// 3. Verify Initialization Exemptions
|
|
72
|
+
console.log(' Testing standard MCP exemptions (initialize, tools/list)...');
|
|
73
|
+
const initRes = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
74
|
+
jsonrpc: '2.0', id: 1, method: 'initialize', params: {}
|
|
75
|
+
}, { headers: { 'x-api-key': TEST_API_KEY } });
|
|
76
|
+
node_assert_1.default.strictEqual(initRes.status, 200, 'initialize should be exempt');
|
|
77
|
+
const listRes = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
78
|
+
jsonrpc: '2.0', id: 2, method: 'tools/list', params: {}
|
|
79
|
+
}, { headers: { 'x-api-key': TEST_API_KEY } });
|
|
80
|
+
node_assert_1.default.strictEqual(listRes.status, 200, 'tools/list should be exempt');
|
|
81
|
+
console.log(' Testing discovery tools exemptions (get_fraim_init)...');
|
|
82
|
+
const discoveryRes = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
83
|
+
jsonrpc: '2.0', id: 3, method: 'tools/call',
|
|
84
|
+
params: { name: 'get_fraim_init', arguments: {} }
|
|
85
|
+
}, { headers: { 'x-api-key': TEST_API_KEY } });
|
|
86
|
+
node_assert_1.default.strictEqual(discoveryRes.status, 200, 'get_fraim_init should be exempt');
|
|
87
|
+
console.log(' ✅ Exemptions verified.');
|
|
88
|
+
// 4. Start Session
|
|
89
|
+
console.log(' Starting session...');
|
|
90
|
+
await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
91
|
+
jsonrpc: '2.0', id: 4, method: 'tools/call',
|
|
92
|
+
params: {
|
|
93
|
+
name: 'fraim_connect',
|
|
94
|
+
arguments: {
|
|
95
|
+
machine: { hostname: 'repro-host' },
|
|
96
|
+
repo: { url: 'http://github.com/repro' }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}, { headers: { 'x-api-key': TEST_API_KEY } });
|
|
100
|
+
// 5. Restart Server (Instance 2)
|
|
101
|
+
console.log(' ♻️ Swapping server instances (Simulating restart)...');
|
|
102
|
+
await stopServer(fraimProcess);
|
|
103
|
+
fraimProcess = await startServer();
|
|
104
|
+
// 6. Verify Re-hydration
|
|
105
|
+
console.log(' Checking if session re-hydrated from DB...');
|
|
106
|
+
const secureRes = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
107
|
+
jsonrpc: '2.0', id: 5, method: 'tools/call',
|
|
108
|
+
params: { name: 'get_fraim_workflow', arguments: { workflow: 'test' } }
|
|
109
|
+
}, { headers: { 'x-api-key': TEST_API_KEY } });
|
|
110
|
+
node_assert_1.default.strictEqual(secureRes.status, 200, 'Request should be authorized via re-hydrated session');
|
|
111
|
+
console.log(' ✅ Re-hydration successful!');
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error(' ❌ Test failed:', error.message);
|
|
116
|
+
if (error.response?.data) {
|
|
117
|
+
console.error(' 🔍 Server Error Detail:', JSON.stringify(error.response.data, null, 2));
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
if (dbService) {
|
|
123
|
+
const db = dbService.db;
|
|
124
|
+
await db.collection('fraim_api_keys').deleteOne({ key: TEST_API_KEY }).catch(() => { });
|
|
125
|
+
await db.collection('fraim_telemetry_sessions').deleteMany({ userId: 'test-user-recalc' }).catch(() => { });
|
|
126
|
+
await dbService.close();
|
|
127
|
+
}
|
|
128
|
+
if (fraimProcess)
|
|
129
|
+
await stopServer(fraimProcess);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const testCases = [
|
|
133
|
+
{
|
|
134
|
+
name: 'MCP Session Re-hydration and Initialization',
|
|
135
|
+
description: 'Verifies re-hydration from DB after restart and initialization exemptions',
|
|
136
|
+
testFunction: testRehydrationAndExemptions,
|
|
137
|
+
tags: ['session', 'rehydration']
|
|
138
|
+
}
|
|
139
|
+
];
|
|
140
|
+
(0, test_utils_1.runTests)(testCases, async (t) => t.testFunction(), 'Fraim Session System')
|
|
141
|
+
.then(() => process.exit(0))
|
|
142
|
+
.catch((err) => {
|
|
143
|
+
console.error('Test runner failed:', err);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
|
@@ -98,13 +98,7 @@ async function testServerStartsAndResponds() {
|
|
|
98
98
|
});
|
|
99
99
|
node_assert_1.default.strictEqual(mcpResponse.status, 200);
|
|
100
100
|
node_assert_1.default.ok(mcpResponse.data.result.tools.length > 0);
|
|
101
|
-
// 7.
|
|
102
|
-
console.log(' Verifying usage logging...');
|
|
103
|
-
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
104
|
-
const log = await db.collection('fraim_usage_logs').findOne({ keyId: TEST_API_KEY });
|
|
105
|
-
node_assert_1.default.ok(log, 'Usage log should have been created');
|
|
106
|
-
node_assert_1.default.strictEqual(log.userId, 'test-user@ashley.ai');
|
|
107
|
-
// 8. Test Admin API - List Keys
|
|
101
|
+
// 7. Test Admin API - List Keys
|
|
108
102
|
console.log(' Testing Admin API - List Keys...');
|
|
109
103
|
const listKeysRes = await axios_1.default.get(`${BASE_URL}/admin/keys`, {
|
|
110
104
|
headers: TEST_ADMIN_HEADER,
|
|
@@ -141,7 +135,7 @@ async function runFraimTest(testCase) {
|
|
|
141
135
|
const testCases = [
|
|
142
136
|
{
|
|
143
137
|
name: 'Fraim Standalone Server Integration',
|
|
144
|
-
description: 'Tests server startup, public health check, authenticated MCP access,
|
|
138
|
+
description: 'Tests server startup, public health check, authenticated MCP access, and admin API',
|
|
145
139
|
testFunction: testServerStartsAndResponds,
|
|
146
140
|
tags: ['smoke', 'fraim']
|
|
147
141
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
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 testTelemetryFlow() {
|
|
14
|
+
console.log(' 🚀 Testing Fraim Telemetry System...');
|
|
15
|
+
let fraimProcess;
|
|
16
|
+
let dbService;
|
|
17
|
+
const PORT = Math.floor(Math.random() * 1000) + 11000; // Use random port to avoid zombie conflicts
|
|
18
|
+
console.log(` 🎲 Selected random port: ${PORT}`);
|
|
19
|
+
const TEST_API_KEY = 'test-fraim-key-telemetry';
|
|
20
|
+
const TEST_ADMIN_KEY = 'test-admin-key-telemetry';
|
|
21
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
22
|
+
const DB_CHECK_DELAY = 1000;
|
|
23
|
+
try {
|
|
24
|
+
// 1. Setup DB and Key
|
|
25
|
+
dbService = new db_service_js_1.FraimDbService();
|
|
26
|
+
await dbService.connect();
|
|
27
|
+
const db = dbService.db;
|
|
28
|
+
// Clean previous test data
|
|
29
|
+
await db.collection('fraim_api_keys').deleteOne({ key: TEST_API_KEY });
|
|
30
|
+
await db.collection('fraim_telemetry_sessions').deleteMany({ userId: 'test-user-telemetry' });
|
|
31
|
+
// Insert Test Key
|
|
32
|
+
await db.collection('fraim_api_keys').insertOne({
|
|
33
|
+
key: TEST_API_KEY,
|
|
34
|
+
userId: 'test-user-telemetry',
|
|
35
|
+
orgId: 'test-org',
|
|
36
|
+
isActive: true,
|
|
37
|
+
createdAt: new Date()
|
|
38
|
+
});
|
|
39
|
+
// 2. Start Server
|
|
40
|
+
console.log(` Starting server on port ${PORT}...`);
|
|
41
|
+
const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
42
|
+
const serverScript = path_1.default.resolve(__dirname, '../src/fraim-mcp-server.ts');
|
|
43
|
+
fraimProcess = (0, node_child_process_1.spawn)(npxCommand, ['tsx', `"${serverScript}"`], {
|
|
44
|
+
env: {
|
|
45
|
+
...process.env,
|
|
46
|
+
FRAIM_MCP_PORT: PORT.toString(),
|
|
47
|
+
FRAIM_ADMIN_KEY: TEST_ADMIN_KEY,
|
|
48
|
+
FRAIM_SKIP_INDEX_ON_START: 'true',
|
|
49
|
+
FRAIM_TELEMETRY_FLUSH_INTERVAL: '200' // Short interval for testing
|
|
50
|
+
},
|
|
51
|
+
stdio: 'inherit',
|
|
52
|
+
shell: true
|
|
53
|
+
});
|
|
54
|
+
// Wait for start
|
|
55
|
+
let started = false;
|
|
56
|
+
for (let i = 0; i < 15; i++) {
|
|
57
|
+
try {
|
|
58
|
+
await axios_1.default.get(`${BASE_URL}/health`, { timeout: 500 });
|
|
59
|
+
started = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!started)
|
|
67
|
+
throw new Error('Server failed to start');
|
|
68
|
+
console.log(' Server started!');
|
|
69
|
+
// 3. Test Enforcement: Call tool WITHOUT handshake -> Expect 400
|
|
70
|
+
console.log(' Testing enforcement (No Session)...');
|
|
71
|
+
try {
|
|
72
|
+
const res = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
73
|
+
jsonrpc: '2.0', id: 1, method: 'tools/call',
|
|
74
|
+
params: { name: 'get_fraim_workflow', arguments: { workflow: 'spec' } }
|
|
75
|
+
}, { headers: { 'x-api-key': TEST_API_KEY }, timeout: 2000 });
|
|
76
|
+
console.error(' ❌ Should have failed with 400 (Session Not Started)');
|
|
77
|
+
console.error(' 🔍 Unexpected Success Response:', JSON.stringify(res.data, null, 2));
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error.response) {
|
|
82
|
+
node_assert_1.default.strictEqual(error.response.status, 400);
|
|
83
|
+
node_assert_1.default.match(error.response.data?.error?.message, /Session Not Started/);
|
|
84
|
+
console.log(' ✅ Enforcement working: Request blocked.');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
throw error; // Network error?
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// 4. Test Handshake: Call fraim_connect -> Expect 200 & DB Record
|
|
91
|
+
console.log(' Testing fraim_connect...');
|
|
92
|
+
const connectRes = await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
93
|
+
jsonrpc: '2.0', id: 2, method: 'tools/call',
|
|
94
|
+
params: {
|
|
95
|
+
name: 'fraim_connect',
|
|
96
|
+
arguments: {
|
|
97
|
+
machine: { hostname: 'test-host', platform: 'test-os' },
|
|
98
|
+
repo: { url: 'http://github.com/test/repo' }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, { headers: { 'x-api-key': TEST_API_KEY }, timeout: 2000 });
|
|
102
|
+
node_assert_1.default.strictEqual(connectRes.status, 200);
|
|
103
|
+
const connectResult = connectRes.data.result;
|
|
104
|
+
const connectedMsg = connectResult.content.find((c) => c.text.includes('Connected!'));
|
|
105
|
+
node_assert_1.default.ok(connectedMsg, 'Should find Connected! message in response content');
|
|
106
|
+
// Verify Session ID returned in payload
|
|
107
|
+
node_assert_1.default.ok(connectResult.sessionId, 'Should return sessionId in response');
|
|
108
|
+
console.log(` ✅ Handshake successful. Session ID: ${connectResult.sessionId}`);
|
|
109
|
+
// Verify DB Record Created
|
|
110
|
+
await new Promise(r => setTimeout(r, DB_CHECK_DELAY));
|
|
111
|
+
const session = await db.collection('fraim_telemetry_sessions').findOne({ userId: 'test-user-telemetry' });
|
|
112
|
+
node_assert_1.default.ok(session, 'Session should exist in DB');
|
|
113
|
+
node_assert_1.default.strictEqual(session.machine.hostname, 'test-host');
|
|
114
|
+
const initialLastActive = session.lastActive.getTime();
|
|
115
|
+
console.log(` ✅ DB Session verified. Initial LastActive: ${initialLastActive}`);
|
|
116
|
+
// 5. Test Implicit Flush (Write-Behind)
|
|
117
|
+
// Wait > 200ms (flush interval)
|
|
118
|
+
console.log(' Waiting 1s to exceed flush interval...');
|
|
119
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
120
|
+
// Call a tool (Authorized) - Should trigger flush
|
|
121
|
+
await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
122
|
+
jsonrpc: '2.0', id: 3, method: 'tools/call',
|
|
123
|
+
params: { name: 'get_fraim_workflow', arguments: { workflow: 'test' } }
|
|
124
|
+
}, { headers: { 'x-api-key': TEST_API_KEY }, timeout: 2000 });
|
|
125
|
+
console.log(' ✅ Tool call successful. Should trigger flush.');
|
|
126
|
+
// Give DB a moment to update
|
|
127
|
+
await new Promise(r => setTimeout(r, DB_CHECK_DELAY));
|
|
128
|
+
const sessionUpdated = await db.collection('fraim_telemetry_sessions').findOne({ userId: 'test-user-telemetry' });
|
|
129
|
+
const updatedLastActive = sessionUpdated.lastActive.getTime();
|
|
130
|
+
console.log(` Updated LastActive: ${updatedLastActive}`);
|
|
131
|
+
node_assert_1.default.ok(updatedLastActive > initialLastActive, 'Implicit flush should have updated the DB timestamp');
|
|
132
|
+
console.log(' ✅ Implicit flush verified.');
|
|
133
|
+
// 6. Test Shutdown Flush
|
|
134
|
+
console.log(' Testing Shutdown Flush...');
|
|
135
|
+
// Trigger ONE MORE activity, but don't wait for implicit flush
|
|
136
|
+
await new Promise(r => setTimeout(r, 10)); // Ensure at least 10ms diff
|
|
137
|
+
await axios_1.default.post(`${BASE_URL}/mcp`, {
|
|
138
|
+
jsonrpc: '2.0', id: 4, method: 'tools/call',
|
|
139
|
+
params: { name: 'get_fraim_workflow', arguments: { workflow: 'test-2' } }
|
|
140
|
+
}, { headers: { 'x-api-key': TEST_API_KEY }, timeout: 2000 });
|
|
141
|
+
console.log(' ✅ Late activity triggered (Pending Flush).');
|
|
142
|
+
// Kill the server with SIGTERM to trigger graceful shutdown
|
|
143
|
+
// Kill the server with SIGTERM to trigger graceful shutdown
|
|
144
|
+
// Use tree-kill to ensure we reach the actual node process through the npx/shell wrapper
|
|
145
|
+
if (fraimProcess && fraimProcess.pid) {
|
|
146
|
+
await new Promise(resolve => (0, tree_kill_1.default)(fraimProcess.pid, 'SIGTERM', () => resolve()));
|
|
147
|
+
}
|
|
148
|
+
// Wait for process to exit and flush
|
|
149
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
150
|
+
const sessionFinal = await db.collection('fraim_telemetry_sessions').findOne({ userId: 'test-user-telemetry' });
|
|
151
|
+
const finalLastActive = sessionFinal.lastActive.getTime();
|
|
152
|
+
console.log(` Final LastActive: ${finalLastActive}`);
|
|
153
|
+
node_assert_1.default.ok(finalLastActive > updatedLastActive, 'Final Flush should have updated the DB timestamp with the pending activity');
|
|
154
|
+
console.log(' ✅ Shutdown flush verified.');
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error(' ❌ Test failed:', error.message);
|
|
159
|
+
if (error.response?.data) {
|
|
160
|
+
console.error(' 🔍 Server Error Detail:', JSON.stringify(error.response.data, null, 2));
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
if (dbService) {
|
|
166
|
+
const db = dbService.db;
|
|
167
|
+
await db.collection('fraim_api_keys').deleteOne({ key: TEST_API_KEY }).catch(() => { });
|
|
168
|
+
await db.collection('fraim_telemetry_sessions').deleteMany({ userId: 'test-user-telemetry' }).catch(() => { });
|
|
169
|
+
await dbService.close();
|
|
170
|
+
}
|
|
171
|
+
if (fraimProcess && fraimProcess.pid) {
|
|
172
|
+
console.log(' 🔫 Cleanup: Killing server process tree:', fraimProcess.pid);
|
|
173
|
+
await new Promise(resolve => (0, tree_kill_1.default)(fraimProcess.pid, 'SIGKILL', () => resolve()));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const testCases = [
|
|
178
|
+
{
|
|
179
|
+
name: 'Fraim Telemetry & Session Management',
|
|
180
|
+
description: 'Tests handshake enforcement, session creation, implicit tracking, and write-behind caching',
|
|
181
|
+
testFunction: testTelemetryFlow,
|
|
182
|
+
tags: ['telemetry']
|
|
183
|
+
}
|
|
184
|
+
];
|
|
185
|
+
(0, test_utils_1.runTests)(testCases, async (t) => t.testFunction(), 'Fraim Telemetry System')
|
|
186
|
+
.then(() => process.exit(0))
|
|
187
|
+
.catch((err) => {
|
|
188
|
+
console.error('Test runner failed:', err);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
});
|