fraim-framework 2.0.22 → 2.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.github/workflows/deploy-fraim.yml +3 -1
  2. package/dist/src/cli/commands/init.js +14 -6
  3. package/dist/src/cli/commands/sync.js +4 -1
  4. package/dist/src/fraim/config-loader.js +30 -18
  5. package/dist/src/fraim/setup-wizard.js +13 -50
  6. package/dist/src/fraim/template-processor.js +1 -1
  7. package/dist/src/fraim/types.js +21 -25
  8. package/dist/src/fraim-mcp-server.js +37 -33
  9. package/dist/src/utils/git-utils.js +24 -1
  10. package/dist/tests/test-cli.js +169 -0
  11. package/dist/tests/test-first-run-journey.js +108 -0
  12. package/dist/tests/test-genericization.js +66 -0
  13. package/{test-prep-issue.ts → dist/tests/test-prep-issue.js} +93 -101
  14. package/{test-standalone.ts → dist/tests/test-standalone.js} +149 -161
  15. package/dist/tests/test-user-journey.js +231 -0
  16. package/dist/tests/test-utils.js +96 -0
  17. package/{test-wizard.ts → dist/tests/test-wizard.js} +71 -81
  18. package/package.json +11 -5
  19. package/registry/rules/architecture.md +1 -1
  20. package/registry/scripts/code-quality-check.sh +5 -4
  21. package/registry/scripts/evaluate-code-quality.ts +36 -0
  22. package/registry/scripts/fraim-config.ts +2 -1
  23. package/registry/scripts/generate-engagement-emails.ts +3 -0
  24. package/registry/scripts/newsletter-helpers.ts +3 -0
  25. package/registry/scripts/{validate-coverage.ts → validate-test-coverage.ts} +39 -39
  26. package/registry/scripts/verify-test-coverage.ts +36 -0
  27. package/registry/templates/bootstrap/ARCHITECTURE-TEMPLATE.md +53 -0
  28. package/registry/templates/bootstrap/CODE-QUALITY-REPORT-TEMPLATE.md +37 -0
  29. package/registry/templates/bootstrap/TEST-COVERAGE-REPORT-TEMPLATE.md +35 -0
  30. package/registry/templates/business-development/PRICING-STRATEGY-TEMPLATE.md +126 -0
  31. package/registry/templates/customer-development/thank-you-email-template.html +76 -60
  32. package/registry/templates/customer-development/weekly-newsletter-template.html +5 -5
  33. package/registry/workflows/bootstrap/create-architecture.md +13 -12
  34. package/registry/workflows/bootstrap/evaluate-code-quality.md +30 -0
  35. package/registry/workflows/bootstrap/verify-test-coverage.md +31 -0
  36. package/registry/workflows/business-development/price-product.md +325 -0
  37. package/registry/workflows/customer-development/weekly-newsletter.md +16 -43
  38. package/tsconfig.json +4 -4
  39. package/test-cli.ts +0 -116
  40. package/test-first-run-journey.ts +0 -122
  41. package/test-user-journey.ts +0 -244
  42. package/test-utils.ts +0 -120
