@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.
Files changed (75) hide show
  1. package/LICENSE.md +51 -0
  2. package/README.md +512 -0
  3. package/dist/__tests__/api.test.d.ts +1 -0
  4. package/dist/__tests__/api.test.js +257 -0
  5. package/dist/__tests__/cli.test.d.ts +1 -0
  6. package/dist/__tests__/cli.test.js +153 -0
  7. package/dist/__tests__/commands/config.test.d.ts +1 -0
  8. package/dist/__tests__/commands/config.test.js +154 -0
  9. package/dist/__tests__/commands/init.test.d.ts +1 -0
  10. package/dist/__tests__/commands/init.test.js +186 -0
  11. package/dist/__tests__/commands/pull-retry.test.d.ts +1 -0
  12. package/dist/__tests__/commands/pull-retry.test.js +156 -0
  13. package/dist/__tests__/commands/pull.test.d.ts +1 -0
  14. package/dist/__tests__/commands/pull.test.js +54 -0
  15. package/dist/__tests__/commands/push-spinner.test.d.ts +1 -0
  16. package/dist/__tests__/commands/push-spinner.test.js +127 -0
  17. package/dist/__tests__/commands/push.test.d.ts +1 -0
  18. package/dist/__tests__/commands/push.test.js +265 -0
  19. package/dist/__tests__/config-validation.test.d.ts +1 -0
  20. package/dist/__tests__/config-validation.test.js +106 -0
  21. package/dist/__tests__/package.test.d.ts +1 -0
  22. package/dist/__tests__/package.test.js +82 -0
  23. package/dist/__tests__/utils/json-comparator.test.d.ts +1 -0
  24. package/dist/__tests__/utils/json-comparator.test.js +174 -0
  25. package/dist/__tests__/utils/port-manager.test.d.ts +1 -0
  26. package/dist/__tests__/utils/port-manager.test.js +144 -0
  27. package/dist/__tests__/utils/ts-loader.test.d.ts +1 -0
  28. package/dist/__tests__/utils/ts-loader.test.js +233 -0
  29. package/dist/api.d.ts +23 -0
  30. package/dist/api.js +140 -0
  31. package/dist/commands/chat-enhanced.d.ts +7 -0
  32. package/dist/commands/chat-enhanced.js +396 -0
  33. package/dist/commands/chat.d.ts +5 -0
  34. package/dist/commands/chat.js +125 -0
  35. package/dist/commands/config.d.ts +6 -0
  36. package/dist/commands/config.js +128 -0
  37. package/dist/commands/init.d.ts +5 -0
  38. package/dist/commands/init.js +171 -0
  39. package/dist/commands/list-graphs.d.ts +6 -0
  40. package/dist/commands/list-graphs.js +131 -0
  41. package/dist/commands/mcp-list.d.ts +4 -0
  42. package/dist/commands/mcp-list.js +156 -0
  43. package/dist/commands/mcp-start-simple.d.ts +5 -0
  44. package/dist/commands/mcp-start-simple.js +193 -0
  45. package/dist/commands/mcp-start.d.ts +5 -0
  46. package/dist/commands/mcp-start.js +217 -0
  47. package/dist/commands/mcp-status.d.ts +1 -0
  48. package/dist/commands/mcp-status.js +96 -0
  49. package/dist/commands/mcp-stop.d.ts +5 -0
  50. package/dist/commands/mcp-stop.js +160 -0
  51. package/dist/commands/pull.d.ts +15 -0
  52. package/dist/commands/pull.js +313 -0
  53. package/dist/commands/pull.llm-generate.d.ts +10 -0
  54. package/dist/commands/pull.llm-generate.js +184 -0
  55. package/dist/commands/push.d.ts +6 -0
  56. package/dist/commands/push.js +268 -0
  57. package/dist/config.d.ts +43 -0
  58. package/dist/config.js +292 -0
  59. package/dist/exports.d.ts +2 -0
  60. package/dist/exports.js +2 -0
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.js +98 -0
  63. package/dist/types/config.d.ts +9 -0
  64. package/dist/types/config.js +3 -0
  65. package/dist/types/graph.d.ts +10 -0
  66. package/dist/types/graph.js +1 -0
  67. package/dist/utils/json-comparator.d.ts +60 -0
  68. package/dist/utils/json-comparator.js +222 -0
  69. package/dist/utils/mcp-runner.d.ts +6 -0
  70. package/dist/utils/mcp-runner.js +147 -0
  71. package/dist/utils/port-manager.d.ts +43 -0
  72. package/dist/utils/port-manager.js +92 -0
  73. package/dist/utils/ts-loader.d.ts +5 -0
  74. package/dist/utils/ts-loader.js +146 -0
  75. 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 {};