@inkeep/agents-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/LICENSE.md +51 -0
- package/README.md +512 -0
- package/dist/__tests__/api.test.d.ts +1 -0
- package/dist/__tests__/api.test.js +257 -0
- package/dist/__tests__/cli.test.d.ts +1 -0
- package/dist/__tests__/cli.test.js +153 -0
- package/dist/__tests__/commands/config.test.d.ts +1 -0
- package/dist/__tests__/commands/config.test.js +154 -0
- package/dist/__tests__/commands/init.test.d.ts +1 -0
- package/dist/__tests__/commands/init.test.js +186 -0
- package/dist/__tests__/commands/pull-retry.test.d.ts +1 -0
- package/dist/__tests__/commands/pull-retry.test.js +156 -0
- package/dist/__tests__/commands/pull.test.d.ts +1 -0
- package/dist/__tests__/commands/pull.test.js +54 -0
- package/dist/__tests__/commands/push-spinner.test.d.ts +1 -0
- package/dist/__tests__/commands/push-spinner.test.js +127 -0
- package/dist/__tests__/commands/push.test.d.ts +1 -0
- package/dist/__tests__/commands/push.test.js +265 -0
- package/dist/__tests__/config-validation.test.d.ts +1 -0
- package/dist/__tests__/config-validation.test.js +106 -0
- package/dist/__tests__/package.test.d.ts +1 -0
- package/dist/__tests__/package.test.js +82 -0
- package/dist/__tests__/utils/json-comparator.test.d.ts +1 -0
- package/dist/__tests__/utils/json-comparator.test.js +174 -0
- package/dist/__tests__/utils/port-manager.test.d.ts +1 -0
- package/dist/__tests__/utils/port-manager.test.js +144 -0
- package/dist/__tests__/utils/ts-loader.test.d.ts +1 -0
- package/dist/__tests__/utils/ts-loader.test.js +233 -0
- package/dist/api.d.ts +23 -0
- package/dist/api.js +140 -0
- package/dist/commands/chat-enhanced.d.ts +7 -0
- package/dist/commands/chat-enhanced.js +396 -0
- package/dist/commands/chat.d.ts +5 -0
- package/dist/commands/chat.js +125 -0
- package/dist/commands/config.d.ts +6 -0
- package/dist/commands/config.js +128 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +171 -0
- package/dist/commands/list-graphs.d.ts +6 -0
- package/dist/commands/list-graphs.js +131 -0
- package/dist/commands/mcp-list.d.ts +4 -0
- package/dist/commands/mcp-list.js +156 -0
- package/dist/commands/mcp-start-simple.d.ts +5 -0
- package/dist/commands/mcp-start-simple.js +193 -0
- package/dist/commands/mcp-start.d.ts +5 -0
- package/dist/commands/mcp-start.js +217 -0
- package/dist/commands/mcp-status.d.ts +1 -0
- package/dist/commands/mcp-status.js +96 -0
- package/dist/commands/mcp-stop.d.ts +5 -0
- package/dist/commands/mcp-stop.js +160 -0
- package/dist/commands/pull.d.ts +15 -0
- package/dist/commands/pull.js +313 -0
- package/dist/commands/pull.llm-generate.d.ts +10 -0
- package/dist/commands/pull.llm-generate.js +184 -0
- package/dist/commands/push.d.ts +6 -0
- package/dist/commands/push.js +268 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +292 -0
- package/dist/exports.d.ts +2 -0
- package/dist/exports.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +98 -0
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.js +3 -0
- package/dist/types/graph.d.ts +10 -0
- package/dist/types/graph.js +1 -0
- package/dist/utils/json-comparator.d.ts +60 -0
- package/dist/utils/json-comparator.js +222 -0
- package/dist/utils/mcp-runner.d.ts +6 -0
- package/dist/utils/mcp-runner.js +147 -0
- package/dist/utils/port-manager.d.ts +43 -0
- package/dist/utils/port-manager.js +92 -0
- package/dist/utils/ts-loader.d.ts +5 -0
- package/dist/utils/ts-loader.js +146 -0
- package/package.json +77 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { pushCommand } from '../../commands/push.js';
|
|
3
|
+
import * as core from '@inkeep/agents-core';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
// Mock all external dependencies
|
|
7
|
+
vi.mock('node:fs');
|
|
8
|
+
vi.mock('@inkeep/agents-core');
|
|
9
|
+
vi.mock('inquirer');
|
|
10
|
+
vi.mock('chalk', () => ({
|
|
11
|
+
default: {
|
|
12
|
+
red: vi.fn((text) => text),
|
|
13
|
+
yellow: vi.fn((text) => text),
|
|
14
|
+
green: vi.fn((text) => text),
|
|
15
|
+
cyan: vi.fn((text) => text),
|
|
16
|
+
gray: vi.fn((text) => text),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
vi.mock('ora', () => ({
|
|
20
|
+
default: vi.fn(() => ({
|
|
21
|
+
start: vi.fn().mockReturnThis(),
|
|
22
|
+
succeed: vi.fn().mockReturnThis(),
|
|
23
|
+
fail: vi.fn().mockReturnThis(),
|
|
24
|
+
warn: vi.fn().mockReturnThis(),
|
|
25
|
+
stop: vi.fn().mockReturnThis(),
|
|
26
|
+
text: '',
|
|
27
|
+
})),
|
|
28
|
+
}));
|
|
29
|
+
vi.mock('../../api.js', () => ({
|
|
30
|
+
ApiClient: {
|
|
31
|
+
create: vi.fn().mockResolvedValue({}),
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
vi.mock('../../config.js', () => ({
|
|
35
|
+
validateConfiguration: vi.fn().mockResolvedValue({
|
|
36
|
+
tenantId: 'test-tenant',
|
|
37
|
+
projectId: 'test-project',
|
|
38
|
+
apiUrl: 'http://localhost:3002',
|
|
39
|
+
sources: {
|
|
40
|
+
tenantId: 'config',
|
|
41
|
+
projectId: 'config',
|
|
42
|
+
apiUrl: 'config',
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
}));
|
|
46
|
+
describe('Push Command - Project Validation', () => {
|
|
47
|
+
let mockDbClient;
|
|
48
|
+
let mockGetProject;
|
|
49
|
+
let mockCreateProject;
|
|
50
|
+
let mockExit;
|
|
51
|
+
let mockLog;
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.clearAllMocks();
|
|
54
|
+
// Setup database client mock
|
|
55
|
+
mockDbClient = {};
|
|
56
|
+
mockGetProject = vi.fn();
|
|
57
|
+
mockCreateProject = vi.fn();
|
|
58
|
+
core.createDatabaseClient.mockReturnValue(mockDbClient);
|
|
59
|
+
core.getProject.mockReturnValue(mockGetProject);
|
|
60
|
+
core.createProject.mockReturnValue(mockCreateProject);
|
|
61
|
+
// Mock process.exit to prevent test runner from exiting
|
|
62
|
+
mockExit = vi.fn();
|
|
63
|
+
vi.spyOn(process, 'exit').mockImplementation(mockExit);
|
|
64
|
+
// Mock console methods
|
|
65
|
+
mockLog = vi.fn();
|
|
66
|
+
vi.spyOn(console, 'log').mockImplementation(mockLog);
|
|
67
|
+
vi.spyOn(console, 'error').mockImplementation(vi.fn());
|
|
68
|
+
// Mock file existence check for graph file
|
|
69
|
+
existsSync.mockReturnValue(true);
|
|
70
|
+
// Default environment
|
|
71
|
+
process.env.DB_FILE_NAME = 'test.db';
|
|
72
|
+
});
|
|
73
|
+
it('should validate project exists before pushing graph', async () => {
|
|
74
|
+
// Mock project exists
|
|
75
|
+
mockGetProject.mockResolvedValue({
|
|
76
|
+
id: 'test-project',
|
|
77
|
+
name: 'Test Project',
|
|
78
|
+
tenantId: 'test-tenant',
|
|
79
|
+
});
|
|
80
|
+
// Mock graph file import
|
|
81
|
+
const mockGraph = {
|
|
82
|
+
init: vi.fn().mockResolvedValue(undefined),
|
|
83
|
+
getId: vi.fn().mockReturnValue('test-graph'),
|
|
84
|
+
getName: vi.fn().mockReturnValue('Test Graph'),
|
|
85
|
+
getAgents: vi.fn().mockReturnValue([]),
|
|
86
|
+
getStats: vi.fn().mockReturnValue({
|
|
87
|
+
agentCount: 1,
|
|
88
|
+
toolCount: 0,
|
|
89
|
+
relationCount: 0,
|
|
90
|
+
}),
|
|
91
|
+
getDefaultAgent: vi.fn().mockReturnValue(null),
|
|
92
|
+
setConfig: vi.fn(),
|
|
93
|
+
};
|
|
94
|
+
vi.doMock('/test/path/graph.js', () => ({
|
|
95
|
+
default: mockGraph,
|
|
96
|
+
}));
|
|
97
|
+
// Run in TypeScript mode (skip tsx spawn)
|
|
98
|
+
process.env.TSX_RUNNING = '1';
|
|
99
|
+
await pushCommand('/test/path/graph.js', {});
|
|
100
|
+
// Verify project validation was called
|
|
101
|
+
expect(mockGetProject).toHaveBeenCalledWith({
|
|
102
|
+
scopes: { tenantId: 'test-tenant', projectId: 'test-project' },
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it('should prompt to create project when it does not exist', async () => {
|
|
106
|
+
// Mock project doesn't exist
|
|
107
|
+
mockGetProject.mockResolvedValue(null);
|
|
108
|
+
// Mock user confirms project creation
|
|
109
|
+
inquirer.prompt
|
|
110
|
+
.mockResolvedValueOnce({ shouldCreate: true })
|
|
111
|
+
.mockResolvedValueOnce({
|
|
112
|
+
projectName: 'New Project',
|
|
113
|
+
projectDescription: 'Test description',
|
|
114
|
+
});
|
|
115
|
+
// Mock project creation success
|
|
116
|
+
mockCreateProject.mockResolvedValue({
|
|
117
|
+
id: 'test-project',
|
|
118
|
+
name: 'New Project',
|
|
119
|
+
description: 'Test description',
|
|
120
|
+
tenantId: 'test-tenant',
|
|
121
|
+
});
|
|
122
|
+
// Mock graph file import
|
|
123
|
+
const mockGraph = {
|
|
124
|
+
init: vi.fn().mockResolvedValue(undefined),
|
|
125
|
+
getId: vi.fn().mockReturnValue('test-graph'),
|
|
126
|
+
getName: vi.fn().mockReturnValue('Test Graph'),
|
|
127
|
+
getAgents: vi.fn().mockReturnValue([]),
|
|
128
|
+
getStats: vi.fn().mockReturnValue({
|
|
129
|
+
agentCount: 1,
|
|
130
|
+
toolCount: 0,
|
|
131
|
+
relationCount: 0,
|
|
132
|
+
}),
|
|
133
|
+
getDefaultAgent: vi.fn().mockReturnValue(null),
|
|
134
|
+
setConfig: vi.fn(),
|
|
135
|
+
};
|
|
136
|
+
vi.doMock('/test/path/graph.js', () => ({
|
|
137
|
+
default: mockGraph,
|
|
138
|
+
}));
|
|
139
|
+
process.env.TSX_RUNNING = '1';
|
|
140
|
+
await pushCommand('/test/path/graph.js', {});
|
|
141
|
+
// Verify project creation was prompted
|
|
142
|
+
expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
|
|
143
|
+
expect.objectContaining({
|
|
144
|
+
type: 'confirm',
|
|
145
|
+
name: 'shouldCreate',
|
|
146
|
+
message: expect.stringContaining('does not exist'),
|
|
147
|
+
}),
|
|
148
|
+
]));
|
|
149
|
+
// Verify project was created
|
|
150
|
+
expect(mockCreateProject).toHaveBeenCalledWith({
|
|
151
|
+
id: 'test-project',
|
|
152
|
+
tenantId: 'test-tenant',
|
|
153
|
+
name: 'New Project',
|
|
154
|
+
description: 'Test description',
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
it('should exit if user declines to create missing project', async () => {
|
|
158
|
+
// Mock project doesn't exist
|
|
159
|
+
mockGetProject.mockResolvedValue(null);
|
|
160
|
+
// Mock user declines project creation
|
|
161
|
+
inquirer.prompt.mockResolvedValueOnce({ shouldCreate: false });
|
|
162
|
+
// Mock graph file import (needed to prevent errors)
|
|
163
|
+
const mockGraph = {
|
|
164
|
+
init: vi.fn().mockResolvedValue(undefined),
|
|
165
|
+
getId: vi.fn().mockReturnValue('test-graph'),
|
|
166
|
+
getName: vi.fn().mockReturnValue('Test Graph'),
|
|
167
|
+
getAgents: vi.fn().mockReturnValue([]),
|
|
168
|
+
getStats: vi.fn().mockReturnValue({
|
|
169
|
+
agentCount: 1,
|
|
170
|
+
toolCount: 0,
|
|
171
|
+
relationCount: 0,
|
|
172
|
+
}),
|
|
173
|
+
getDefaultAgent: vi.fn().mockReturnValue(null),
|
|
174
|
+
setConfig: vi.fn(),
|
|
175
|
+
};
|
|
176
|
+
vi.doMock('/test/path/graph.js', () => ({
|
|
177
|
+
default: mockGraph,
|
|
178
|
+
}));
|
|
179
|
+
process.env.TSX_RUNNING = '1';
|
|
180
|
+
await pushCommand('/test/path/graph.js', {});
|
|
181
|
+
// Verify push was cancelled
|
|
182
|
+
expect(mockExit).toHaveBeenCalledWith(0);
|
|
183
|
+
expect(mockLog).toHaveBeenCalledWith(expect.stringContaining('Push cancelled'));
|
|
184
|
+
// Verify project was not created
|
|
185
|
+
expect(mockCreateProject).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
it('should handle project creation errors gracefully', async () => {
|
|
188
|
+
// Mock project doesn't exist
|
|
189
|
+
mockGetProject.mockResolvedValue(null);
|
|
190
|
+
// Mock user confirms project creation
|
|
191
|
+
inquirer.prompt
|
|
192
|
+
.mockResolvedValueOnce({ shouldCreate: true })
|
|
193
|
+
.mockResolvedValueOnce({
|
|
194
|
+
projectName: 'New Project',
|
|
195
|
+
projectDescription: '',
|
|
196
|
+
});
|
|
197
|
+
// Mock project creation failure
|
|
198
|
+
mockCreateProject.mockRejectedValue(new Error('Database error'));
|
|
199
|
+
process.env.TSX_RUNNING = '1';
|
|
200
|
+
await pushCommand('/test/path/graph.js', {});
|
|
201
|
+
// Verify error handling
|
|
202
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
203
|
+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Error'), 'Database error');
|
|
204
|
+
});
|
|
205
|
+
it('should use DB_FILE_NAME environment variable for database location', async () => {
|
|
206
|
+
process.env.DB_FILE_NAME = 'custom-location.db';
|
|
207
|
+
mockGetProject.mockResolvedValue({
|
|
208
|
+
id: 'test-project',
|
|
209
|
+
name: 'Test Project',
|
|
210
|
+
tenantId: 'test-tenant',
|
|
211
|
+
});
|
|
212
|
+
const mockGraph = {
|
|
213
|
+
init: vi.fn().mockResolvedValue(undefined),
|
|
214
|
+
getId: vi.fn().mockReturnValue('test-graph'),
|
|
215
|
+
getName: vi.fn().mockReturnValue('Test Graph'),
|
|
216
|
+
getAgents: vi.fn().mockReturnValue([]),
|
|
217
|
+
getStats: vi.fn().mockReturnValue({
|
|
218
|
+
agentCount: 1,
|
|
219
|
+
toolCount: 0,
|
|
220
|
+
relationCount: 0,
|
|
221
|
+
}),
|
|
222
|
+
getDefaultAgent: vi.fn().mockReturnValue(null),
|
|
223
|
+
setConfig: vi.fn(),
|
|
224
|
+
};
|
|
225
|
+
vi.doMock('/test/path/graph.js', () => ({
|
|
226
|
+
default: mockGraph,
|
|
227
|
+
}));
|
|
228
|
+
process.env.TSX_RUNNING = '1';
|
|
229
|
+
await pushCommand('/test/path/graph.js', {});
|
|
230
|
+
// Verify correct database URL was used
|
|
231
|
+
expect(core.createDatabaseClient).toHaveBeenCalledWith({
|
|
232
|
+
url: expect.stringContaining('custom-location.db'),
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
it('should default to local.db when DB_FILE_NAME is not set', async () => {
|
|
236
|
+
delete process.env.DB_FILE_NAME;
|
|
237
|
+
mockGetProject.mockResolvedValue({
|
|
238
|
+
id: 'test-project',
|
|
239
|
+
name: 'Test Project',
|
|
240
|
+
tenantId: 'test-tenant',
|
|
241
|
+
});
|
|
242
|
+
const mockGraph = {
|
|
243
|
+
init: vi.fn().mockResolvedValue(undefined),
|
|
244
|
+
getId: vi.fn().mockReturnValue('test-graph'),
|
|
245
|
+
getName: vi.fn().mockReturnValue('Test Graph'),
|
|
246
|
+
getAgents: vi.fn().mockReturnValue([]),
|
|
247
|
+
getStats: vi.fn().mockReturnValue({
|
|
248
|
+
agentCount: 1,
|
|
249
|
+
toolCount: 0,
|
|
250
|
+
relationCount: 0,
|
|
251
|
+
}),
|
|
252
|
+
getDefaultAgent: vi.fn().mockReturnValue(null),
|
|
253
|
+
setConfig: vi.fn(),
|
|
254
|
+
};
|
|
255
|
+
vi.doMock('/test/path/graph.js', () => ({
|
|
256
|
+
default: mockGraph,
|
|
257
|
+
}));
|
|
258
|
+
process.env.TSX_RUNNING = '1';
|
|
259
|
+
await pushCommand('/test/path/graph.js', {});
|
|
260
|
+
// Verify default database URL was used
|
|
261
|
+
expect(core.createDatabaseClient).toHaveBeenCalledWith({
|
|
262
|
+
url: expect.stringContaining('local.db'),
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { validateConfiguration } from '../config.js';
|
|
3
|
+
// Save original env and cwd
|
|
4
|
+
const originalEnv = process.env;
|
|
5
|
+
const originalCwd = process.cwd();
|
|
6
|
+
// Mock the file system to prevent finding actual config files
|
|
7
|
+
vi.mock('node:fs', () => ({
|
|
8
|
+
existsSync: vi.fn(() => false),
|
|
9
|
+
}));
|
|
10
|
+
// Mock process.cwd to avoid loading the actual config file
|
|
11
|
+
vi.mock('node:process', () => ({
|
|
12
|
+
cwd: () => '/tmp/test-cli',
|
|
13
|
+
}));
|
|
14
|
+
describe('Configuration Validation', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
process.env = { ...originalEnv };
|
|
18
|
+
delete process.env.INKEEP_TENANT_ID;
|
|
19
|
+
delete process.env.INKEEP_API_URL;
|
|
20
|
+
delete process.env.INKEEP_MANAGEMENT_API_URL;
|
|
21
|
+
delete process.env.INKEEP_EXECUTION_API_URL;
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
process.env = originalEnv;
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
describe('validateConfiguration', () => {
|
|
28
|
+
describe('Valid Configurations', () => {
|
|
29
|
+
it('should accept --tenant-id with --management-api-url and --execution-api-url flags', async () => {
|
|
30
|
+
const config = await validateConfiguration('test-tenant', 'http://localhost:3002', 'http://localhost:3003', undefined);
|
|
31
|
+
expect(config.tenantId).toBe('test-tenant');
|
|
32
|
+
expect(config.managementApiUrl).toBe('http://localhost:3002');
|
|
33
|
+
expect(config.executionApiUrl).toBe('http://localhost:3003');
|
|
34
|
+
expect(config.sources.tenantId).toBe('command-line flag (--tenant-id)');
|
|
35
|
+
expect(config.sources.managementApiUrl).toBe('command-line flag (--management-api-url)');
|
|
36
|
+
expect(config.sources.executionApiUrl).toBe('command-line flag (--execution-api-url)');
|
|
37
|
+
});
|
|
38
|
+
it('should use environment variables when no flags provided', async () => {
|
|
39
|
+
process.env.INKEEP_TENANT_ID = 'env-tenant';
|
|
40
|
+
process.env.INKEEP_MANAGEMENT_API_URL = 'http://localhost:9090';
|
|
41
|
+
process.env.INKEEP_EXECUTION_API_URL = 'http://localhost:9091';
|
|
42
|
+
const config = await validateConfiguration(undefined, undefined, undefined, undefined);
|
|
43
|
+
expect(config.tenantId).toBe('env-tenant');
|
|
44
|
+
expect(config.managementApiUrl).toBe('http://localhost:9090');
|
|
45
|
+
expect(config.executionApiUrl).toBe('http://localhost:9091');
|
|
46
|
+
expect(config.sources.tenantId).toBe('environment variable (INKEEP_TENANT_ID)');
|
|
47
|
+
expect(config.sources.managementApiUrl).toBe('environment variable (INKEEP_MANAGEMENT_API_URL)');
|
|
48
|
+
expect(config.sources.executionApiUrl).toBe('environment variable (INKEEP_EXECUTION_API_URL)');
|
|
49
|
+
});
|
|
50
|
+
it('should allow command-line flags to override environment variables', async () => {
|
|
51
|
+
process.env.INKEEP_TENANT_ID = 'env-tenant';
|
|
52
|
+
process.env.INKEEP_MANAGEMENT_API_URL = 'http://localhost:9090';
|
|
53
|
+
process.env.INKEEP_EXECUTION_API_URL = 'http://localhost:9091';
|
|
54
|
+
const config = await validateConfiguration('cli-tenant', 'http://cli-management', 'http://cli-execution', undefined);
|
|
55
|
+
expect(config.tenantId).toBe('cli-tenant');
|
|
56
|
+
expect(config.managementApiUrl).toBe('http://cli-management');
|
|
57
|
+
expect(config.executionApiUrl).toBe('http://cli-execution');
|
|
58
|
+
expect(config.sources.tenantId).toBe('command-line flag (--tenant-id)');
|
|
59
|
+
expect(config.sources.managementApiUrl).toBe('command-line flag (--management-api-url)');
|
|
60
|
+
expect(config.sources.executionApiUrl).toBe('command-line flag (--execution-api-url)');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('Invalid Configurations', () => {
|
|
64
|
+
it('should reject --config-file-path with --tenant-id', async () => {
|
|
65
|
+
await expect(validateConfiguration('test-tenant', undefined, undefined, '/path/to/config.js')).rejects.toThrow('Invalid configuration combination');
|
|
66
|
+
});
|
|
67
|
+
it('should reject --tenant-id without both API URLs', async () => {
|
|
68
|
+
await expect(validateConfiguration('test-tenant', undefined, undefined, undefined)).rejects.toThrow('--tenant-id requires --management-api-url and --execution-api-url');
|
|
69
|
+
});
|
|
70
|
+
it('should reject when no configuration is provided', async () => {
|
|
71
|
+
await expect(validateConfiguration(undefined, undefined, undefined, undefined)).rejects.toThrow('No configuration found');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('Configuration Source Tracking', () => {
|
|
75
|
+
it('should correctly identify command-line flag sources', async () => {
|
|
76
|
+
const config = await validateConfiguration('cli-tenant', 'http://cli-management', 'http://cli-execution', undefined);
|
|
77
|
+
expect(config.sources.tenantId).toBe('command-line flag (--tenant-id)');
|
|
78
|
+
expect(config.sources.managementApiUrl).toBe('command-line flag (--management-api-url)');
|
|
79
|
+
expect(config.sources.executionApiUrl).toBe('command-line flag (--execution-api-url)');
|
|
80
|
+
expect(config.sources.configFile).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
it('should correctly identify environment variable sources', async () => {
|
|
83
|
+
process.env.INKEEP_TENANT_ID = 'env-tenant';
|
|
84
|
+
process.env.INKEEP_MANAGEMENT_API_URL = 'http://env-management';
|
|
85
|
+
process.env.INKEEP_EXECUTION_API_URL = 'http://env-execution';
|
|
86
|
+
const config = await validateConfiguration(undefined, undefined, undefined, undefined);
|
|
87
|
+
expect(config.sources.tenantId).toBe('environment variable (INKEEP_TENANT_ID)');
|
|
88
|
+
expect(config.sources.managementApiUrl).toBe('environment variable (INKEEP_MANAGEMENT_API_URL)');
|
|
89
|
+
expect(config.sources.executionApiUrl).toBe('environment variable (INKEEP_EXECUTION_API_URL)');
|
|
90
|
+
});
|
|
91
|
+
it('should correctly identify mixed sources with env and flag', async () => {
|
|
92
|
+
process.env.INKEEP_TENANT_ID = 'env-tenant';
|
|
93
|
+
process.env.INKEEP_MANAGEMENT_API_URL = 'http://env-management';
|
|
94
|
+
process.env.INKEEP_EXECUTION_API_URL = 'http://env-execution';
|
|
95
|
+
// Override only the management API URL with a flag
|
|
96
|
+
const config = await validateConfiguration(undefined, 'http://override-management', undefined, undefined);
|
|
97
|
+
expect(config.tenantId).toBe('env-tenant');
|
|
98
|
+
expect(config.managementApiUrl).toBe('http://override-management');
|
|
99
|
+
expect(config.executionApiUrl).toBe('http://env-execution');
|
|
100
|
+
expect(config.sources.tenantId).toBe('environment variable (INKEEP_TENANT_ID)');
|
|
101
|
+
expect(config.sources.managementApiUrl).toBe('command-line flag (--management-api-url)');
|
|
102
|
+
expect(config.sources.executionApiUrl).toBe('environment variable (INKEEP_EXECUTION_API_URL)');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
describe('Package Configuration', () => {
|
|
8
|
+
const packageJsonPath = join(__dirname, '..', '..', 'package.json');
|
|
9
|
+
const tsConfigTypeCheckPath = join(__dirname, '..', '..', 'tsconfig.typecheck.json');
|
|
10
|
+
let packageJson;
|
|
11
|
+
let tsConfigTypeCheck;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
const packageJsonContent = readFileSync(packageJsonPath, 'utf-8');
|
|
14
|
+
packageJson = JSON.parse(packageJsonContent);
|
|
15
|
+
const tsConfigTypeCheckContent = readFileSync(tsConfigTypeCheckPath, 'utf-8');
|
|
16
|
+
tsConfigTypeCheck = JSON.parse(tsConfigTypeCheckContent);
|
|
17
|
+
});
|
|
18
|
+
describe('package.json', () => {
|
|
19
|
+
it('should have correct package name', () => {
|
|
20
|
+
expect(packageJson.name).toBe('@inkeep/agents-cli');
|
|
21
|
+
});
|
|
22
|
+
it('should have a valid version', () => {
|
|
23
|
+
expect(packageJson.version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
24
|
+
});
|
|
25
|
+
it('should have correct bin configuration', () => {
|
|
26
|
+
expect(packageJson.bin).toEqual({
|
|
27
|
+
inkeep: './dist/index.js',
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
it('should have correct main entry point', () => {
|
|
31
|
+
expect(packageJson.main).toBe('./dist/exports.js');
|
|
32
|
+
});
|
|
33
|
+
it('should be set to module type', () => {
|
|
34
|
+
expect(packageJson.type).toBe('module');
|
|
35
|
+
});
|
|
36
|
+
it('should have required dependencies', () => {
|
|
37
|
+
expect(packageJson.dependencies).toHaveProperty('commander');
|
|
38
|
+
expect(packageJson.dependencies).toHaveProperty('chalk');
|
|
39
|
+
});
|
|
40
|
+
it('should have required dev dependencies', () => {
|
|
41
|
+
expect(packageJson.devDependencies).toHaveProperty('typescript');
|
|
42
|
+
expect(packageJson.devDependencies).toHaveProperty('vitest');
|
|
43
|
+
expect(packageJson.devDependencies).toHaveProperty('@types/node');
|
|
44
|
+
});
|
|
45
|
+
it('should have test scripts', () => {
|
|
46
|
+
expect(packageJson.scripts).toHaveProperty('test');
|
|
47
|
+
expect(packageJson.scripts).toHaveProperty('test:watch');
|
|
48
|
+
expect(packageJson.scripts).toHaveProperty('test:coverage');
|
|
49
|
+
});
|
|
50
|
+
it('should have typecheck script', () => {
|
|
51
|
+
expect(packageJson.scripts).toHaveProperty('typecheck');
|
|
52
|
+
expect(packageJson.scripts.typecheck).toBe('tsc --noEmit --project tsconfig.typecheck.json');
|
|
53
|
+
});
|
|
54
|
+
it('should have correct Node.js engine requirement', () => {
|
|
55
|
+
expect(packageJson.engines.node).toBe('>=20.x');
|
|
56
|
+
});
|
|
57
|
+
it('should have correct author', () => {
|
|
58
|
+
expect(packageJson.author).toBe('Inkeep <support@inkeep.com>');
|
|
59
|
+
});
|
|
60
|
+
it('should have correct license reference', () => {
|
|
61
|
+
expect(packageJson.license).toBe('SEE LICENSE IN LICENSE.md');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('tsconfig.typecheck.json', () => {
|
|
65
|
+
it('should extend base tsconfig', () => {
|
|
66
|
+
expect(tsConfigTypeCheck.extends).toBe('./tsconfig.json');
|
|
67
|
+
});
|
|
68
|
+
it('should have correct compiler options', () => {
|
|
69
|
+
expect(tsConfigTypeCheck.compilerOptions).toHaveProperty('noEmit', true);
|
|
70
|
+
expect(tsConfigTypeCheck.compilerOptions).toHaveProperty('skipLibCheck', true);
|
|
71
|
+
});
|
|
72
|
+
it('should include src files', () => {
|
|
73
|
+
expect(tsConfigTypeCheck.include).toContain('src/**/*');
|
|
74
|
+
});
|
|
75
|
+
it('should exclude test files and build artifacts', () => {
|
|
76
|
+
expect(tsConfigTypeCheck.exclude).toContain('node_modules');
|
|
77
|
+
expect(tsConfigTypeCheck.exclude).toContain('dist');
|
|
78
|
+
expect(tsConfigTypeCheck.exclude).toContain('**/*.test.ts');
|
|
79
|
+
expect(tsConfigTypeCheck.exclude).toContain('src/__tests__/**/*');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { compareJsonObjects, getDifferenceSummary, normalizeJsonObject, } from '../../utils/json-comparator.js';
|
|
3
|
+
describe('json-comparator', () => {
|
|
4
|
+
describe('compareJsonObjects', () => {
|
|
5
|
+
it('should return true for identical objects', () => {
|
|
6
|
+
const obj1 = { a: 1, b: 'test', c: [1, 2, 3] };
|
|
7
|
+
const obj2 = { a: 1, b: 'test', c: [1, 2, 3] };
|
|
8
|
+
const result = compareJsonObjects(obj1, obj2);
|
|
9
|
+
expect(result.isEqual).toBe(true);
|
|
10
|
+
expect(result.differences).toHaveLength(0);
|
|
11
|
+
});
|
|
12
|
+
it('should return false for different objects', () => {
|
|
13
|
+
const obj1 = { a: 1, b: 'test' };
|
|
14
|
+
const obj2 = { a: 2, b: 'test' };
|
|
15
|
+
const result = compareJsonObjects(obj1, obj2);
|
|
16
|
+
expect(result.isEqual).toBe(false);
|
|
17
|
+
expect(result.differences).toHaveLength(1);
|
|
18
|
+
expect(result.differences[0].path).toBe('a');
|
|
19
|
+
expect(result.differences[0].type).toBe('different');
|
|
20
|
+
});
|
|
21
|
+
it('should handle arrays with different order when ignoreArrayOrder is true', () => {
|
|
22
|
+
const obj1 = { items: [1, 2, 3] };
|
|
23
|
+
const obj2 = { items: [3, 1, 2] };
|
|
24
|
+
const result = compareJsonObjects(obj1, obj2, { ignoreArrayOrder: true });
|
|
25
|
+
expect(result.isEqual).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
it('should detect arrays with different order when ignoreArrayOrder is false', () => {
|
|
28
|
+
const obj1 = { items: [1, 2, 3] };
|
|
29
|
+
const obj2 = { items: [3, 1, 2] };
|
|
30
|
+
const result = compareJsonObjects(obj1, obj2, { ignoreArrayOrder: false });
|
|
31
|
+
expect(result.isEqual).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
it('should handle missing keys', () => {
|
|
34
|
+
const obj1 = { a: 1, b: 2 };
|
|
35
|
+
const obj2 = { a: 1 };
|
|
36
|
+
const result = compareJsonObjects(obj1, obj2);
|
|
37
|
+
expect(result.isEqual).toBe(false);
|
|
38
|
+
expect(result.differences).toHaveLength(1);
|
|
39
|
+
expect(result.differences[0].type).toBe('extra');
|
|
40
|
+
expect(result.differences[0].path).toBe('b');
|
|
41
|
+
});
|
|
42
|
+
it('should handle extra keys', () => {
|
|
43
|
+
const obj1 = { a: 1 };
|
|
44
|
+
const obj2 = { a: 1, b: 2 };
|
|
45
|
+
const result = compareJsonObjects(obj1, obj2);
|
|
46
|
+
expect(result.isEqual).toBe(false);
|
|
47
|
+
expect(result.differences).toHaveLength(1);
|
|
48
|
+
expect(result.differences[0].type).toBe('missing');
|
|
49
|
+
expect(result.differences[0].path).toBe('b');
|
|
50
|
+
});
|
|
51
|
+
it('should handle nested objects', () => {
|
|
52
|
+
const obj1 = { user: { name: 'John', age: 30 } };
|
|
53
|
+
const obj2 = { user: { name: 'John', age: 31 } };
|
|
54
|
+
const result = compareJsonObjects(obj1, obj2);
|
|
55
|
+
expect(result.isEqual).toBe(false);
|
|
56
|
+
expect(result.differences).toHaveLength(1);
|
|
57
|
+
expect(result.differences[0].path).toBe('user.age');
|
|
58
|
+
});
|
|
59
|
+
it('should handle type mismatches', () => {
|
|
60
|
+
const obj1 = { value: '123' };
|
|
61
|
+
const obj2 = { value: 123 };
|
|
62
|
+
const result = compareJsonObjects(obj1, obj2);
|
|
63
|
+
expect(result.isEqual).toBe(false);
|
|
64
|
+
expect(result.differences).toHaveLength(1);
|
|
65
|
+
expect(result.differences[0].type).toBe('type_mismatch');
|
|
66
|
+
});
|
|
67
|
+
it('should ignore specified paths', () => {
|
|
68
|
+
const obj1 = { a: 1, b: 2, c: 3 };
|
|
69
|
+
const obj2 = { a: 1, b: 999, c: 3 };
|
|
70
|
+
const result = compareJsonObjects(obj1, obj2, { ignorePaths: ['b'] });
|
|
71
|
+
expect(result.isEqual).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('should ignore paths with wildcards', () => {
|
|
74
|
+
const obj1 = { user: { name: 'John', age: 30 }, meta: { created: '2023-01-01' } };
|
|
75
|
+
const obj2 = { user: { name: 'John', age: 30 }, meta: { created: '2023-01-02' } };
|
|
76
|
+
const result = compareJsonObjects(obj1, obj2, { ignorePaths: ['meta.*'] });
|
|
77
|
+
expect(result.isEqual).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
it('should handle case insensitive comparison', () => {
|
|
80
|
+
const obj1 = { name: 'John' };
|
|
81
|
+
const obj2 = { name: 'JOHN' };
|
|
82
|
+
const result = compareJsonObjects(obj1, obj2, { ignoreCase: true });
|
|
83
|
+
expect(result.isEqual).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
it('should handle whitespace insensitive comparison', () => {
|
|
86
|
+
const obj1 = { description: 'Hello world' };
|
|
87
|
+
const obj2 = { description: ' Hello world ' };
|
|
88
|
+
const result = compareJsonObjects(obj1, obj2, { ignoreWhitespace: true });
|
|
89
|
+
expect(result.isEqual).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
it('should provide accurate statistics', () => {
|
|
92
|
+
const obj1 = { a: 1, b: 2, c: 3 };
|
|
93
|
+
const obj2 = { a: 1, b: 999, d: 4 };
|
|
94
|
+
const result = compareJsonObjects(obj1, obj2);
|
|
95
|
+
expect(result.stats.totalKeys).toBe(4);
|
|
96
|
+
expect(result.stats.differentKeys).toBe(1); // 'b' has different values
|
|
97
|
+
expect(result.stats.missingKeys).toBe(1); // 'd' is missing in obj1
|
|
98
|
+
expect(result.stats.extraKeys).toBe(1); // 'c' is extra in obj1
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('normalizeJsonObject', () => {
|
|
102
|
+
it('should normalize strings with case and whitespace options', () => {
|
|
103
|
+
const obj = { name: ' John DOE ', items: ['A', 'B', 'C'] };
|
|
104
|
+
const normalized = normalizeJsonObject(obj, {
|
|
105
|
+
ignoreCase: true,
|
|
106
|
+
ignoreWhitespace: true,
|
|
107
|
+
ignoreArrayOrder: true,
|
|
108
|
+
});
|
|
109
|
+
expect(normalized.name).toBe('john doe');
|
|
110
|
+
expect(normalized.items).toEqual(['a', 'b', 'c']); // Sorted alphabetically and case normalized
|
|
111
|
+
});
|
|
112
|
+
it('should sort object keys', () => {
|
|
113
|
+
const obj = { c: 3, a: 1, b: 2 };
|
|
114
|
+
const normalized = normalizeJsonObject(obj);
|
|
115
|
+
expect(Object.keys(normalized)).toEqual(['a', 'b', 'c']);
|
|
116
|
+
});
|
|
117
|
+
it('should handle nested objects', () => {
|
|
118
|
+
const obj = { user: { name: 'John', age: 30 }, meta: { version: '1.0' } };
|
|
119
|
+
const normalized = normalizeJsonObject(obj);
|
|
120
|
+
expect(Object.keys(normalized)).toEqual(['meta', 'user']);
|
|
121
|
+
expect(Object.keys(normalized.user)).toEqual(['age', 'name']);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe('getDifferenceSummary', () => {
|
|
125
|
+
it('should return success message for equal objects', () => {
|
|
126
|
+
const result = {
|
|
127
|
+
isEqual: true,
|
|
128
|
+
differences: [],
|
|
129
|
+
stats: { totalKeys: 0, differentKeys: 0, missingKeys: 0, extraKeys: 0 },
|
|
130
|
+
};
|
|
131
|
+
const summary = getDifferenceSummary(result);
|
|
132
|
+
expect(summary).toBe('✅ Objects are equivalent');
|
|
133
|
+
});
|
|
134
|
+
it('should return detailed summary for different objects', () => {
|
|
135
|
+
const result = {
|
|
136
|
+
isEqual: false,
|
|
137
|
+
differences: [
|
|
138
|
+
{
|
|
139
|
+
path: 'a',
|
|
140
|
+
type: 'different',
|
|
141
|
+
value1: 1,
|
|
142
|
+
value2: 2,
|
|
143
|
+
description: 'Value mismatch',
|
|
144
|
+
},
|
|
145
|
+
{ path: 'b', type: 'missing', value2: 3, description: 'Missing key' },
|
|
146
|
+
],
|
|
147
|
+
stats: { totalKeys: 2, differentKeys: 1, missingKeys: 1, extraKeys: 0 },
|
|
148
|
+
};
|
|
149
|
+
const summary = getDifferenceSummary(result);
|
|
150
|
+
expect(summary).toContain('❌ Objects differ');
|
|
151
|
+
expect(summary).toContain('Total keys: 2');
|
|
152
|
+
expect(summary).toContain('Different values: 1');
|
|
153
|
+
expect(summary).toContain('Missing keys: 1');
|
|
154
|
+
expect(summary).toContain('a: Value mismatch');
|
|
155
|
+
expect(summary).toContain('b: Missing key');
|
|
156
|
+
});
|
|
157
|
+
it('should limit displayed differences to 10', () => {
|
|
158
|
+
const differences = Array.from({ length: 15 }, (_, i) => ({
|
|
159
|
+
path: `key${i}`,
|
|
160
|
+
type: 'different',
|
|
161
|
+
value1: i,
|
|
162
|
+
value2: i + 1,
|
|
163
|
+
description: `Difference ${i}`,
|
|
164
|
+
}));
|
|
165
|
+
const result = {
|
|
166
|
+
isEqual: false,
|
|
167
|
+
differences,
|
|
168
|
+
stats: { totalKeys: 15, differentKeys: 15, missingKeys: 0, extraKeys: 0 },
|
|
169
|
+
};
|
|
170
|
+
const summary = getDifferenceSummary(result);
|
|
171
|
+
expect(summary).toContain('... and 5 more differences');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|