@memnexus-ai/cli 0.1.0
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/.env.example +13 -0
- package/.eslintrc.js +24 -0
- package/.github/ISSUE_TEMPLATE/phase-1-foundation.md +1078 -0
- package/.github/workflows/publish.yml +277 -0
- package/.github/workflows/test-app-token.yml +54 -0
- package/.npmrc.backup +3 -0
- package/.npmrc.example +6 -0
- package/.prettierignore +4 -0
- package/.prettierrc +8 -0
- package/CHANGELOG.md +138 -0
- package/PLATFORM_TESTING.md +243 -0
- package/README.md +986 -0
- package/RELEASE.md +428 -0
- package/RELEASE_READINESS.md +253 -0
- package/USAGE.md +1373 -0
- package/bin/mx.js +2 -0
- package/dist/commands/apikeys.d.ts +7 -0
- package/dist/commands/apikeys.d.ts.map +1 -0
- package/dist/commands/apikeys.js +133 -0
- package/dist/commands/apikeys.js.map +1 -0
- package/dist/commands/artifacts.d.ts +7 -0
- package/dist/commands/artifacts.d.ts.map +1 -0
- package/dist/commands/artifacts.js +277 -0
- package/dist/commands/artifacts.js.map +1 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +119 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/communities.d.ts +7 -0
- package/dist/commands/communities.d.ts.map +1 -0
- package/dist/commands/communities.js +137 -0
- package/dist/commands/communities.js.map +1 -0
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +138 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/conversations.d.ts +7 -0
- package/dist/commands/conversations.d.ts.map +1 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/conversations.js.map +1 -0
- package/dist/commands/facts.d.ts +7 -0
- package/dist/commands/facts.d.ts.map +1 -0
- package/dist/commands/facts.js +298 -0
- package/dist/commands/facts.js.map +1 -0
- package/dist/commands/graphrag.d.ts +7 -0
- package/dist/commands/graphrag.d.ts.map +1 -0
- package/dist/commands/graphrag.js +139 -0
- package/dist/commands/graphrag.js.map +1 -0
- package/dist/commands/memories.d.ts +7 -0
- package/dist/commands/memories.d.ts.map +1 -0
- package/dist/commands/memories.js +304 -0
- package/dist/commands/memories.js.map +1 -0
- package/dist/commands/patterns.d.ts +7 -0
- package/dist/commands/patterns.d.ts.map +1 -0
- package/dist/commands/patterns.js +227 -0
- package/dist/commands/patterns.js.map +1 -0
- package/dist/commands/system.d.ts +7 -0
- package/dist/commands/system.d.ts.map +1 -0
- package/dist/commands/system.js +97 -0
- package/dist/commands/system.js.map +1 -0
- package/dist/commands/topics.d.ts +7 -0
- package/dist/commands/topics.d.ts.map +1 -0
- package/dist/commands/topics.js +314 -0
- package/dist/commands/topics.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +29 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +64 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/auth.d.ts +10 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +47 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/config.d.ts +19 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +59 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/errors.d.ts +7 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +133 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/formatters.d.ts +12 -0
- package/dist/lib/formatters.d.ts.map +1 -0
- package/dist/lib/formatters.js +103 -0
- package/dist/lib/formatters.js.map +1 -0
- package/dist/lib/spinner.d.ts +54 -0
- package/dist/lib/spinner.d.ts.map +1 -0
- package/dist/lib/spinner.js +108 -0
- package/dist/lib/spinner.js.map +1 -0
- package/dist/lib/validators.d.ts +92 -0
- package/dist/lib/validators.d.ts.map +1 -0
- package/dist/lib/validators.js +257 -0
- package/dist/lib/validators.js.map +1 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/docs/README.md +219 -0
- package/docs/code-generation-strategy.md +560 -0
- package/docs/prd.md +748 -0
- package/docs/sync-strategy.md +533 -0
- package/jest.config.js +30 -0
- package/package.json +67 -0
- package/scripts/install-deps.sh +38 -0
- package/src/commands/apikeys.ts +144 -0
- package/src/commands/artifacts.ts +296 -0
- package/src/commands/auth.ts +122 -0
- package/src/commands/communities.ts +153 -0
- package/src/commands/config.ts +144 -0
- package/src/commands/conversations.ts +176 -0
- package/src/commands/facts.ts +320 -0
- package/src/commands/graphrag.ts +149 -0
- package/src/commands/memories.ts +332 -0
- package/src/commands/patterns.ts +251 -0
- package/src/commands/system.ts +102 -0
- package/src/commands/topics.ts +354 -0
- package/src/index.ts +43 -0
- package/src/lib/api-client.ts +68 -0
- package/src/lib/auth.ts +42 -0
- package/src/lib/config.ts +68 -0
- package/src/lib/errors.ts +143 -0
- package/src/lib/formatters.ts +123 -0
- package/src/lib/spinner.ts +113 -0
- package/src/lib/validators.ts +302 -0
- package/src/types/index.ts +17 -0
- package/tests/__mocks__/chalk.ts +16 -0
- package/tests/__mocks__/cli-table3.ts +37 -0
- package/tests/__mocks__/configstore.ts +38 -0
- package/tests/commands/apikeys.test.ts +179 -0
- package/tests/commands/artifacts.test.ts +194 -0
- package/tests/commands/auth.test.ts +120 -0
- package/tests/commands/communities.test.ts +154 -0
- package/tests/commands/config.test.ts +154 -0
- package/tests/commands/conversations.test.ts +136 -0
- package/tests/commands/facts.test.ts +210 -0
- package/tests/commands/graphrag.test.ts +194 -0
- package/tests/commands/memories.test.ts +215 -0
- package/tests/commands/patterns.test.ts +201 -0
- package/tests/commands/system.test.ts +172 -0
- package/tests/commands/topics.test.ts +274 -0
- package/tests/lib/auth.test.ts +77 -0
- package/tests/lib/config.test.ts +50 -0
- package/tests/lib/errors.test.ts +126 -0
- package/tests/lib/formatters.test.ts +87 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { getApiClient } from '../../src/lib/api-client';
|
|
2
|
+
|
|
3
|
+
// Mock the API client
|
|
4
|
+
jest.mock('../../src/lib/api-client');
|
|
5
|
+
|
|
6
|
+
// Mock chalk
|
|
7
|
+
jest.mock('chalk', () => ({
|
|
8
|
+
default: {
|
|
9
|
+
green: (str: string) => str,
|
|
10
|
+
red: (str: string) => str,
|
|
11
|
+
yellow: (str: string) => str,
|
|
12
|
+
cyan: (str: string) => str,
|
|
13
|
+
gray: (str: string) => str,
|
|
14
|
+
},
|
|
15
|
+
green: (str: string) => str,
|
|
16
|
+
red: (str: string) => str,
|
|
17
|
+
yellow: (str: string) => str,
|
|
18
|
+
cyan: (str: string) => str,
|
|
19
|
+
gray: (str: string) => str,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// Mock ora
|
|
23
|
+
jest.mock('ora', () => {
|
|
24
|
+
return jest.fn(() => ({
|
|
25
|
+
start: jest.fn(function() { return this; }),
|
|
26
|
+
succeed: jest.fn(function() { return this; }),
|
|
27
|
+
fail: jest.fn(function() { return this; }),
|
|
28
|
+
}));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('System Commands', () => {
|
|
32
|
+
let mockClient: any;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
|
|
37
|
+
mockClient = {
|
|
38
|
+
system: {
|
|
39
|
+
health: jest.fn(),
|
|
40
|
+
status: jest.fn(),
|
|
41
|
+
stats: jest.fn(),
|
|
42
|
+
clearCache: jest.fn(),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
(getApiClient as jest.Mock).mockReturnValue(mockClient);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('system health', () => {
|
|
50
|
+
it('should check system health', async () => {
|
|
51
|
+
const mockResult = {
|
|
52
|
+
status: 'healthy',
|
|
53
|
+
components: {
|
|
54
|
+
database: { status: 'healthy' },
|
|
55
|
+
cache: { status: 'healthy' },
|
|
56
|
+
api: { status: 'healthy' },
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
mockClient.system.health.mockResolvedValue(mockResult);
|
|
61
|
+
|
|
62
|
+
const result = await mockClient.system.health();
|
|
63
|
+
|
|
64
|
+
expect(mockClient.system.health).toHaveBeenCalled();
|
|
65
|
+
expect(result.status).toBe('healthy');
|
|
66
|
+
expect(Object.keys(result.components)).toHaveLength(3);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should report unhealthy status', async () => {
|
|
70
|
+
const mockResult = {
|
|
71
|
+
status: 'unhealthy',
|
|
72
|
+
components: {
|
|
73
|
+
database: { status: 'healthy' },
|
|
74
|
+
cache: { status: 'unhealthy', error: 'Connection timeout' },
|
|
75
|
+
api: { status: 'healthy' },
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
mockClient.system.health.mockResolvedValue(mockResult);
|
|
80
|
+
|
|
81
|
+
const result = await mockClient.system.health();
|
|
82
|
+
|
|
83
|
+
expect(result.status).toBe('unhealthy');
|
|
84
|
+
expect(result.components.cache.status).toBe('unhealthy');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('system status', () => {
|
|
89
|
+
it('should get system status', async () => {
|
|
90
|
+
const mockResult = {
|
|
91
|
+
uptime: 86400,
|
|
92
|
+
version: '1.0.0',
|
|
93
|
+
environment: 'production',
|
|
94
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
mockClient.system.status.mockResolvedValue(mockResult);
|
|
98
|
+
|
|
99
|
+
const result = await mockClient.system.status();
|
|
100
|
+
|
|
101
|
+
expect(mockClient.system.status).toHaveBeenCalled();
|
|
102
|
+
expect(result.version).toBe('1.0.0');
|
|
103
|
+
expect(result.environment).toBe('production');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('system stats', () => {
|
|
108
|
+
it('should get system statistics', async () => {
|
|
109
|
+
const mockResult = {
|
|
110
|
+
period: '24h',
|
|
111
|
+
metrics: {
|
|
112
|
+
requestCount: 10000,
|
|
113
|
+
averageResponseTime: 150,
|
|
114
|
+
errorRate: 0.01,
|
|
115
|
+
memoryUsage: 2048,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
mockClient.system.stats.mockResolvedValue(mockResult);
|
|
120
|
+
|
|
121
|
+
const result = await mockClient.system.stats({ period: '24h' });
|
|
122
|
+
|
|
123
|
+
expect(mockClient.system.stats).toHaveBeenCalledWith({ period: '24h' });
|
|
124
|
+
expect(result.metrics.requestCount).toBe(10000);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should support different time periods', async () => {
|
|
128
|
+
const mockResult = {
|
|
129
|
+
period: '7d',
|
|
130
|
+
metrics: {},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
mockClient.system.stats.mockResolvedValue(mockResult);
|
|
134
|
+
|
|
135
|
+
const result = await mockClient.system.stats({ period: '7d' });
|
|
136
|
+
|
|
137
|
+
expect(mockClient.system.stats).toHaveBeenCalledWith({ period: '7d' });
|
|
138
|
+
expect(result.period).toBe('7d');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('system clear-cache', () => {
|
|
143
|
+
it('should clear all cache', async () => {
|
|
144
|
+
const mockResult = {
|
|
145
|
+
itemsCleared: 1000,
|
|
146
|
+
spaceFreed: '512MB',
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
mockClient.system.clearCache.mockResolvedValue(mockResult);
|
|
150
|
+
|
|
151
|
+
const result = await mockClient.system.clearCache({ cacheType: 'all' });
|
|
152
|
+
|
|
153
|
+
expect(mockClient.system.clearCache).toHaveBeenCalledWith({ cacheType: 'all' });
|
|
154
|
+
expect(result.itemsCleared).toBe(1000);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should clear specific cache type', async () => {
|
|
158
|
+
const mockResult = {
|
|
159
|
+
itemsCleared: 500,
|
|
160
|
+
spaceFreed: '256MB',
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
mockClient.system.clearCache.mockResolvedValue(mockResult);
|
|
164
|
+
|
|
165
|
+
const result = await mockClient.system.clearCache({ cacheType: 'redis' });
|
|
166
|
+
|
|
167
|
+
expect(mockClient.system.clearCache).toHaveBeenCalledWith({ cacheType: 'redis' });
|
|
168
|
+
expect(result.itemsCleared).toBe(500);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { getApiClient } from '../../src/lib/api-client';
|
|
2
|
+
|
|
3
|
+
// Mock the API client
|
|
4
|
+
jest.mock('../../src/lib/api-client');
|
|
5
|
+
|
|
6
|
+
// Mock chalk
|
|
7
|
+
jest.mock('chalk', () => ({
|
|
8
|
+
default: {
|
|
9
|
+
green: (str: string) => str,
|
|
10
|
+
red: (str: string) => str,
|
|
11
|
+
yellow: (str: string) => str,
|
|
12
|
+
cyan: (str: string) => str,
|
|
13
|
+
gray: (str: string) => str,
|
|
14
|
+
},
|
|
15
|
+
green: (str: string) => str,
|
|
16
|
+
red: (str: string) => str,
|
|
17
|
+
yellow: (str: string) => str,
|
|
18
|
+
cyan: (str: string) => str,
|
|
19
|
+
gray: (str: string) => str,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// Mock ora
|
|
23
|
+
jest.mock('ora', () => {
|
|
24
|
+
return jest.fn(() => ({
|
|
25
|
+
start: jest.fn(function() { return this; }),
|
|
26
|
+
succeed: jest.fn(function() { return this; }),
|
|
27
|
+
fail: jest.fn(function() { return this; }),
|
|
28
|
+
}));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('Topics Commands', () => {
|
|
32
|
+
let mockClient: any;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
|
|
37
|
+
mockClient = {
|
|
38
|
+
topics: {
|
|
39
|
+
list: jest.fn(),
|
|
40
|
+
get: jest.fn(),
|
|
41
|
+
merge: jest.fn(),
|
|
42
|
+
discoverRelated: jest.fn(),
|
|
43
|
+
similarity: jest.fn(),
|
|
44
|
+
findSimilar: jest.fn(),
|
|
45
|
+
cluster: jest.fn(),
|
|
46
|
+
detectCommunities: jest.fn(),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
(getApiClient as jest.Mock).mockReturnValue(mockClient);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('topics list', () => {
|
|
54
|
+
it('should list topics with default pagination', async () => {
|
|
55
|
+
const mockResponse = {
|
|
56
|
+
data: [
|
|
57
|
+
{
|
|
58
|
+
id: 'topic_1',
|
|
59
|
+
name: 'Test Topic 1',
|
|
60
|
+
memoryCount: 5,
|
|
61
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'topic_2',
|
|
65
|
+
name: 'Test Topic 2',
|
|
66
|
+
memoryCount: 3,
|
|
67
|
+
createdAt: '2024-01-02T00:00:00Z',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
pagination: {
|
|
71
|
+
limit: 20,
|
|
72
|
+
offset: 0,
|
|
73
|
+
count: 2,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
mockClient.topics.list.mockResolvedValue(mockResponse);
|
|
78
|
+
|
|
79
|
+
const result = await mockClient.topics.list({ limit: 20, offset: 0 });
|
|
80
|
+
|
|
81
|
+
expect(mockClient.topics.list).toHaveBeenCalledWith({ limit: 20, offset: 0 });
|
|
82
|
+
expect(result.data).toHaveLength(2);
|
|
83
|
+
expect(result.pagination.count).toBe(2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should handle context filter', async () => {
|
|
87
|
+
const mockResponse = {
|
|
88
|
+
data: [],
|
|
89
|
+
pagination: { limit: 20, offset: 0, count: 0 },
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
mockClient.topics.list.mockResolvedValue(mockResponse);
|
|
93
|
+
|
|
94
|
+
await mockClient.topics.list({ limit: 20, offset: 0, contextId: 'ctx_123' });
|
|
95
|
+
|
|
96
|
+
expect(mockClient.topics.list).toHaveBeenCalledWith({
|
|
97
|
+
limit: 20,
|
|
98
|
+
offset: 0,
|
|
99
|
+
contextId: 'ctx_123',
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('topics get', () => {
|
|
105
|
+
it('should get topic details', async () => {
|
|
106
|
+
const mockTopic = {
|
|
107
|
+
id: 'topic_1',
|
|
108
|
+
name: 'Test Topic',
|
|
109
|
+
memoryCount: 5,
|
|
110
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
mockClient.topics.get.mockResolvedValue(mockTopic);
|
|
114
|
+
|
|
115
|
+
const result = await mockClient.topics.get('topic_1');
|
|
116
|
+
|
|
117
|
+
expect(mockClient.topics.get).toHaveBeenCalledWith('topic_1');
|
|
118
|
+
expect(result.id).toBe('topic_1');
|
|
119
|
+
expect(result.name).toBe('Test Topic');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('topics merge', () => {
|
|
124
|
+
it('should merge two topics', async () => {
|
|
125
|
+
const mockResult = {
|
|
126
|
+
id: 'topic_2',
|
|
127
|
+
name: 'Test Topic 2',
|
|
128
|
+
memoryCount: 8,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
mockClient.topics.merge.mockResolvedValue(mockResult);
|
|
132
|
+
|
|
133
|
+
const result = await mockClient.topics.merge({
|
|
134
|
+
sourceId: 'topic_1',
|
|
135
|
+
targetId: 'topic_2',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(mockClient.topics.merge).toHaveBeenCalledWith({
|
|
139
|
+
sourceId: 'topic_1',
|
|
140
|
+
targetId: 'topic_2',
|
|
141
|
+
});
|
|
142
|
+
expect(result.memoryCount).toBe(8);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('topics discover-related', () => {
|
|
147
|
+
it('should discover related topics', async () => {
|
|
148
|
+
const mockResponse = {
|
|
149
|
+
data: [
|
|
150
|
+
{
|
|
151
|
+
id: 'topic_2',
|
|
152
|
+
name: 'Related Topic',
|
|
153
|
+
relationshipType: 'similar',
|
|
154
|
+
strength: 0.85,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
mockClient.topics.discoverRelated.mockResolvedValue(mockResponse);
|
|
160
|
+
|
|
161
|
+
const result = await mockClient.topics.discoverRelated({
|
|
162
|
+
topicId: 'topic_1',
|
|
163
|
+
limit: 20,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(mockClient.topics.discoverRelated).toHaveBeenCalledWith({
|
|
167
|
+
topicId: 'topic_1',
|
|
168
|
+
limit: 20,
|
|
169
|
+
});
|
|
170
|
+
expect(result.data).toHaveLength(1);
|
|
171
|
+
expect(result.data[0].strength).toBe(0.85);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('topics similarity', () => {
|
|
176
|
+
it('should calculate similarity between topics', async () => {
|
|
177
|
+
const mockResult = {
|
|
178
|
+
topicId1: 'topic_1',
|
|
179
|
+
topicId2: 'topic_2',
|
|
180
|
+
similarityScore: 0.78,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
mockClient.topics.similarity.mockResolvedValue(mockResult);
|
|
184
|
+
|
|
185
|
+
const result = await mockClient.topics.similarity({
|
|
186
|
+
topicId1: 'topic_1',
|
|
187
|
+
topicId2: 'topic_2',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(mockClient.topics.similarity).toHaveBeenCalledWith({
|
|
191
|
+
topicId1: 'topic_1',
|
|
192
|
+
topicId2: 'topic_2',
|
|
193
|
+
});
|
|
194
|
+
expect(result.similarityScore).toBe(0.78);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('topics find-similar', () => {
|
|
199
|
+
it('should find similar topics', async () => {
|
|
200
|
+
const mockResponse = {
|
|
201
|
+
data: [
|
|
202
|
+
{
|
|
203
|
+
id: 'topic_2',
|
|
204
|
+
name: 'Similar Topic',
|
|
205
|
+
similarityScore: 0.85,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
mockClient.topics.findSimilar.mockResolvedValue(mockResponse);
|
|
211
|
+
|
|
212
|
+
const result = await mockClient.topics.findSimilar({
|
|
213
|
+
topicId: 'topic_1',
|
|
214
|
+
threshold: 0.7,
|
|
215
|
+
limit: 20,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(mockClient.topics.findSimilar).toHaveBeenCalledWith({
|
|
219
|
+
topicId: 'topic_1',
|
|
220
|
+
threshold: 0.7,
|
|
221
|
+
limit: 20,
|
|
222
|
+
});
|
|
223
|
+
expect(result.data).toHaveLength(1);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('topics cluster', () => {
|
|
228
|
+
it('should cluster topics', async () => {
|
|
229
|
+
const mockResult = {
|
|
230
|
+
clusters: [
|
|
231
|
+
{ id: 0, topics: ['topic_1', 'topic_2'] },
|
|
232
|
+
{ id: 1, topics: ['topic_3', 'topic_4'] },
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
mockClient.topics.cluster.mockResolvedValue(mockResult);
|
|
237
|
+
|
|
238
|
+
const result = await mockClient.topics.cluster({
|
|
239
|
+
algorithm: 'kmeans',
|
|
240
|
+
numClusters: 2,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(mockClient.topics.cluster).toHaveBeenCalledWith({
|
|
244
|
+
algorithm: 'kmeans',
|
|
245
|
+
numClusters: 2,
|
|
246
|
+
});
|
|
247
|
+
expect(result.clusters).toHaveLength(2);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('topics detect-communities', () => {
|
|
252
|
+
it('should detect communities', async () => {
|
|
253
|
+
const mockResult = {
|
|
254
|
+
communities: [
|
|
255
|
+
{ id: 0, topics: ['topic_1', 'topic_2', 'topic_3'], modularity: 0.45 },
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
mockClient.topics.detectCommunities.mockResolvedValue(mockResult);
|
|
260
|
+
|
|
261
|
+
const result = await mockClient.topics.detectCommunities({
|
|
262
|
+
algorithm: 'louvain',
|
|
263
|
+
minSize: 2,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(mockClient.topics.detectCommunities).toHaveBeenCalledWith({
|
|
267
|
+
algorithm: 'louvain',
|
|
268
|
+
minSize: 2,
|
|
269
|
+
});
|
|
270
|
+
expect(result.communities).toHaveLength(1);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
storeApiKey,
|
|
3
|
+
removeApiKey,
|
|
4
|
+
hasApiKey,
|
|
5
|
+
getApiKeyStatus,
|
|
6
|
+
encryptApiKey,
|
|
7
|
+
decryptApiKey,
|
|
8
|
+
} from '../../src/lib/auth';
|
|
9
|
+
import { config } from '../../src/lib/config';
|
|
10
|
+
|
|
11
|
+
describe('Authentication Helpers', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
config.reset();
|
|
14
|
+
delete process.env.MX_API_KEY;
|
|
15
|
+
// Mock console.log to avoid output during tests
|
|
16
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
jest.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should store API key', () => {
|
|
24
|
+
storeApiKey('test-key-123');
|
|
25
|
+
expect(hasApiKey()).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should reject empty API key', () => {
|
|
29
|
+
expect(() => storeApiKey('')).toThrow('API key cannot be empty');
|
|
30
|
+
expect(() => storeApiKey(' ')).toThrow('API key cannot be empty');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should remove API key', () => {
|
|
34
|
+
storeApiKey('test-key');
|
|
35
|
+
expect(hasApiKey()).toBe(true);
|
|
36
|
+
removeApiKey();
|
|
37
|
+
expect(hasApiKey()).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should detect API key source - none', () => {
|
|
41
|
+
const status = getApiKeyStatus();
|
|
42
|
+
expect(status.configured).toBe(false);
|
|
43
|
+
expect(status.source).toBe('none');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should detect API key source - config', () => {
|
|
47
|
+
storeApiKey('config-key');
|
|
48
|
+
const status = getApiKeyStatus();
|
|
49
|
+
expect(status.configured).toBe(true);
|
|
50
|
+
expect(status.source).toBe('config');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should detect API key source - env', () => {
|
|
54
|
+
process.env.MX_API_KEY = 'env-key';
|
|
55
|
+
const status = getApiKeyStatus();
|
|
56
|
+
expect(status.configured).toBe(true);
|
|
57
|
+
expect(status.source).toBe('env');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should prioritize env over config', () => {
|
|
61
|
+
storeApiKey('config-key');
|
|
62
|
+
process.env.MX_API_KEY = 'env-key';
|
|
63
|
+
const status = getApiKeyStatus();
|
|
64
|
+
expect(status.source).toBe('env');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Placeholder tests for encryption (to be implemented)
|
|
68
|
+
it('should encrypt API key (placeholder)', () => {
|
|
69
|
+
const encrypted = encryptApiKey('test-key');
|
|
70
|
+
expect(encrypted).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should decrypt API key (placeholder)', () => {
|
|
74
|
+
const decrypted = decryptApiKey('encrypted-key');
|
|
75
|
+
expect(decrypted).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { config } from '../../src/lib/config';
|
|
2
|
+
|
|
3
|
+
describe('ConfigManager', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
config.reset();
|
|
6
|
+
delete process.env.MX_API_URL;
|
|
7
|
+
delete process.env.MX_API_KEY;
|
|
8
|
+
delete process.env.MX_OUTPUT_FORMAT;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should return default config', () => {
|
|
12
|
+
const cfg = config.get();
|
|
13
|
+
expect(cfg).toHaveProperty('apiUrl');
|
|
14
|
+
expect(cfg).toHaveProperty('defaultFormat');
|
|
15
|
+
expect(cfg).toHaveProperty('defaultPageSize');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should set and get values', () => {
|
|
19
|
+
config.set('apiUrl', 'http://localhost:3000');
|
|
20
|
+
expect(config.get('apiUrl')).toBe('http://localhost:3000');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should respect environment variables for API URL', () => {
|
|
24
|
+
process.env.MX_API_URL = 'http://env-url:3000';
|
|
25
|
+
expect(config.getApiUrl()).toBe('http://env-url:3000');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should respect environment variables for API key', () => {
|
|
29
|
+
process.env.MX_API_KEY = 'env-api-key';
|
|
30
|
+
expect(config.getApiKey()).toBe('env-api-key');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should reset to defaults', () => {
|
|
34
|
+
config.set('apiUrl', 'http://custom:3000');
|
|
35
|
+
config.reset();
|
|
36
|
+
expect(config.get('apiUrl')).toBe('https://api.memnexus.io');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should check if API key exists', () => {
|
|
40
|
+
expect(config.hasApiKey()).toBe(false);
|
|
41
|
+
config.setApiKey('test-key');
|
|
42
|
+
expect(config.hasApiKey()).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should get format from config or env', () => {
|
|
46
|
+
expect(config.getFormat()).toBe('table');
|
|
47
|
+
process.env.MX_OUTPUT_FORMAT = 'json';
|
|
48
|
+
expect(config.getFormat()).toBe('json');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { CLIError, handleError } from '../../src/lib/errors';
|
|
2
|
+
|
|
3
|
+
describe('Error Handling', () => {
|
|
4
|
+
let exitSpy: jest.SpyInstance;
|
|
5
|
+
let consoleErrorSpy: jest.SpyInstance;
|
|
6
|
+
let consoleLogSpy: jest.SpyInstance;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
exitSpy = jest
|
|
10
|
+
.spyOn(process, 'exit')
|
|
11
|
+
.mockImplementation(((code?: string | number | null) => {
|
|
12
|
+
throw new Error(`process.exit: ${code}`);
|
|
13
|
+
}) as any);
|
|
14
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
15
|
+
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
jest.restoreAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('CLIError', () => {
|
|
23
|
+
it('should create error with default exit code', () => {
|
|
24
|
+
const error = new CLIError('Test error');
|
|
25
|
+
expect(error.message).toBe('Test error');
|
|
26
|
+
expect(error.exitCode).toBe(1);
|
|
27
|
+
expect(error.name).toBe('CLIError');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should create error with custom exit code', () => {
|
|
31
|
+
const error = new CLIError('Test error', 2);
|
|
32
|
+
expect(error.exitCode).toBe(2);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should store details', () => {
|
|
36
|
+
const error = new CLIError('Test error', 1, { foo: 'bar' });
|
|
37
|
+
expect(error.details).toEqual({ foo: 'bar' });
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('handleError', () => {
|
|
42
|
+
it('should handle CLIError', () => {
|
|
43
|
+
const error = new CLIError('Test error', 2);
|
|
44
|
+
expect(() => handleError(error)).toThrow('process.exit: 2');
|
|
45
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should handle 401 API errors', () => {
|
|
49
|
+
const error = {
|
|
50
|
+
isAxiosError: true,
|
|
51
|
+
response: {
|
|
52
|
+
status: 401,
|
|
53
|
+
statusText: 'Unauthorized',
|
|
54
|
+
data: {},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
expect(() => handleError(error)).toThrow('process.exit: 3');
|
|
59
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
60
|
+
expect.stringContaining('Authentication Error')
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle 404 API errors', () => {
|
|
65
|
+
const error = {
|
|
66
|
+
isAxiosError: true,
|
|
67
|
+
response: {
|
|
68
|
+
status: 404,
|
|
69
|
+
statusText: 'Not Found',
|
|
70
|
+
data: {},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
expect(() => handleError(error)).toThrow('process.exit: 1');
|
|
75
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Not Found'));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should handle 422 validation errors', () => {
|
|
79
|
+
const error = {
|
|
80
|
+
isAxiosError: true,
|
|
81
|
+
response: {
|
|
82
|
+
status: 422,
|
|
83
|
+
statusText: 'Unprocessable Entity',
|
|
84
|
+
data: { message: 'Invalid input' },
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
expect(() => handleError(error)).toThrow('process.exit: 2');
|
|
89
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
90
|
+
expect.stringContaining('Validation Error')
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle 429 rate limit errors', () => {
|
|
95
|
+
const error = {
|
|
96
|
+
isAxiosError: true,
|
|
97
|
+
response: {
|
|
98
|
+
status: 429,
|
|
99
|
+
statusText: 'Too Many Requests',
|
|
100
|
+
data: {},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
expect(() => handleError(error)).toThrow('process.exit: 5');
|
|
105
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Rate Limit'));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle network errors', () => {
|
|
109
|
+
const error = {
|
|
110
|
+
isAxiosError: true,
|
|
111
|
+
response: undefined,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
expect(() => handleError(error)).toThrow('process.exit: 4');
|
|
115
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Network Error'));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle unknown errors', () => {
|
|
119
|
+
const error = new Error('Unknown error');
|
|
120
|
+
expect(() => handleError(error)).toThrow('process.exit: 1');
|
|
121
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
122
|
+
expect.stringContaining('unexpected error')
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|