@@ -1,161 +1,149 @@
1
- import { spawn, ChildProcess } from 'node:child_process';
2
- import axios from 'axios';
3
- import { FraimDbService } from './src/fraim/db-service.js';
4
- import { BaseTestCase, runTests } from './test-utils';
5
- import assert from 'node:assert';
6
- import kill from 'tree-kill';
7
-
8
- interface FraimStandaloneTestCase extends BaseTestCase {
9
- testFunction: () => Promise<boolean>;
10
- }
11
-
12
- async function testServerStartsAndResponds(): Promise<boolean> {
13
- console.log(' 🚀 Testing Fraim Standalone Server...');
14
- let fraimProcess: ChildProcess | undefined;
15
- let dbService: FraimDbService | undefined;
16
- const PORT = 10001; // Use a different port to avoid conflicts
17
- const TEST_API_KEY = 'test-fraim-key-integration';
18
- const TEST_ADMIN_KEY = 'test-admin-key-integration';
19
- const TEST_ADMIN_HEADER = { 'x-admin-key': TEST_ADMIN_KEY };
20
- const BASE_URL = `http://localhost:${PORT}`;
21
-
22
- try {
23
- // 1. Seed the test API key in the database
24
- dbService = new FraimDbService();
25
- await dbService.connect();
26
-
27
- const db = (dbService as any).db;
28
- await db.collection('fraim_api_keys').updateOne(
29
- { key: TEST_API_KEY },
30
- {
31
- $set: {
32
- userId: 'test-user@ashley.ai',
33
- orgId: 'test-org',
34
- isActive: true,
35
- createdAt: new Date()
36
- }
37
- },
38
- { upsert: true }
39
- );
40
-
41
- // 2. Start server in standalone mode
42
- console.log(` Starting server on port ${PORT}...`);
43
- const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
44
- fraimProcess = spawn(npxCommand, ['tsx', 'src/fraim-mcp-server.ts'], {
45
- env: {
46
- ...process.env,
47
- FRAIM_MCP_PORT: PORT.toString(),
48
- FRAIM_ADMIN_KEY: TEST_ADMIN_KEY,
49
- FRAIM_SKIP_INDEX_ON_START: 'true' // Potential optimization if implemented
50
- },
51
- stdio: 'inherit',
52
- shell: true
53
- });
54
-
55
- // 3. Wait for server to start
56
- console.log(' Waiting for server to start...');
57
- let started = false;
58
- for (let i = 0; i < 15; i++) {
59
- try {
60
- await axios.get(`${BASE_URL}/health`, { timeout: 1000 });
61
- started = true;
62
- console.log(' Server started!');
63
- break;
64
- } catch (e) {
65
- await new Promise(resolve => setTimeout(resolve, 1000));
66
- }
67
- }
68
-
69
- if (!started) {
70
- console.error(' ❌ Server failed to start within timeout');
71
- return false;
72
- }
73
-
74
- // 4. Test health check (public)
75
- console.log(' Testing public health check...');
76
- const healthRes = await axios.get(`${BASE_URL}/health`, { timeout: 2000 });
77
- assert.strictEqual(healthRes.status, 200);
78
- assert.strictEqual(healthRes.data.status, 'ok');
79
-
80
- // 5. Test MCP tool list without API key (fail)
81
- console.log(' Testing MCP without auth (should fail)...');
82
- try {
83
- await axios.post(`${BASE_URL}/mcp`, {
84
- jsonrpc: '2.0',
85
- id: 1,
86
- method: 'tools/list',
87
- params: {}
88
- }, { timeout: 2000 });
89
- console.error(' Should have failed without API key');
90
- return false;
91
- } catch (error: any) {
92
- assert.ok(error.response, 'Should have a response');
93
- assert.strictEqual(error.response.status, 401);
94
- }
95
-
96
- // 6. Test MCP tool list with correct API key (success)
97
- console.log(' Testing MCP with correct auth...');
98
- const mcpResponse = await axios.post(`${BASE_URL}/mcp`, {
99
- jsonrpc: '2.0',
100
- id: 1,
101
- method: 'tools/list',
102
- params: {}
103
- }, {
104
- headers: { 'x-api-key': TEST_API_KEY },
105
- timeout: 5000
106
- });
107
- assert.strictEqual(mcpResponse.status, 200);
108
- assert.ok(mcpResponse.data.result.tools.length > 0);
109
-
110
- // 7. Verify usage logging
111
- console.log(' Verifying usage logging...');
112
- await new Promise(resolve => setTimeout(resolve, 1500));
113
- const log = await db.collection('fraim_usage_logs').findOne({ keyId: TEST_API_KEY });
114
- assert.ok(log, 'Usage log should have been created');
115
- assert.strictEqual(log.userId, 'test-user@ashley.ai');
116
-
117
- // 8. Test Admin API - List Keys
118
- console.log(' Testing Admin API - List Keys...');
119
- const listKeysRes = await axios.get(`${BASE_URL}/admin/keys`, {
120
- headers: TEST_ADMIN_HEADER,
121
- timeout: 2000
122
- });
123
- assert.strictEqual(listKeysRes.status, 200);
124
- assert.ok(Array.isArray(listKeysRes.data));
125
- assert.ok(listKeysRes.data.some((k: any) => k.key === TEST_API_KEY));
126
-
127
- return true;
128
- } catch (error) {
129
- console.error(' ❌ Test failed:', error);
130
- return false;
131
- } finally {
132
- console.log(' Cleaning up...');
133
- if (dbService) {
134
- const db = (dbService as any).db;
135
- if (db) {
136
- await db.collection('fraim_api_keys').deleteOne({ key: TEST_API_KEY }).catch(() => { });
137
- }
138
- await dbService.close().catch(() => { });
139
- }
140
- if (fraimProcess && fraimProcess.pid) {
141
- const pid = fraimProcess.pid;
142
- await new Promise<void>((resolve) => kill(pid, 'SIGKILL', () => resolve()));
143
- console.log(` Terminated server process ${pid}`);
144
- }
145
- }
146
- }
147
-
148
- async function runFraimTest(testCase: FraimStandaloneTestCase): Promise<boolean> {
149
- return await testCase.testFunction();
150
- }
151
-
152
- const testCases: FraimStandaloneTestCase[] = [
153
- {
154
- name: 'Fraim Standalone Server Integration',
155
- description: 'Tests server startup, public health check, authenticated MCP access, usage logging, and admin API',
156
- testFunction: testServerStartsAndResponds,
157
- tags: ['smoke', 'fraim']
158
- }
159
- ];
160
-
161
- runTests(testCases, runFraimTest, 'Fraim Standalone Server');
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 testServerStartsAndResponds() {
14
+ console.log(' 🚀 Testing Fraim Standalone Server...');
15
+ let fraimProcess;
16
+ let dbService;
17
+ const PORT = 10001; // Use a different port to avoid conflicts
18
+ const TEST_API_KEY = 'test-fraim-key-integration';
19
+ const TEST_ADMIN_KEY = 'test-admin-key-integration';
20
+ const TEST_ADMIN_HEADER = { 'x-admin-key': TEST_ADMIN_KEY };
21
+ const BASE_URL = `http://localhost:${PORT}`;
22
+ try {
23
+ // 1. Seed the test API key in the database
24
+ dbService = new db_service_js_1.FraimDbService();
25
+ await dbService.connect();
26
+ const db = dbService.db;
27
+ await db.collection('fraim_api_keys').updateOne({ key: TEST_API_KEY }, {
28
+ $set: {
29
+ userId: 'test-user@ashley.ai',
30
+ orgId: 'test-org',
31
+ isActive: true,
32
+ createdAt: new Date()
33
+ }
34
+ }, { upsert: true });
35
+ // 2. Start server in standalone mode
36
+ console.log(` Starting server on port ${PORT}...`);
37
+ const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
38
+ const serverScript = path_1.default.resolve(__dirname, '../src/fraim-mcp-server.ts');
39
+ fraimProcess = (0, node_child_process_1.spawn)(npxCommand, ['tsx', `"${serverScript}"`], {
40
+ env: {
41
+ ...process.env,
42
+ FRAIM_MCP_PORT: PORT.toString(),
43
+ FRAIM_ADMIN_KEY: TEST_ADMIN_KEY,
44
+ FRAIM_SKIP_INDEX_ON_START: 'true' // Potential optimization if implemented
45
+ },
46
+ stdio: 'inherit',
47
+ shell: true
48
+ });
49
+ // 3. Wait for server to start
50
+ console.log(' Waiting for server to start...');
51
+ let started = false;
52
+ for (let i = 0; i < 15; i++) {
53
+ try {
54
+ await axios_1.default.get(`${BASE_URL}/health`, { timeout: 1000 });
55
+ started = true;
56
+ console.log(' Server started!');
57
+ break;
58
+ }
59
+ catch (e) {
60
+ await new Promise(resolve => setTimeout(resolve, 1000));
61
+ }
62
+ }
63
+ if (!started) {
64
+ console.error(' ❌ Server failed to start within timeout');
65
+ return false;
66
+ }
67
+ // 4. Test health check (public)
68
+ console.log(' Testing public health check...');
69
+ const healthRes = await axios_1.default.get(`${BASE_URL}/health`, { timeout: 2000 });
70
+ node_assert_1.default.strictEqual(healthRes.status, 200);
71
+ node_assert_1.default.strictEqual(healthRes.data.status, 'ok');
72
+ // 5. Test MCP tool list without API key (fail)
73
+ console.log(' Testing MCP without auth (should fail)...');
74
+ try {
75
+ await axios_1.default.post(`${BASE_URL}/mcp`, {
76
+ jsonrpc: '2.0',
77
+ id: 1,
78
+ method: 'tools/list',
79
+ params: {}
80
+ }, { timeout: 2000 });
81
+ console.error(' Should have failed without API key');
82
+ return false;
83
+ }
84
+ catch (error) {
85
+ node_assert_1.default.ok(error.response, 'Should have a response');
86
+ node_assert_1.default.strictEqual(error.response.status, 401);
87
+ }
88
+ // 6. Test MCP tool list with correct API key (success)
89
+ console.log(' Testing MCP with correct auth...');
90
+ const mcpResponse = await axios_1.default.post(`${BASE_URL}/mcp`, {
91
+ jsonrpc: '2.0',
92
+ id: 1,
93
+ method: 'tools/list',
94
+ params: {}
95
+ }, {
96
+ headers: { 'x-api-key': TEST_API_KEY },
97
+ timeout: 5000
98
+ });
99
+ node_assert_1.default.strictEqual(mcpResponse.status, 200);
100
+ node_assert_1.default.ok(mcpResponse.data.result.tools.length > 0);
101
+ // 7. Verify usage logging
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
108
+ console.log(' Testing Admin API - List Keys...');
109
+ const listKeysRes = await axios_1.default.get(`${BASE_URL}/admin/keys`, {
110
+ headers: TEST_ADMIN_HEADER,
111
+ timeout: 2000
112
+ });
113
+ node_assert_1.default.strictEqual(listKeysRes.status, 200);
114
+ node_assert_1.default.ok(Array.isArray(listKeysRes.data));
115
+ node_assert_1.default.ok(listKeysRes.data.some((k) => k.key === TEST_API_KEY));
116
+ return true;
117
+ }
118
+ catch (error) {
119
+ console.error(' ❌ Test failed:', error);
120
+ return false;
121
+ }
122
+ finally {
123
+ console.log(' Cleaning up...');
124
+ if (dbService) {
125
+ const db = dbService.db;
126
+ if (db) {
127
+ await db.collection('fraim_api_keys').deleteOne({ key: TEST_API_KEY }).catch(() => { });
128
+ }
129
+ await dbService.close().catch(() => { });
130
+ }
131
+ if (fraimProcess && fraimProcess.pid) {
132
+ const pid = fraimProcess.pid;
133
+ await new Promise((resolve) => (0, tree_kill_1.default)(pid, 'SIGKILL', () => resolve()));
134
+ console.log(` Terminated server process ${pid}`);
135
+ }
136
+ }
137
+ }
138
+ async function runFraimTest(testCase) {
139
+ return await testCase.testFunction();
140
+ }
141
+ const testCases = [
142
+ {
143
+ name: 'Fraim Standalone Server Integration',
144
+ description: 'Tests server startup, public health check, authenticated MCP access, usage logging, and admin API',
145
+ testFunction: testServerStartsAndResponds,
146
+ tags: ['smoke', 'fraim']
147
+ }
148
+ ];
149
+ (0, test_utils_1.runTests)(testCases, runFraimTest, 'Fraim Standalone Server');
@@ -0,0 +1,231 @@
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 db_service_1 = require("../src/fraim/db-service");
10
+ const node_assert_1 = __importDefault(require("node:assert"));
11
+ const tree_kill_1 = __importDefault(require("tree-kill"));
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const os_1 = __importDefault(require("os"));
15
+ async function testDualDiscoveryJourney() {
16
+ console.log(' 🚀 Starting Dual Discovery User Journey Test...');
17
+ // Setup
18
+ const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'fraim-journey-'));
19
+ console.log(` 📂 Project Context: ${tempDir}`);
20
+ let fraimProcess;
21
+ let dbService;
22
+ const PORT = 10004;
23
+ const BASE_URL = `http://localhost:${PORT}`;
24
+ const TEST_API_KEY = 'fraim-journey-key';
25
+ try {
26
+ // --- PRE-REQUISITE: DB Setup ---
27
+ dbService = new db_service_1.FraimDbService();
28
+ await dbService.connect();
29
+ const db = dbService.db;
30
+ await db.collection('fraim_api_keys').updateOne({ key: TEST_API_KEY }, { $set: { userId: 'journey-user', orgId: 'journey-org', isActive: true } }, { upsert: true });
31
+ // --- SCENARIO 1: Project Initialization ---
32
+ console.log('\n [1/4] Scenario: User initializes project');
33
+ // Mock registry in the repo for sync to work
34
+ // We will point the test to use the repo's actual registry folder
35
+ // But for 'init' we just need the script
36
+ const cliScript = path_1.default.resolve(__dirname, '../src/cli/fraim.ts');
37
+ const tsxCli = path_1.default.resolve(__dirname, '../node_modules/tsx/dist/cli.mjs');
38
+ const runFraim = async (args) => {
39
+ return new Promise(resolve => {
40
+ const ps = (0, node_child_process_1.spawn)(process.execPath, [tsxCli, cliScript, ...args], {
41
+ cwd: tempDir,
42
+ env: { ...process.env, TEST_MODE: 'true' },
43
+ shell: false
44
+ });
45
+ let stdout = '';
46
+ let stderr = '';
47
+ ps.stdout.on('data', d => stdout += d.toString());
48
+ ps.stderr.on('data', d => stderr += d.toString());
49
+ ps.on('close', code => resolve({ code, stdout: stdout + '\nSTDERR:\n' + stderr }));
50
+ });
51
+ };
52
+ const initRes = await runFraim(['init']);
53
+ node_assert_1.default.strictEqual(initRes.code, 0, 'Init failed');
54
+ node_assert_1.default.ok(fs_1.default.existsSync(path_1.default.join(tempDir, '.fraim', 'config.json')), 'Config missing');
55
+ console.log(' ✅ `fraim init` created project structure');
56
+ // --- SEED REGISTRY ---
57
+ // The CLI sync command looks for 'registry' in the project root if it can't find it relative to the script.
58
+ // We simulate a valid environment by copying the registry to the temp project.
59
+ const sourceRegistry = path_1.default.join(__dirname, '../registry');
60
+ const targetRegistry = path_1.default.join(tempDir, 'registry');
61
+ // Simple recursive copy helper
62
+ const copyRecursiveSync = (src, dest) => {
63
+ if (fs_1.default.existsSync(src)) {
64
+ if (fs_1.default.lstatSync(src).isDirectory()) {
65
+ if (!fs_1.default.existsSync(dest))
66
+ fs_1.default.mkdirSync(dest);
67
+ fs_1.default.readdirSync(src).forEach(child => {
68
+ copyRecursiveSync(path_1.default.join(src, child), path_1.default.join(dest, child));
69
+ });
70
+ }
71
+ else {
72
+ fs_1.default.copyFileSync(src, dest);
73
+ }
74
+ }
75
+ };
76
+ copyRecursiveSync(sourceRegistry, targetRegistry);
77
+ console.log(' 📂 Seeded test registry');
78
+ // --- SCENARIO 2: Workflow Discovery (Stubbing) ---
79
+ console.log('\n [2/4] Scenario: User expects stubs for discovery');
80
+ // Create a fake registry workflow to test syncing
81
+ // The registry is now seeded in tempDir/registry, so sync should find it via fallback.
82
+ const syncRes = await runFraim(['sync']);
83
+ if (syncRes.code !== 0) {
84
+ console.error(' ❌ Sync failed. Output:', syncRes.stdout);
85
+ // Spawn helper above captures stdout/stderr mixed or just stdout?
86
+ // The helper captures stdout from ps.stdout and ignore stderr?
87
+ // Let's check helper.
88
+ }
89
+ node_assert_1.default.strictEqual(syncRes.code, 0, 'Sync failed');
90
+ // Verify Stub Content
91
+ // The registry has 'product-building/design.md'.
92
+ // Sync should now preserve the 'product-building/' folder.
93
+ const localWorkflowsDir = path_1.default.join(tempDir, '.fraim', 'workflows');
94
+ if (!fs_1.default.existsSync(localWorkflowsDir))
95
+ fs_1.default.mkdirSync(localWorkflowsDir, { recursive: true });
96
+ // Check specifically for the nested file
97
+ // We know 'design.md' is inside 'product-building' in the registry
98
+ // Wait, where is 'product-building/design.md' in the source registry provided to the test?
99
+ // In Step 1, we recursively copied 'registry' to 'tempDir/registry'.
100
+ // If the source registry has 'workflows/product-building/design.md', then sync should produce '.fraim/workflows/product-building/design.md'.
101
+ // Let's verify recursively
102
+ const countFiles = (dir) => {
103
+ let count = 0;
104
+ if (!fs_1.default.existsSync(dir))
105
+ return 0;
106
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
107
+ for (const e of entries) {
108
+ if (e.isDirectory())
109
+ count += countFiles(path_1.default.join(dir, e.name));
110
+ else if (e.name.endsWith('.md'))
111
+ count++;
112
+ }
113
+ return count;
114
+ };
115
+ const fileCount = countFiles(localWorkflowsDir);
116
+ node_assert_1.default.ok(fileCount > 0, 'No stubs generated');
117
+ // Check content of one file
118
+ const findFirstStub = (dir) => {
119
+ if (!fs_1.default.existsSync(dir))
120
+ return null;
121
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
122
+ for (const e of entries) {
123
+ if (e.isDirectory()) {
124
+ const f = findFirstStub(path_1.default.join(dir, e.name));
125
+ if (f)
126
+ return f;
127
+ }
128
+ else if (e.name.endsWith('.md'))
129
+ return path_1.default.join(dir, e.name);
130
+ }
131
+ return null;
132
+ };
133
+ const firstStubPath = findFirstStub(localWorkflowsDir);
134
+ if (firstStubPath) {
135
+ const stubContent = fs_1.default.readFileSync(firstStubPath, 'utf8');
136
+ node_assert_1.default.ok(stubContent.includes('## Intent'), 'Stub missing intent header');
137
+ const intentContent = stubContent.match(/## Intent\n([\s\S]*?)$/)?.[1]?.trim();
138
+ node_assert_1.default.ok(intentContent && intentContent !== 'No intent defined.', `Intent should be populated, found fallback in ${firstStubPath}`);
139
+ node_assert_1.default.ok(intentContent && intentContent.length > 20, `Intent too short in ${firstStubPath}`);
140
+ node_assert_1.default.ok(!stubContent.includes('Core Principles'), 'Stub should NOT have detailed principles');
141
+ node_assert_1.default.ok(stubContent.includes('DO NOT EXECUTE'), 'Stub must warn agent');
142
+ node_assert_1.default.ok(!stubContent.includes('Commit this file'), 'Stub footer should be removed');
143
+ console.log(` ✅ "fraim sync" generated ${fileCount} lightweight stubs (Verified content & structure)`);
144
+ }
145
+ else {
146
+ throw new Error('No stubs found to verify content');
147
+ }
148
+ // --- SCENARIO 3: Agent Interaction (Version Check) ---
149
+ console.log('\n [3/4] Scenario: Agent connects to MCP Server');
150
+ // Start Server in the project context
151
+ // Mock a mismatched version in package.json
152
+ fs_1.default.writeFileSync(path_1.default.join(tempDir, 'package.json'), JSON.stringify({
153
+ dependencies: { "@fraim/framework": "1.0.0" }
154
+ }));
155
+ const serverScript = path_1.default.resolve(__dirname, '../src/fraim-mcp-server.ts');
156
+ fraimProcess = (0, node_child_process_1.spawn)(process.execPath, [tsxCli, serverScript], {
157
+ cwd: tempDir,
158
+ env: { ...process.env, FRAIM_MCP_PORT: PORT.toString(), FRAIM_SKIP_INDEX_ON_START: 'true' },
159
+ stdio: 'pipe', // Quiet outcome
160
+ shell: false
161
+ });
162
+ let serverLog = '';
163
+ if (fraimProcess.stdout)
164
+ fraimProcess.stdout.on('data', d => serverLog += d.toString());
165
+ if (fraimProcess.stderr)
166
+ fraimProcess.stderr.on('data', d => serverLog += d.toString());
167
+ // Loop wait for health
168
+ let active = false;
169
+ for (let i = 0; i < 60; i++) {
170
+ try {
171
+ await axios_1.default.get(`${BASE_URL}/health`);
172
+ active = true;
173
+ break;
174
+ }
175
+ catch {
176
+ await new Promise(r => setTimeout(r, 500));
177
+ }
178
+ }
179
+ if (!active) {
180
+ console.error('❌ Server failed to start. Logs:\n', serverLog);
181
+ throw new Error('Server failed to start');
182
+ }
183
+ // Test Version Header on Root Endpoint
184
+ const mcpRes = await axios_1.default.post(`${BASE_URL}/`, {
185
+ jsonrpc: '2.0', id: 1, method: 'initialize', params: {}
186
+ }, { headers: { 'x-api-key': TEST_API_KEY } });
187
+ node_assert_1.default.ok(mcpRes.headers['x-fraim-version-notice'], 'Missing Version Notice Header');
188
+ console.log(' ✅ Server detected version mismatch and sent guidance header');
189
+ // --- SCENARIO 4: Upgrade Guidance (Tool Result) ---
190
+ console.log('\n [4/4] Scenario: Agent executes tool with outdated version');
191
+ // Call a tool (e.g., list_tools is simple)
192
+ const toolRes = await axios_1.default.post(`${BASE_URL}/`, {
193
+ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {}
194
+ }, { headers: { 'x-api-key': TEST_API_KEY } });
195
+ // The middleware injects the notice into the request.
196
+ // The handler `handleToolCall` or logic usually injects it into CONTENT.
197
+ // `tools/list` might not return 'content'. `handleToolCall` does.
198
+ // Let's call a tool that doesn't really exist or fails?
199
+ // Actually `versionCheck` middleware puts it in a header always.
200
+ // Logic in `handleToolCall` injects it into text content.
201
+ // Let's try to simulate a tool call. We need a valid tool or mocked one.
202
+ // We can check if the header persists.
203
+ node_assert_1.default.ok(toolRes.headers['x-fraim-version-notice'], 'Header missing on tool call');
204
+ console.log(' ✅ Guidance persists on tool execution');
205
+ return true;
206
+ }
207
+ catch (e) {
208
+ console.error(' ❌ User Journey Logic Failed:', e);
209
+ return false;
210
+ }
211
+ finally {
212
+ if (dbService)
213
+ await dbService.close();
214
+ if (fraimProcess?.pid) {
215
+ await new Promise((resolve) => (0, tree_kill_1.default)(fraimProcess.pid, 'SIGKILL', () => resolve()));
216
+ }
217
+ try {
218
+ fs_1.default.rmSync(tempDir, { recursive: true, force: true });
219
+ }
220
+ catch { }
221
+ }
222
+ }
223
+ async function runJourney(testCase) {
224
+ return await testCase.testFunction();
225
+ }
226
+ (0, test_utils_1.runTests)([{
227
+ name: 'Dual Discovery User Journey',
228
+ description: 'End-to-End verification of the Developer -> Agent -> Framework loop',
229
+ testFunction: testDualDiscoveryJourney,
230
+ tags: ['e2e', 'journey']
231
+ }], runJourney, 'Fraim User Journey');
@@ -0,0 +1,96 @@
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
+ exports.runTests = runTests;
7
+ const dotenv_1 = __importDefault(require("dotenv"));
8
+ dotenv_1.default.config({ override: true });
9
+ const node_test_1 = require("node:test");
10
+ // Global tracker for failed tests across all suites
11
+ const globalFailedTests = [];
12
+ // Generic test runner function that can run any type of test
13
+ async function runTests(testCases, runTestFn, testTitle) {
14
+ console.log(`🧪 Testing ${testTitle}...\n`);
15
+ // Check if we need to filter by tags
16
+ let tagsFilter = [];
17
+ console.log(`Command line arguments: ${process.argv.join(', ')}`);
18
+ // First try command line arguments
19
+ const tagsArg = process.argv.find(arg => arg && typeof arg === 'string' && arg.startsWith('--tags='));
20
+ if (tagsArg) {
21
+ const tagValue = tagsArg.split('=')[1];
22
+ if (tagValue) {
23
+ tagsFilter = tagValue.split(',');
24
+ console.log(`Filtering tests by tags (from CLI): ${tagsFilter.join(', ')}`);
25
+ }
26
+ }
27
+ // Then try environment variable (for npm scripts)
28
+ if (tagsFilter.length === 0 && process.env.TAGS) {
29
+ tagsFilter = process.env.TAGS.split(',');
30
+ console.log(`Filtering tests by tags (from ENV): ${tagsFilter.join(', ')}`);
31
+ }
32
+ // Check for exclusion tags
33
+ let excludeTags = [];
34
+ if (process.env.EXCLUDE_TAGS) {
35
+ excludeTags = process.env.EXCLUDE_TAGS.split(',');
36
+ console.log(`Excluding tests with tags (from ENV): ${excludeTags.join(', ')}`);
37
+ }
38
+ // Filter test cases by tags if specified
39
+ const testsToRun = testCases.filter(test => {
40
+ // First check inclusion filter
41
+ if (tagsFilter.length > 0) {
42
+ // Ensure test.tags exists before calling .some() on it
43
+ if (!test.tags || !test.tags.some(tag => tagsFilter.includes(tag))) {
44
+ return false;
45
+ }
46
+ }
47
+ // Then check exclusion filter
48
+ if (excludeTags.length > 0) {
49
+ // Exclude tests with specified tags
50
+ if (test.tags && test.tags.some(tag => excludeTags.includes(tag))) {
51
+ return false;
52
+ }
53
+ }
54
+ return true;
55
+ });
56
+ if (testsToRun.length === 0) {
57
+ console.log('No tests to run for suite: ' + testTitle);
58
+ return;
59
+ }
60
+ console.log(`Running ${testsToRun.length} tests${tagsFilter.length > 0 ? ` with tags: ${tagsFilter.join(', ')}` : ''}\n`);
61
+ // Use Node.js built-in test() function for each test case
62
+ // This allows the TAP reporter to properly aggregate results across all test files
63
+ for (const testCase of testsToRun) {
64
+ await (0, node_test_1.test)(testCase.name, async () => {
65
+ try {
66
+ const success = await runTestFn(testCase);
67
+ if (!success) {
68
+ const failedTestName = `${testTitle}: ${testCase.name}`;
69
+ if (!globalFailedTests.includes(failedTestName)) {
70
+ globalFailedTests.push(failedTestName);
71
+ }
72
+ throw new Error(`Test failed: ${testCase.name}`);
73
+ }
74
+ }
75
+ catch (error) {
76
+ const failedTestName = `${testTitle}: ${testCase.name}`;
77
+ if (!globalFailedTests.includes(failedTestName)) {
78
+ globalFailedTests.push(failedTestName);
79
+ }
80
+ throw error;
81
+ }
82
+ });
83
+ }
84
+ // Display comprehensive final summary
85
+ console.log(`\n🚨 FINAL TEST SUMMARY:`);
86
+ console.log(` ❌ Total Failed Tests: ${globalFailedTests.length}`);
87
+ if (globalFailedTests.length > 0) {
88
+ console.log(` 📋 Failed Test Names:`);
89
+ globalFailedTests.forEach(testName => {
90
+ console.log(` - ${testName}`);
91
+ });
92
+ }
93
+ if (globalFailedTests.length > 0) {
94
+ process.exitCode = 1;
95
+ }
96
+ }