@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,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 {};
|