@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,186 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { initCommand } from '../../commands/init.js';
3
+ // Mock inquirer
4
+ vi.mock('inquirer', () => ({
5
+ default: {
6
+ prompt: vi.fn(),
7
+ },
8
+ }));
9
+ // Mock fs functions
10
+ vi.mock('node:fs', async () => {
11
+ const actual = await vi.importActual('node:fs');
12
+ return {
13
+ ...actual,
14
+ existsSync: vi.fn(),
15
+ writeFileSync: vi.fn(),
16
+ readdirSync: vi.fn(),
17
+ };
18
+ });
19
+ describe('Init Command', () => {
20
+ let consoleLogSpy;
21
+ let consoleErrorSpy;
22
+ let processExitSpy;
23
+ beforeEach(() => {
24
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
25
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
26
+ processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
27
+ throw new Error('process.exit called');
28
+ });
29
+ vi.clearAllMocks();
30
+ });
31
+ afterEach(() => {
32
+ consoleLogSpy.mockRestore();
33
+ consoleErrorSpy.mockRestore();
34
+ processExitSpy.mockRestore();
35
+ });
36
+ describe('initCommand', () => {
37
+ it('should create a new config file when none exists', async () => {
38
+ const { existsSync, writeFileSync, readdirSync } = await import('node:fs');
39
+ const inquirer = await import('inquirer');
40
+ vi.mocked(existsSync).mockReturnValue(false);
41
+ vi.mocked(readdirSync).mockReturnValue(['package.json']);
42
+ const promptMock = vi.mocked(inquirer.default.prompt);
43
+ promptMock
44
+ .mockResolvedValueOnce({
45
+ confirmedPath: './inkeep.config.ts',
46
+ })
47
+ .mockResolvedValueOnce({
48
+ tenantId: 'test-tenant-123',
49
+ apiUrl: 'http://localhost:3002',
50
+ });
51
+ await initCommand();
52
+ expect(existsSync).toHaveBeenCalledWith(expect.stringContaining('inkeep.config.ts'));
53
+ expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('inkeep.config.ts'), expect.stringContaining("tenantId: 'test-tenant-123'"));
54
+ expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('inkeep.config.ts'), expect.stringContaining("apiUrl: 'http://localhost:3002'"));
55
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.any(String), // The checkmark
56
+ expect.stringContaining('Created'));
57
+ });
58
+ it('should prompt for overwrite when config file exists', async () => {
59
+ const { existsSync, writeFileSync, readdirSync } = await import('node:fs');
60
+ const inquirer = await import('inquirer');
61
+ vi.mocked(readdirSync).mockReturnValue(['package.json']);
62
+ vi.mocked(existsSync).mockImplementation((path) => {
63
+ return path.toString().includes('inkeep.config.ts');
64
+ });
65
+ const promptMock = vi.mocked(inquirer.default.prompt);
66
+ promptMock
67
+ .mockResolvedValueOnce({ confirmedPath: './inkeep.config.ts' })
68
+ .mockResolvedValueOnce({ overwrite: true })
69
+ .mockResolvedValueOnce({
70
+ tenantId: 'new-tenant-456',
71
+ apiUrl: 'https://api.example.com',
72
+ });
73
+ await initCommand();
74
+ expect(inquirer.default.prompt).toHaveBeenCalledWith(expect.arrayContaining([
75
+ expect.objectContaining({
76
+ type: 'confirm',
77
+ name: 'overwrite',
78
+ message: expect.stringContaining('already exists'),
79
+ }),
80
+ ]));
81
+ expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('inkeep.config.ts'), expect.stringContaining("tenantId: 'new-tenant-456'"));
82
+ });
83
+ it('should cancel when user chooses not to overwrite', async () => {
84
+ const { existsSync, writeFileSync, readdirSync } = await import('node:fs');
85
+ const inquirer = await import('inquirer');
86
+ vi.mocked(readdirSync).mockReturnValue(['package.json']);
87
+ vi.mocked(existsSync).mockImplementation((path) => {
88
+ return path.toString().includes('inkeep.config.ts');
89
+ });
90
+ const promptMock = vi.mocked(inquirer.default.prompt);
91
+ promptMock
92
+ .mockResolvedValueOnce({ confirmedPath: './inkeep.config.ts' })
93
+ .mockResolvedValueOnce({ overwrite: false });
94
+ await initCommand();
95
+ expect(writeFileSync).not.toHaveBeenCalled();
96
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Init cancelled'));
97
+ });
98
+ it('should validate tenant ID is not empty', async () => {
99
+ const { existsSync, readdirSync } = await import('node:fs');
100
+ const inquirer = await import('inquirer');
101
+ vi.mocked(existsSync).mockReturnValue(false);
102
+ vi.mocked(readdirSync).mockReturnValue(['package.json']);
103
+ let pathCallCount = 0;
104
+ const promptMock = vi.mocked(inquirer.default.prompt);
105
+ promptMock.mockImplementation(async (questions) => {
106
+ pathCallCount++;
107
+ if (pathCallCount === 1) {
108
+ // First call is for path confirmation
109
+ return { confirmedPath: './inkeep.config.ts' };
110
+ }
111
+ else {
112
+ // Second call is for tenant ID and API URL
113
+ const tenantIdQuestion = questions.find((q) => q.name === 'tenantId');
114
+ expect(tenantIdQuestion.validate('')).toBe('Tenant ID is required');
115
+ expect(tenantIdQuestion.validate(' ')).toBe('Tenant ID is required');
116
+ expect(tenantIdQuestion.validate('valid-tenant')).toBe(true);
117
+ return {
118
+ tenantId: 'valid-tenant',
119
+ apiUrl: 'http://localhost:3002',
120
+ };
121
+ }
122
+ });
123
+ await initCommand();
124
+ });
125
+ it('should validate API URL format', async () => {
126
+ const { existsSync, readdirSync } = await import('node:fs');
127
+ const inquirer = await import('inquirer');
128
+ vi.mocked(existsSync).mockReturnValue(false);
129
+ vi.mocked(readdirSync).mockReturnValue(['package.json']);
130
+ let pathCallCount = 0;
131
+ const promptMock = vi.mocked(inquirer.default.prompt);
132
+ promptMock.mockImplementation(async (questions) => {
133
+ pathCallCount++;
134
+ if (pathCallCount === 1) {
135
+ // First call is for path confirmation
136
+ return { confirmedPath: './inkeep.config.ts' };
137
+ }
138
+ else {
139
+ // Second call is for tenant ID and API URL
140
+ const apiUrlQuestion = questions.find((q) => q.name === 'apiUrl');
141
+ expect(apiUrlQuestion.validate('not-a-url')).toBe('Please enter a valid URL');
142
+ expect(apiUrlQuestion.validate('http://localhost:3002')).toBe(true);
143
+ expect(apiUrlQuestion.validate('https://api.example.com')).toBe(true);
144
+ return {
145
+ tenantId: 'test-tenant',
146
+ apiUrl: 'http://localhost:3002',
147
+ };
148
+ }
149
+ });
150
+ await initCommand();
151
+ });
152
+ it('should accept a path parameter', async () => {
153
+ const { existsSync, writeFileSync } = await import('node:fs');
154
+ const inquirer = await import('inquirer');
155
+ vi.mocked(existsSync).mockReturnValue(false);
156
+ const promptMock = vi.mocked(inquirer.default.prompt);
157
+ promptMock.mockResolvedValue({
158
+ tenantId: 'test-tenant',
159
+ apiUrl: 'http://localhost:3002',
160
+ });
161
+ await initCommand({ path: './custom/path' });
162
+ expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('custom/path/inkeep.config.ts'), expect.any(String));
163
+ });
164
+ it('should handle write errors gracefully', async () => {
165
+ const { existsSync, writeFileSync, readdirSync } = await import('node:fs');
166
+ const inquirer = await import('inquirer');
167
+ vi.mocked(existsSync).mockReturnValue(false);
168
+ vi.mocked(readdirSync).mockReturnValue(['package.json']);
169
+ const promptMock = vi.mocked(inquirer.default.prompt);
170
+ promptMock
171
+ .mockResolvedValueOnce({
172
+ confirmedPath: './inkeep.config.ts',
173
+ })
174
+ .mockResolvedValueOnce({
175
+ tenantId: 'test-tenant',
176
+ apiUrl: 'http://localhost:3002',
177
+ });
178
+ vi.mocked(writeFileSync).mockImplementation(() => {
179
+ throw new Error('Permission denied');
180
+ });
181
+ await expect(initCommand()).rejects.toThrow('process.exit called');
182
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to create config file'), expect.any(Error));
183
+ expect(processExitSpy).toHaveBeenCalledWith(1);
184
+ });
185
+ });
186
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,156 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { pullCommand } from '../../commands/pull.js';
3
+ // Mock dependencies
4
+ vi.mock('../../config.js', () => ({
5
+ validateConfiguration: vi.fn().mockResolvedValue({
6
+ tenantId: 'test-tenant',
7
+ projectId: 'test-project',
8
+ apiUrl: 'http://test-api.com',
9
+ sources: {
10
+ tenantId: 'env',
11
+ projectId: 'env',
12
+ apiUrl: 'env',
13
+ },
14
+ modelConfig: {
15
+ model: 'anthropic/claude-3-5-sonnet-20241022',
16
+ providerOptions: { anthropic: {} },
17
+ },
18
+ }),
19
+ }));
20
+ vi.mock('../../commands/pull.llm-generate.js', () => ({
21
+ generateTypeScriptFileWithLLM: vi.fn(),
22
+ }));
23
+ vi.mock('../../utils/graph-converter.js', () => ({
24
+ convertTypeScriptToJson: vi.fn(),
25
+ }));
26
+ vi.mock('../../utils/json-comparator.js', () => ({
27
+ compareJsonObjects: vi.fn(),
28
+ getDifferenceSummary: vi.fn(),
29
+ }));
30
+ vi.mock('node:fs', () => ({
31
+ existsSync: vi.fn().mockReturnValue(true),
32
+ mkdirSync: vi.fn(),
33
+ writeFileSync: vi.fn(),
34
+ }));
35
+ vi.mock('node:path', () => ({
36
+ join: vi.fn((...args) => args.join('/')),
37
+ resolve: vi.fn((...args) => args.join('/')),
38
+ }));
39
+ describe('pull command retry functionality', () => {
40
+ beforeEach(() => {
41
+ vi.clearAllMocks();
42
+ });
43
+ it('should retry LLM generation when validation fails', async () => {
44
+ const { generateTypeScriptFileWithLLM } = await import('../../commands/pull.llm-generate.js');
45
+ const { convertTypeScriptToJson } = await import('../../commands/pull.js');
46
+ const { compareJsonObjects } = await import('../../utils/json-comparator.js');
47
+ // Mock fetch to return graph data
48
+ global.fetch = vi.fn().mockResolvedValue({
49
+ ok: true,
50
+ json: () => Promise.resolve({
51
+ data: {
52
+ id: 'test-graph',
53
+ name: 'Test Graph',
54
+ agents: [],
55
+ },
56
+ }),
57
+ });
58
+ // Mock validation to fail first time, then succeed
59
+ vi.mocked(compareJsonObjects)
60
+ .mockReturnValueOnce({
61
+ isEqual: false,
62
+ differences: [
63
+ {
64
+ path: 'agents[0].id',
65
+ type: 'different',
66
+ value1: 'agent1',
67
+ value2: 'agent2',
68
+ description: 'ID mismatch',
69
+ },
70
+ ],
71
+ stats: { totalKeys: 1, differentKeys: 1, missingKeys: 0, extraKeys: 0 },
72
+ })
73
+ .mockReturnValueOnce({
74
+ isEqual: true,
75
+ differences: [],
76
+ stats: { totalKeys: 1, differentKeys: 0, missingKeys: 0, extraKeys: 0 },
77
+ });
78
+ vi.mocked(convertTypeScriptToJson).mockResolvedValue({
79
+ id: 'test-graph',
80
+ name: 'Test Graph',
81
+ agents: [],
82
+ tools: [],
83
+ contextConfigs: [],
84
+ credentialReferences: [],
85
+ });
86
+ vi.mocked(generateTypeScriptFileWithLLM).mockResolvedValue(undefined);
87
+ // Mock process.exit to prevent actual exit
88
+ const originalExit = process.exit;
89
+ process.exit = vi.fn();
90
+ try {
91
+ await pullCommand('test-graph', { maxRetries: 2 });
92
+ // Verify that generateTypeScriptFileWithLLM was called twice (initial + retry)
93
+ expect(generateTypeScriptFileWithLLM).toHaveBeenCalledTimes(2);
94
+ // Verify retry context was passed on second call
95
+ const secondCall = vi.mocked(generateTypeScriptFileWithLLM).mock.calls[1];
96
+ expect(secondCall[4]).toEqual({
97
+ attempt: 2,
98
+ maxRetries: 2,
99
+ previousDifferences: ['agents[0].id: ID mismatch'],
100
+ });
101
+ }
102
+ finally {
103
+ process.exit = originalExit;
104
+ }
105
+ });
106
+ it('should fail after max retries exceeded', async () => {
107
+ const { generateTypeScriptFileWithLLM } = await import('../../commands/pull.llm-generate.js');
108
+ const { convertTypeScriptToJson } = await import('../../commands/pull.js');
109
+ const { compareJsonObjects } = await import('../../utils/json-comparator.js');
110
+ // Mock fetch to return graph data
111
+ global.fetch = vi.fn().mockResolvedValue({
112
+ ok: true,
113
+ json: () => Promise.resolve({
114
+ data: {
115
+ id: 'test-graph',
116
+ name: 'Test Graph',
117
+ agents: [],
118
+ },
119
+ }),
120
+ });
121
+ // Mock validation to always fail
122
+ vi.mocked(compareJsonObjects).mockReturnValue({
123
+ isEqual: false,
124
+ differences: [
125
+ {
126
+ path: 'agents[0].id',
127
+ type: 'different',
128
+ value1: 'agent1',
129
+ value2: 'agent2',
130
+ description: 'ID mismatch',
131
+ },
132
+ ],
133
+ stats: { totalKeys: 1, differentKeys: 1, missingKeys: 0, extraKeys: 0 },
134
+ });
135
+ vi.mocked(convertTypeScriptToJson).mockResolvedValue({
136
+ id: 'test-graph',
137
+ name: 'Test Graph',
138
+ agents: [],
139
+ tools: [],
140
+ contextConfigs: [],
141
+ credentialReferences: [],
142
+ });
143
+ vi.mocked(generateTypeScriptFileWithLLM).mockResolvedValue(undefined);
144
+ // Mock process.exit to prevent actual exit
145
+ const originalExit = process.exit;
146
+ process.exit = vi.fn();
147
+ try {
148
+ await pullCommand('test-graph', { maxRetries: 2 });
149
+ // Verify that generateTypeScriptFileWithLLM was called the maximum number of times
150
+ expect(generateTypeScriptFileWithLLM).toHaveBeenCalledTimes(2);
151
+ }
152
+ finally {
153
+ process.exit = originalExit;
154
+ }
155
+ });
156
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { convertTypeScriptToJson } from '../../commands/pull.js';
3
+ // Mock the convertTypeScriptToJson function
4
+ vi.mock('../../commands/pull.js', async () => {
5
+ const actual = await vi.importActual('../../commands/pull.js');
6
+ return {
7
+ ...actual,
8
+ convertTypeScriptToJson: vi.fn(),
9
+ };
10
+ });
11
+ describe('Pull Command', () => {
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+ describe('convertTypeScriptToJson', () => {
16
+ it('should convert TypeScript file to JSON using tsx spawn', async () => {
17
+ const mockResult = { id: 'test-graph', name: 'Test Graph' };
18
+ convertTypeScriptToJson.mockResolvedValue(mockResult);
19
+ const result = await convertTypeScriptToJson('test-graph.ts');
20
+ expect(convertTypeScriptToJson).toHaveBeenCalledWith('test-graph.ts');
21
+ expect(result).toEqual(mockResult);
22
+ });
23
+ it('should handle tsx spawn errors', async () => {
24
+ convertTypeScriptToJson.mockRejectedValue(new Error('Failed to load TypeScript file: tsx not found'));
25
+ await expect(convertTypeScriptToJson('test-graph.ts')).rejects.toThrow('Failed to load TypeScript file: tsx not found');
26
+ });
27
+ it('should handle tsx exit with non-zero code', async () => {
28
+ convertTypeScriptToJson.mockRejectedValue(new Error('Conversion failed: Error: Module not found'));
29
+ await expect(convertTypeScriptToJson('test-graph.ts')).rejects.toThrow('Conversion failed: Error: Module not found');
30
+ });
31
+ it('should handle missing JSON markers in tsx output', async () => {
32
+ convertTypeScriptToJson.mockRejectedValue(new Error('JSON markers not found in output'));
33
+ await expect(convertTypeScriptToJson('test-graph.ts')).rejects.toThrow('JSON markers not found in output');
34
+ });
35
+ it('should handle invalid JSON in tsx output', async () => {
36
+ convertTypeScriptToJson.mockRejectedValue(new Error('Failed to parse conversion result'));
37
+ await expect(convertTypeScriptToJson('test-graph.ts')).rejects.toThrow('Failed to parse conversion result');
38
+ });
39
+ it('should handle file not found error', async () => {
40
+ convertTypeScriptToJson.mockRejectedValue(new Error('File not found: nonexistent.ts'));
41
+ await expect(convertTypeScriptToJson('nonexistent.ts')).rejects.toThrow('File not found: nonexistent.ts');
42
+ });
43
+ it('should handle non-TypeScript files directly', async () => {
44
+ const mockResult = { id: 'test-graph' };
45
+ convertTypeScriptToJson.mockResolvedValue(mockResult);
46
+ const result = await convertTypeScriptToJson('test-graph.js');
47
+ expect(result).toEqual(mockResult);
48
+ });
49
+ it('should handle modules with no graph exports', async () => {
50
+ convertTypeScriptToJson.mockRejectedValue(new Error('No AgentGraph exported from configuration file'));
51
+ await expect(convertTypeScriptToJson('test-graph.js')).rejects.toThrow('No AgentGraph exported from configuration file');
52
+ });
53
+ });
54
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { spawn } from 'node:child_process';
3
+ import { pushCommand } from '../../commands/push.js';
4
+ import { existsSync } from 'node:fs';
5
+ // Mock dependencies
6
+ vi.mock('node:fs');
7
+ vi.mock('node:child_process');
8
+ vi.mock('@inkeep/agents-core');
9
+ vi.mock('../../config.js', () => ({
10
+ validateConfiguration: vi.fn().mockResolvedValue({
11
+ tenantId: 'test-tenant',
12
+ projectId: 'test-project',
13
+ managementApiUrl: 'http://localhost:3002',
14
+ sources: {
15
+ tenantId: 'config',
16
+ projectId: 'config',
17
+ managementApiUrl: 'config',
18
+ },
19
+ }),
20
+ }));
21
+ // Store the actual ora mock instance
22
+ let oraInstance;
23
+ vi.mock('ora', () => ({
24
+ default: vi.fn(() => {
25
+ oraInstance = {
26
+ start: vi.fn().mockReturnThis(),
27
+ succeed: vi.fn().mockReturnThis(),
28
+ fail: vi.fn().mockReturnThis(),
29
+ warn: vi.fn().mockReturnThis(),
30
+ stop: vi.fn().mockReturnThis(),
31
+ text: '',
32
+ };
33
+ return oraInstance;
34
+ }),
35
+ }));
36
+ describe('Push Command - TypeScript Spinner Fix', () => {
37
+ let mockSpawn;
38
+ let mockExit;
39
+ beforeEach(() => {
40
+ vi.clearAllMocks();
41
+ // Reset ora instance
42
+ oraInstance = null;
43
+ // Ensure TSX_RUNNING is not set
44
+ delete process.env.TSX_RUNNING;
45
+ // Mock file exists
46
+ existsSync.mockReturnValue(true);
47
+ // Mock process.exit
48
+ mockExit = vi.fn();
49
+ vi.spyOn(process, 'exit').mockImplementation(mockExit);
50
+ // Mock console methods
51
+ vi.spyOn(console, 'log').mockImplementation(vi.fn());
52
+ vi.spyOn(console, 'error').mockImplementation(vi.fn());
53
+ // Setup spawn mock
54
+ mockSpawn = vi.fn().mockReturnValue({
55
+ on: vi.fn((event, callback) => {
56
+ if (event === 'exit') {
57
+ // Simulate successful exit
58
+ setTimeout(() => callback(0), 10);
59
+ }
60
+ }),
61
+ });
62
+ spawn.mockImplementation(mockSpawn);
63
+ });
64
+ it('should stop spinner before spawning tsx process for TypeScript files', async () => {
65
+ await pushCommand('/test/path/graph.ts', {});
66
+ // Wait for async operations
67
+ await new Promise(resolve => setTimeout(resolve, 20));
68
+ // Verify spinner was created and stopped
69
+ expect(oraInstance).toBeDefined();
70
+ expect(oraInstance.start).toHaveBeenCalled();
71
+ expect(oraInstance.stop).toHaveBeenCalled();
72
+ // Verify spinner.stop() was called before spawn
73
+ const stopCallOrder = oraInstance.stop.mock.invocationCallOrder[0];
74
+ const spawnCallOrder = mockSpawn.mock.invocationCallOrder[0];
75
+ expect(stopCallOrder).toBeLessThan(spawnCallOrder);
76
+ });
77
+ it('should spawn tsx process with correct arguments for TypeScript files', async () => {
78
+ const options = {
79
+ tenantId: 'custom-tenant',
80
+ managementApiUrl: 'https://api.example.com',
81
+ configFilePath: '/path/to/config.json',
82
+ };
83
+ await pushCommand('/test/path/graph.ts', options);
84
+ // Wait for async operations
85
+ await new Promise(resolve => setTimeout(resolve, 20));
86
+ // Verify spawn was called with correct arguments
87
+ expect(mockSpawn).toHaveBeenCalledWith('npx', expect.arrayContaining([
88
+ 'tsx',
89
+ expect.stringContaining('index.js'),
90
+ 'push',
91
+ '/test/path/graph.ts',
92
+ '--tenant-id',
93
+ 'custom-tenant',
94
+ '--management-api-url',
95
+ 'https://api.example.com',
96
+ '--config-file-path',
97
+ '/path/to/config.json',
98
+ ]), expect.objectContaining({
99
+ cwd: process.cwd(),
100
+ stdio: 'inherit',
101
+ env: expect.objectContaining({
102
+ TSX_RUNNING: '1',
103
+ }),
104
+ }));
105
+ });
106
+ it('should handle spawn errors correctly without spinner', async () => {
107
+ // Setup spawn to simulate an error
108
+ mockSpawn.mockReturnValue({
109
+ on: vi.fn((event, callback) => {
110
+ if (event === 'error') {
111
+ setTimeout(() => callback(new Error('Spawn failed')), 10);
112
+ }
113
+ }),
114
+ });
115
+ await pushCommand('/test/path/graph.ts', {});
116
+ // Wait for async operations
117
+ await new Promise(resolve => setTimeout(resolve, 20));
118
+ // Verify spinner was stopped before error handling
119
+ expect(oraInstance.stop).toHaveBeenCalled();
120
+ // Verify error was logged without using spinner.fail
121
+ expect(oraInstance.fail).not.toHaveBeenCalled();
122
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Failed to load TypeScript file'));
123
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Error'), 'Spawn failed');
124
+ // Verify process exited with error code
125
+ expect(mockExit).toHaveBeenCalledWith(1);
126
+ });
127
+ });
@@ -0,0 +1 @@
1
+ export {};