@unifiedmemory/cli 1.3.1 → 1.3.6

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.
@@ -0,0 +1,92 @@
1
+ name: Test and Publish
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ node-version: [18, 20, 22]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Setup Node.js ${{ matrix.node-version }}
20
+ uses: actions/setup-node@v4
21
+ with:
22
+ node-version: ${{ matrix.node-version }}
23
+ cache: 'npm'
24
+
25
+ - name: Install dependencies
26
+ run: npm ci
27
+
28
+ - name: Run tests
29
+ run: npm run test:ci
30
+ env:
31
+ CI: true
32
+
33
+ - name: Upload test results
34
+ uses: actions/upload-artifact@v4
35
+ if: always()
36
+ with:
37
+ name: test-results-node-${{ matrix.node-version }}
38
+ path: test-results.xml
39
+
40
+ publish:
41
+ needs: test
42
+ runs-on: ubuntu-latest
43
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
44
+ permissions:
45
+ contents: write
46
+
47
+ steps:
48
+ - uses: actions/checkout@v4
49
+
50
+ - name: Setup Node.js
51
+ uses: actions/setup-node@v4
52
+ with:
53
+ node-version: 24
54
+ registry-url: 'https://registry.npmjs.org'
55
+ cache: 'npm'
56
+
57
+ - name: Install dependencies
58
+ run: npm ci
59
+
60
+ - name: Check if version changed
61
+ id: version-check
62
+ run: |
63
+ CURRENT_VERSION=$(node -p "require('./package.json').version")
64
+ NPM_VERSION=$(npm view @unifiedmemory/cli version 2>/dev/null || echo "0.0.0")
65
+ echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
66
+ echo "published=$NPM_VERSION" >> $GITHUB_OUTPUT
67
+ if [ "$CURRENT_VERSION" != "$NPM_VERSION" ]; then
68
+ echo "version_changed=true" >> $GITHUB_OUTPUT
69
+ echo "✅ Version changed: $NPM_VERSION → $CURRENT_VERSION"
70
+ else
71
+ echo "version_changed=false" >> $GITHUB_OUTPUT
72
+ echo "ℹ️ Version unchanged ($CURRENT_VERSION) - skipping publish"
73
+ fi
74
+
75
+ - name: Publish to npm
76
+ if: steps.version-check.outputs.version_changed == 'true'
77
+ run: npm publish --access public
78
+ env:
79
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
80
+
81
+ - name: Create Git tag
82
+ if: steps.version-check.outputs.version_changed == 'true'
83
+ run: |
84
+ git config user.name "github-actions[bot]"
85
+ git config user.email "github-actions[bot]@users.noreply.github.com"
86
+ git tag v${{ steps.version-check.outputs.current }}
87
+ git push origin v${{ steps.version-check.outputs.current }}
88
+
89
+ - name: Skip publish notification
90
+ if: steps.version-check.outputs.version_changed == 'false'
91
+ run: |
92
+ echo "::notice::Skipped npm publish - version ${{ steps.version-check.outputs.current }} already published"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unifiedmemory/cli",
3
- "version": "1.3.1",
3
+ "version": "1.3.6",
4
4
  "description": "UnifiedMemory CLI - AI code assistant integration",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -9,7 +9,13 @@
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node index.js",
12
- "build": "pkg . --targets node18-linux-x64,node18-macos-x64,node18-win-x64 --output dist/um"
12
+ "build": "pkg . --targets node18-linux-x64,node18-macos-x64,node18-win-x64 --output dist/um",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:coverage": "vitest run --coverage",
16
+ "test:unit": "vitest run tests/unit",
17
+ "test:integration": "vitest run tests/integration",
18
+ "test:ci": "vitest run --reporter=junit --outputFile=test-results.xml"
13
19
  },
14
20
  "keywords": [
15
21
  "ai",
@@ -42,10 +48,16 @@
42
48
  "open": "^10.0.3"
43
49
  },
