@unifiedmemory/cli 1.3.1 → 1.3.7
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/test-and-publish.yml +96 -0
- package/index.js +8 -2
- package/lib/welcome.js +32 -4
- package/package.json +16 -4
- package/tests/fixtures/expired-auth.json +20 -0
- package/tests/fixtures/mock-auth.json +21 -0
- package/tests/fixtures/mock-project-config.json +12 -0
- package/tests/mocks/api.mock.js +200 -0
- package/tests/mocks/token-storage.mock.js +79 -0
- package/tests/setup.js +29 -0
- package/tests/unit/config.test.js +165 -0
- package/tests/unit/jwt-utils.test.js +217 -0
- package/tests/unit/mcp-proxy.test.js +459 -0
- package/tests/unit/provider-detector.test.js +344 -0
- package/tests/unit/token-storage.test.js +138 -0
- package/vitest.config.js +37 -0
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
if ! git ls-remote --tags origin | grep -q "refs/tags/v${{ steps.version-check.outputs.current }}"; then
|
|
87
|
+
git tag v${{ steps.version-check.outputs.current }}
|
|
88
|
+
git push origin v${{ steps.version-check.outputs.current }}
|
|
89
|
+
else
|
|
90
|
+
echo "Tag v${{ steps.version-check.outputs.current }} already exists, skipping"
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
- name: Skip publish notification
|
|
94
|
+
if: steps.version-check.outputs.version_changed == 'false'
|
|
95
|
+
run: |
|
|
96
|
+
echo "::notice::Skipped npm publish - version ${{ steps.version-check.outputs.current }} already published"
|
package/index.js
CHANGED
|
@@ -4,6 +4,9 @@ import { Command } from 'commander';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import fs from 'fs-extra';
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
7
10
|
import { login } from './commands/login.js';
|
|
8
11
|
import { init } from './commands/init.js';
|
|
9
12
|
import { switchOrg, showOrg } from './commands/org.js';
|
|
@@ -13,12 +16,15 @@ import { getSelectedOrg } from './lib/token-storage.js';
|
|
|
13
16
|
import { loadAndRefreshToken } from './lib/token-validation.js';
|
|
14
17
|
import { showWelcome } from './lib/welcome.js';
|
|
15
18
|
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
|
|
21
|
+
|
|
16
22
|
const program = new Command();
|
|
17
23
|
|
|
18
24
|
program
|
|
19
25
|
.name('um')
|
|
20
26
|
.description('UnifiedMemory CLI - AI code assistant integration')
|
|
21
|
-
.version(
|
|
27
|
+
.version(packageJson.version);
|
|
22
28
|
|
|
23
29
|
// Unified command (primary)
|
|
24
30
|
program
|
|
@@ -199,7 +205,7 @@ noteCommand
|
|
|
199
205
|
|
|
200
206
|
// Show welcome splash if no command provided
|
|
201
207
|
if (process.argv.length === 2) {
|
|
202
|
-
showWelcome();
|
|
208
|
+
await showWelcome();
|
|
203
209
|
program.help();
|
|
204
210
|
}
|
|
205
211
|
|
package/lib/welcome.js
CHANGED
|
@@ -2,22 +2,50 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { readFileSync } from 'fs';
|
|
3
3
|
import { dirname, join } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
+
import axios from 'axios';
|
|
5
6
|
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
8
9
|
const version = packageJson.version;
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const PACKAGE_NAME = '@unifiedmemory/cli';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check npm registry for latest version (non-blocking)
|
|
15
|
+
* @returns {Promise<{latest: string, isOutdated: boolean} | null>}
|
|
16
|
+
*/
|
|
17
|
+
export async function checkLatestVersion() {
|
|
18
|
+
try {
|
|
19
|
+
const response = await axios.get(
|
|
20
|
+
`https://registry.npmjs.org/${PACKAGE_NAME}/latest`,
|
|
21
|
+
{ timeout: 2000 }
|
|
22
|
+
);
|
|
23
|
+
const latest = response.data.version;
|
|
24
|
+
const isOutdated = latest !== version;
|
|
25
|
+
return { latest, isOutdated };
|
|
26
|
+
} catch {
|
|
27
|
+
return null; // Silently fail - don't block CLI
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function showWelcome() {
|
|
14
32
|
const gray = chalk.gray;
|
|
15
33
|
const white = chalk.white;
|
|
34
|
+
const yellow = chalk.yellow;
|
|
16
35
|
|
|
17
36
|
console.log('');
|
|
18
37
|
console.log(white.bold(' UnifiedMemory CLI ') + gray(`v${version}`));
|
|
19
38
|
console.log(gray(' AI-powered knowledge assistant'));
|
|
20
39
|
console.log('');
|
|
40
|
+
|
|
41
|
+
// Check for updates (non-blocking with short timeout)
|
|
42
|
+
const versionInfo = await checkLatestVersion();
|
|
43
|
+
if (versionInfo?.isOutdated) {
|
|
44
|
+
console.log(yellow(' ⚠ Update available: ') + white(`v${versionInfo.latest}`));
|
|
45
|
+
console.log(gray(' Run: ') + white('npm install -g @unifiedmemory/cli'));
|
|
46
|
+
console.log('');
|
|
47
|
+
}
|
|
48
|
+
|
|
21
49
|
console.log(white(' Quick Start:'));
|
|
22
50
|
console.log(gray(' um init ') + white('Initialize in current project'));
|
|
23
51
|
console.log(gray(' um status ') + white('Check configuration'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unifiedmemory/cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.7",
|
|
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
|
-
"
|
|
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/
|
|
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
|
+
});
|