44
50
  "devDependencies": {
45
- "pkg": "^5.8.1"
51
+ "@vitest/coverage-v8": "^3.0.0",
52
+ "msw": "^2.7.0",
53
+ "pkg": "^5.8.1",
54
+ "vitest": "^3.0.0"
46
55
  },
47
56
  "repository": {
48
57
  "type": "git",
49
- "url": "https://github.com/unifiedmemory/um-cli.git"
58
+ "url": "https://github.com/Episodic-Solutions/um-cli.git"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
50
62
  }
51
63
  }
@@ -0,0 +1,20 @@
1
+ {
2
+ "accessToken": "mock_expired_access_token",
3
+ "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMzQ1Njc4OSIsImVtYWlsIjoidGVzdEB0ZXN0LmNvbSIsImV4cCI6MTYwMDAwMDAwMCwiaWF0IjoxNTk5OTk2NDAwLCJzaWQiOiJzZXNzXzEyMzQ1Njc4OSJ9.mock_signature",
4
+ "refresh_token": "mock_refresh_token_for_expired",
5
+ "tokenType": "Bearer",
6
+ "expiresIn": 3600,
7
+ "receivedAt": 1599996400000,
8
+ "decoded": {
9
+ "sub": "user_123456789",
10
+ "email": "test@test.com",
11
+ "exp": 1600000000,
12
+ "iat": 1599996400,
13
+ "sid": "sess_123456789"
14
+ },
15
+ "selectedOrg": {
16
+ "id": "org_456789012",
17
+ "name": "Test Organization",
18
+ "role": "admin"
19
+ }
20
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "accessToken": "mock_access_token_abc123",
3
+ "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMzQ1Njc4OSIsImVtYWlsIjoidGVzdEB0ZXN0LmNvbSIsImV4cCI6OTk5OTk5OTk5OSwiaWF0IjoxNjAwMDAwMDAwLCJzaWQiOiJzZXNzXzEyMzQ1Njc4OSIsIm9yZ19pZCI6Im9yZ180NTY3ODkwMTIifQ.mock_signature",
4
+ "refresh_token": "mock_refresh_token_xyz789",
5
+ "tokenType": "Bearer",
6
+ "expiresIn": 3600,
7
+ "receivedAt": 1700000000000,
8
+ "decoded": {
9
+ "sub": "user_123456789",
10
+ "email": "test@test.com",
11
+ "exp": 9999999999,
12
+ "iat": 1600000000,
13
+ "sid": "sess_123456789",
14
+ "org_id": "org_456789012"
15
+ },
16
+ "selectedOrg": {
17
+ "id": "org_456789012",
18
+ "name": "Test Organization",
19
+ "role": "admin"
20
+ }
21
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": "1.0",
3
+ "org_id": "org_456789012",
4
+ "project_id": "proj_987654321",
5
+ "project_name": "test-project",
6
+ "api_url": "https://api.test.unifiedmemory.ai",
7
+ "created_at": "2024-01-01T00:00:00.000Z",
8
+ "mcp_config": {
9
+ "inject_headers": true,
10
+ "auto_context": true
11
+ }
12
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * MSW (Mock Service Worker) handlers for API mocking
3
+ *
4
+ * These handlers intercept HTTP requests during tests and return
5
+ * predefined responses without hitting real APIs.
6
+ */
7
+
8
+ import { http, HttpResponse } from 'msw';
9
+
10
+ const API_BASE_URL = 'https://rose-asp-main-1c0b114.d2.zuplo.dev';
11
+
12
+ // Sample project data
13
+ const mockProjects = [
14
+ {
15
+ id: 'proj_001',
16
+ name: 'project-one',
17
+ display_name: 'Project One',
18
+ slug: 'project-one',
19
+ description: 'First test project',
20
+ created_at: '2024-01-01T00:00:00.000Z',
21
+ },
22
+ {
23
+ id: 'proj_002',
24
+ name: 'project-two',
25
+ display_name: 'Project Two',
26
+ slug: 'project-two',
27
+ description: 'Second test project',
28
+ created_at: '2024-01-02T00:00:00.000Z',
29
+ },
30
+ ];
31
+
32
+ // Sample MCP tools data
33
+ const mockMCPTools = [
34
+ {
35
+ name: 'create_note',
36
+ description: 'Create a new note in the vault',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ content: { type: 'string', description: 'Note content' },
41
+ pathParams: {
42
+ type: 'object',
43
+ properties: {
44
+ org: { type: 'string' },
45
+ proj: { type: 'string' },
46
+ },
47
+ required: ['org', 'proj'],
48
+ },
49
+ headers: {
50
+ type: 'object',
51
+ properties: {
52
+ 'X-Org-Id': { type: 'string' },
53
+ },
54
+ },
55
+ },
56
+ required: ['content', 'pathParams'],
57
+ },
58
+ },
59
+ {
60
+ name: 'search_notes',
61
+ description: 'Search notes in the vault',
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {
65
+ query: { type: 'string', description: 'Search query' },
66
+ pathParams: {
67
+ type: 'object',
68
+ properties: {
69
+ org: { type: 'string' },
70
+ proj: { type: 'string' },
71
+ },
72
+ required: ['org', 'proj'],
73
+ },
74
+ },
75
+ required: ['query', 'pathParams'],
76
+ },
77
+ },
78
+ ];
79
+
80
+ /**
81
+ * Default API handlers for common endpoints
82
+ */
83
+ export const handlers = [
84
+ // GET /v1/orgs/:org/projects - List projects
85
+ http.get(`${API_BASE_URL}/v1/orgs/:org/projects`, ({ request, params }) => {
86
+ const authHeader = request.headers.get('Authorization');
87
+
88
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
89
+ return HttpResponse.json(
90
+ { error: 'Unauthorized', message: 'Missing or invalid authorization header' },
91
+ { status: 401 }
92
+ );
93
+ }
94
+
95
+ return HttpResponse.json({ items: mockProjects });
96
+ }),
97
+
98
+ // POST /v1/orgs/:org/projects - Create project
99
+ http.post(`${API_BASE_URL}/v1/orgs/:org/projects`, async ({ request, params }) => {
100
+ const authHeader = request.headers.get('Authorization');
101
+
102
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
103
+ return HttpResponse.json(
104
+ { error: 'Unauthorized', message: 'Missing or invalid authorization header' },
105
+ { status: 401 }
106
+ );
107
+ }
108
+
109
+ const body = await request.json();
110
+
111
+ return HttpResponse.json({
112
+ id: `proj_${Date.now()}`,
113
+ name: body.display_name.toLowerCase().replace(/\s+/g, '-'),
114
+ display_name: body.display_name,
115
+ slug: body.display_name.toLowerCase().replace(/\s+/g, '-'),
116
+ description: body.description || '',
117
+ created_at: new Date().toISOString(),
118
+ });
119
+ }),
120
+
121
+ // POST /mcp - MCP endpoint for tools/list
122
+ http.post(`${API_BASE_URL}/mcp`, async ({ request }) => {
123
+ const body = await request.json();
124
+
125
+ if (body.method === 'tools/list') {
126
+ return HttpResponse.json({
127
+ jsonrpc: '2.0',
128
+ id: body.id,
129
+ result: {
130
+ tools: mockMCPTools,
131
+ },
132
+ });
133
+ }
134
+
135
+ if (body.method === 'tools/call') {
136
+ return HttpResponse.json({
137
+ jsonrpc: '2.0',
138
+ id: body.id,
139
+ result: {
140
+ content: [
141
+ {
142
+ type: 'text',
143
+ text: `Tool ${body.params.name} executed successfully`,
144
+ },
145
+ ],
146
+ },
147
+ });
148
+ }
149
+
150
+ return HttpResponse.json(
151
+ {
152
+ jsonrpc: '2.0',
153
+ id: body.id,
154
+ error: { code: -32601, message: 'Method not found' },
155
+ },
156
+ { status: 400 }
157
+ );
158
+ }),
159
+
160
+ // GET /health - Health check
161
+ http.get(`${API_BASE_URL}/health`, () => {
162
+ return HttpResponse.json({ status: 'healthy' });
163
+ }),
164
+ ];
165
+
166
+ /**
167
+ * Handler that returns 401 for all requests (unauthorized)
168
+ */
169
+ export const unauthorizedHandlers = [
170
+ http.all(`${API_BASE_URL}/*`, () => {
171
+ return HttpResponse.json(
172
+ { error: 'Unauthorized', message: 'Token expired or invalid' },
173
+ { status: 401 }
174
+ );
175
+ }),
176
+ ];
177
+
178
+ /**
179
+ * Handler that returns 500 for all requests (server error)
180
+ */
181
+ export const serverErrorHandlers = [
182
+ http.all(`${API_BASE_URL}/*`, () => {
183
+ return HttpResponse.json(
184
+ { error: 'Internal Server Error', message: 'Something went wrong' },
185
+ { status: 500 }
186
+ );
187
+ }),
188
+ ];
189
+
190
+ /**
191
+ * Handler that simulates network failure
192
+ */
193
+ export const networkErrorHandlers = [
194
+ http.all(`${API_BASE_URL}/*`, () => {
195
+ return HttpResponse.error();
196
+ }),
197
+ ];
198
+
199
+ // Export mock data for use in tests
200
+ export { mockProjects, mockMCPTools };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Mock for token-storage.js
3
+ *
4
+ * Provides mock implementations of token storage functions
5
+ * that don't interact with the real filesystem.
6
+ */
7
+
8
+ import { vi } from 'vitest';
9
+ import mockAuthData from '../fixtures/mock-auth.json' with { type: 'json' };
10
+ import expiredAuthData from '../fixtures/expired-auth.json' with { type: 'json' };
11
+
12
+ /**
13
+ * Creates a mock token storage module with configurable behavior
14
+ * @param {Object} options - Configuration options
15
+ * @param {Object|null} options.initialToken - Initial token data to return (use null for no token)
16
+ * @param {boolean} options.expired - Whether to use expired token fixture
17
+ * @param {boolean} options.empty - Whether to start with no token
18
+ * @returns {Object} Mock token storage functions
19
+ */
20
+ export function createTokenStorageMock(options = {}) {
21
+ let tokenData;
22
+
23
+ if (options.empty) {
24
+ tokenData = null;
25
+ } else if (options.expired) {
26
+ tokenData = { ...expiredAuthData };
27
+ } else if (options.initialToken !== undefined) {
28
+ tokenData = options.initialToken;
29
+ } else {
30
+ tokenData = { ...mockAuthData };
31
+ }
32
+
33
+ return {
34
+ saveToken: vi.fn((data) => {
35
+ tokenData = data;
36
+ }),
37
+
38
+ getToken: vi.fn(() => tokenData),
39
+
40
+ clearToken: vi.fn(() => {
41
+ tokenData = null;
42
+ }),
43
+
44
+ updateSelectedOrg: vi.fn((orgData) => {
45
+ if (tokenData) {
46
+ tokenData.selectedOrg = orgData;
47
+ } else {
48
+ throw new Error('No token found. Please login first.');
49
+ }
50
+ }),
51
+
52
+ getSelectedOrg: vi.fn(() => tokenData?.selectedOrg || null),
53
+
54
+ // Helper to reset state between tests
55
+ _reset: (newData = null) => {
56
+ tokenData = newData ?? { ...mockAuthData };
57
+ },
58
+
59
+ // Helper to get current state for assertions
60
+ _getState: () => tokenData,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Creates mock token storage that returns null (no token)
66
+ */
67
+ export function createEmptyTokenStorageMock() {
68
+ return createTokenStorageMock({ empty: true });
69
+ }
70
+
71
+ /**
72
+ * Creates mock token storage with expired token
73
+ */
74
+ export function createExpiredTokenStorageMock() {
75
+ return createTokenStorageMock({ expired: true });
76
+ }
77
+
78
+ // Export fixtures for direct use in tests
79
+ export { mockAuthData, expiredAuthData };
package/tests/setup.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Vitest Global Test Setup
3
+ *
4
+ * This file runs before all tests and sets up the test environment.
5
+ * It configures mocks and ensures tests don't interact with real auth.
6
+ */
7
+
8
+ import { vi, beforeAll, afterAll, afterEach } from 'vitest';
9
+
10
+ // Ensure we're in test mode
11
+ process.env.NODE_ENV = 'test';
12
+
13
+ // Mock console.error to reduce noise in tests (optional)
14
+ // Uncomment if you want to suppress console output during tests:
15
+ // vi.spyOn(console, 'error').mockImplementation(() => {});
16
+ // vi.spyOn(console, 'log').mockImplementation(() => {});
17
+
18
+ beforeAll(() => {
19
+ // Global setup before all tests
20
+ });
21
+
22
+ afterEach(() => {
23
+ // Clean up mocks after each test
24
+ vi.restoreAllMocks();
25
+ });
26
+
27
+ afterAll(() => {
28
+ // Global cleanup after all tests
29
+ });
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Unit tests for lib/config.js
3
+ *
4
+ * Tests configuration loading and validation.
5
+ * Note: The config module loads environment variables at import time,
6
+ * so we test the validateConfig function and config structure.
7
+ */
8
+
9
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
+
11
+ describe('config', () => {
12
+ // Store original env vars
13
+ const originalEnv = { ...process.env };
14
+
15
+ beforeEach(() => {
16
+ // Reset modules to get fresh config import
17
+ vi.resetModules();
18
+ });
19
+
20
+ afterEach(() => {
21
+ // Restore original environment
22
+ process.env = { ...originalEnv };
23
+ vi.resetModules();
24
+ });
25
+
26
+ describe('default configuration', () => {
27
+ it('should have default values when no environment variables are set', async () => {
28
+ // Clear relevant env vars
29
+ delete process.env.CLERK_CLIENT_ID;
30
+ delete process.env.CLERK_DOMAIN;
31
+ delete process.env.API_ENDPOINT;
32
+ delete process.env.REDIRECT_URI;
33
+ delete process.env.PORT;
34
+
35
+ // Fresh import to get defaults
36
+ const { config } = await import('../../lib/config.js');
37
+
38
+ expect(config.clerkClientId).toBe('nULlnomaKB9rRGP2');
39
+ expect(config.clerkDomain).toBe('clear-caiman-45.clerk.accounts.dev');
40
+ expect(config.apiEndpoint).toBe('https://rose-asp-main-1c0b114.d2.zuplo.dev');
41
+ expect(config.redirectUri).toBe('http://localhost:3333/callback');
42
+ expect(config.port).toBe(3333);
43
+ });
44
+
45
+ it('should have clerkClientSecret as undefined by default', async () => {
46
+ delete process.env.CLERK_CLIENT_SECRET;
47
+
48
+ const { config } = await import('../../lib/config.js');
49
+
50
+ expect(config.clerkClientSecret).toBeUndefined();
51
+ });
52
+ });
53
+
54
+ describe('environment variable overrides', () => {
55
+ it('should use CLERK_CLIENT_ID from environment', async () => {
56
+ process.env.CLERK_CLIENT_ID = 'custom_client_id';
57
+
58
+ const { config } = await import('../../lib/config.js');
59
+
60
+ expect(config.clerkClientId).toBe('custom_client_id');
61
+ });
62
+
63
+ it('should use CLERK_DOMAIN from environment', async () => {
64
+ process.env.CLERK_DOMAIN = 'custom.clerk.dev';
65
+
66
+ const { config } = await import('../../lib/config.js');
67
+
68
+ expect(config.clerkDomain).toBe('custom.clerk.dev');
69
+ });
70
+
71
+ it('should use API_ENDPOINT from environment', async () => {
72
+ process.env.API_ENDPOINT = 'https://custom-api.example.com';
73
+
74
+ const { config } = await import('../../lib/config.js');
75
+
76
+ expect(config.apiEndpoint).toBe('https://custom-api.example.com');
77
+ });
78
+
79
+ it('should use REDIRECT_URI from environment', async () => {
80
+ process.env.REDIRECT_URI = 'http://localhost:8080/callback';
81
+
82
+ const { config } = await import('../../lib/config.js');
83
+
84
+ expect(config.redirectUri).toBe('http://localhost:8080/callback');
85
+ });
86
+
87
+ it('should use PORT from environment and parse as integer', async () => {
88
+ process.env.PORT = '8080';
89
+
90
+ const { config } = await import('../../lib/config.js');
91
+
92
+ expect(config.port).toBe(8080);
93
+ expect(typeof config.port).toBe('number');
94
+ });
95
+
96
+ it('should use CLERK_CLIENT_SECRET from environment', async () => {
97
+ process.env.CLERK_CLIENT_SECRET = 'secret_key_123';
98
+
99
+ const { config } = await import('../../lib/config.js');
100
+
101
+ expect(config.clerkClientSecret).toBe('secret_key_123');
102
+ });
103
+ });
104
+
105
+ describe('validateConfig', () => {
106
+ it('should return true for valid default configuration', async () => {
107
+ const { validateConfig } = await import('../../lib/config.js');
108
+
109
+ expect(validateConfig()).toBe(true);
110
+ });
111
+
112
+ it('should throw error for invalid API_ENDPOINT URL', async () => {
113
+ process.env.API_ENDPOINT = 'not-a-valid-url';
114
+
115
+ const { validateConfig } = await import('../../lib/config.js');
116
+
117
+ expect(() => validateConfig()).toThrow('API_ENDPOINT must be a valid URL');
118
+ });
119
+
120
+ it('should use default when CLERK_DOMAIN is empty (falsy falls back to default)', async () => {
121
+ process.env.CLERK_DOMAIN = '';
122
+
123
+ const { config, validateConfig } = await import('../../lib/config.js');
124
+
125
+ // Empty string is falsy, so it falls back to default
126
+ expect(config.clerkDomain).toBe('clear-caiman-45.clerk.accounts.dev');
127
+ expect(validateConfig()).toBe(true);
128
+ });
129
+
130
+ it('should throw error when CLERK_DOMAIN is whitespace only', async () => {
131
+ // Whitespace is truthy, so it will be used instead of default
132
+ process.env.CLERK_DOMAIN = ' ';
133
+
134
+ const { validateConfig } = await import('../../lib/config.js');
135
+
136
+ expect(() => validateConfig()).toThrow('CLERK_DOMAIN cannot be empty');
137
+ });
138
+
139
+ it('should use default when CLERK_CLIENT_ID is empty (falsy falls back to default)', async () => {
140
+ process.env.CLERK_CLIENT_ID = '';
141
+
142
+ const { config, validateConfig } = await import('../../lib/config.js');
143
+
144
+ // Empty string is falsy, so it falls back to default
145
+ expect(config.clerkClientId).toBe('nULlnomaKB9rRGP2');
146
+ expect(validateConfig()).toBe(true);
147
+ });
148
+
149
+ it('should accept valid HTTPS URL for API_ENDPOINT', async () => {
150
+ process.env.API_ENDPOINT = 'https://api.example.com/v1';
151
+
152
+ const { validateConfig } = await import('../../lib/config.js');
153
+
154
+ expect(validateConfig()).toBe(true);
155
+ });
156
+
157
+ it('should accept valid HTTP URL for API_ENDPOINT (development)', async () => {
158
+ process.env.API_ENDPOINT = 'http://localhost:8000';
159
+
160
+ const { validateConfig } = await import('../../lib/config.js');
161
+
162
+ expect(validateConfig()).toBe(true);
163
+ });
164
+ });
165
+ });