@inkeep/create-agents 0.0.0-chat-to-edit-20251119071712
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 +56 -0
- package/README.md +149 -0
- package/SUPPLEMENTAL_TERMS.md +40 -0
- package/dist/__tests__/e2e/quickstart.test.d.ts +1 -0
- package/dist/__tests__/e2e/quickstart.test.js +178 -0
- package/dist/__tests__/e2e/utils.d.ts +41 -0
- package/dist/__tests__/e2e/utils.js +217 -0
- package/dist/__tests__/templates.test.d.ts +1 -0
- package/dist/__tests__/templates.test.js +283 -0
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +333 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +40 -0
- package/dist/templates.d.ts +17 -0
- package/dist/templates.js +266 -0
- package/dist/utils.d.ts +46 -0
- package/dist/utils.js +460 -0
- package/package.json +74 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path, { dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
/**
|
|
8
|
+
* Run the create-agents CLI with the given arguments
|
|
9
|
+
*/
|
|
10
|
+
export async function runCreateAgentsCLI(args, cwd, timeout = 300000 // 5 minutes default for full flow
|
|
11
|
+
) {
|
|
12
|
+
const cliPath = path.join(__dirname, '../../../src/index.ts');
|
|
13
|
+
try {
|
|
14
|
+
// Run using tsx to execute TypeScript directly
|
|
15
|
+
const result = await execa('tsx', [cliPath, ...args], {
|
|
16
|
+
cwd,
|
|
17
|
+
timeout,
|
|
18
|
+
env: { ...process.env, FORCE_COLOR: '0' }, // Disable colors for easier assertion
|
|
19
|
+
all: true, // Capture combined stdout + stderr in order
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
stdout: result.stdout,
|
|
23
|
+
stderr: result.stderr,
|
|
24
|
+
exitCode: result.exitCode,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
// execa throws on non-zero exit codes, capture the error info
|
|
29
|
+
return {
|
|
30
|
+
stdout: error.stdout || '',
|
|
31
|
+
stderr: error.stderr || '',
|
|
32
|
+
exitCode: error.exitCode || 1,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Run a command in the created project directory
|
|
38
|
+
*/
|
|
39
|
+
export async function runCommand(command, args, cwd, timeout = 120000 // 2 minutes default
|
|
40
|
+
) {
|
|
41
|
+
try {
|
|
42
|
+
const result = await execa(command, args, {
|
|
43
|
+
cwd,
|
|
44
|
+
timeout,
|
|
45
|
+
env: { ...process.env, FORCE_COLOR: '0' },
|
|
46
|
+
shell: true,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
stdout: result.stdout,
|
|
50
|
+
stderr: result.stderr,
|
|
51
|
+
exitCode: result.exitCode,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return {
|
|
56
|
+
stdout: error.stdout || '',
|
|
57
|
+
stderr: error.stderr || '',
|
|
58
|
+
exitCode: error.exitCode || 1,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create a temporary directory for testing
|
|
64
|
+
*/
|
|
65
|
+
export async function createTempDir(prefix = 'create-agents-e2e-') {
|
|
66
|
+
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Clean up a test directory with retries
|
|
70
|
+
*/
|
|
71
|
+
export async function cleanupDir(dir) {
|
|
72
|
+
if (!(await fs.pathExists(dir))) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
// Try multiple times with delays (common in CI)
|
|
77
|
+
for (let i = 0; i < 3; i++) {
|
|
78
|
+
try {
|
|
79
|
+
await fs.remove(dir);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (i === 2)
|
|
84
|
+
throw error; // Last attempt, throw the error
|
|
85
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1s before retry
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// If still failing, try force removal
|
|
91
|
+
if (error.code === 'ENOTEMPTY' || error.code === 'EBUSY') {
|
|
92
|
+
await execa('rm', ['-rf', dir], { shell: true }).catch(() => {
|
|
93
|
+
console.warn(`Failed to clean up ${dir}`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Verify that a file exists and optionally check its contents
|
|
100
|
+
*/
|
|
101
|
+
export async function verifyFile(filePath, expectedContents) {
|
|
102
|
+
const exists = await fs.pathExists(filePath);
|
|
103
|
+
if (!exists) {
|
|
104
|
+
throw new Error(`Expected file to exist: ${filePath}`);
|
|
105
|
+
}
|
|
106
|
+
if (expectedContents) {
|
|
107
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
108
|
+
for (const expected of expectedContents) {
|
|
109
|
+
if (typeof expected === 'string') {
|
|
110
|
+
if (!content.includes(expected)) {
|
|
111
|
+
throw new Error(`Expected file ${filePath} to contain: ${expected}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
if (!expected.test(content)) {
|
|
116
|
+
throw new Error(`Expected file ${filePath} to match pattern: ${expected}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Verify that a directory has the expected structure
|
|
124
|
+
*/
|
|
125
|
+
export async function verifyDirectoryStructure(baseDir, expectedPaths) {
|
|
126
|
+
for (const expectedPath of expectedPaths) {
|
|
127
|
+
const fullPath = path.join(baseDir, expectedPath);
|
|
128
|
+
const exists = await fs.pathExists(fullPath);
|
|
129
|
+
if (!exists) {
|
|
130
|
+
throw new Error(`Expected path to exist: ${fullPath}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Link local monorepo packages to the created project
|
|
136
|
+
* This replaces published @inkeep packages with local versions for testing
|
|
137
|
+
*/
|
|
138
|
+
export async function linkLocalPackages(projectDir, monorepoRoot) {
|
|
139
|
+
const packageJsonPaths = [
|
|
140
|
+
path.join(projectDir, 'package.json'),
|
|
141
|
+
path.join(projectDir, 'apps/manage-api/package.json'),
|
|
142
|
+
path.join(projectDir, 'apps/run-api/package.json'),
|
|
143
|
+
];
|
|
144
|
+
const packageJsons = {};
|
|
145
|
+
for (const packageJsonPath of packageJsonPaths) {
|
|
146
|
+
packageJsons[packageJsonPath] = await fs.readJson(packageJsonPath);
|
|
147
|
+
}
|
|
148
|
+
// Define local @inkeep packages to link
|
|
149
|
+
const inkeepPackages = {
|
|
150
|
+
'@inkeep/agents-sdk': `link:${path.join(monorepoRoot, 'packages/agents-sdk')}`,
|
|
151
|
+
'@inkeep/agents-core': `link:${path.join(monorepoRoot, 'packages/agents-core')}`,
|
|
152
|
+
'@inkeep/agents-manage-api': `link:${path.join(monorepoRoot, 'agents-manage-api')}`,
|
|
153
|
+
'@inkeep/agents-run-api': `link:${path.join(monorepoRoot, 'agents-run-api')}`,
|
|
154
|
+
'@inkeep/agents-cli': `link:${path.join(monorepoRoot, 'agents-cli')}`,
|
|
155
|
+
};
|
|
156
|
+
// Replace package versions with local links
|
|
157
|
+
for (const [pkg, linkPath] of Object.entries(inkeepPackages)) {
|
|
158
|
+
for (const packageJsonPath of packageJsonPaths) {
|
|
159
|
+
if (packageJsons[packageJsonPath].dependencies?.[pkg]) {
|
|
160
|
+
packageJsons[packageJsonPath].dependencies[pkg] = linkPath;
|
|
161
|
+
}
|
|
162
|
+
if (packageJsons[packageJsonPath].devDependencies?.[pkg]) {
|
|
163
|
+
packageJsons[packageJsonPath].devDependencies[pkg] = linkPath;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Write updated package.json
|
|
168
|
+
for (const packageJsonPath of packageJsonPaths) {
|
|
169
|
+
await fs.writeJson(packageJsonPath, packageJsons[packageJsonPath], { spaces: 2 });
|
|
170
|
+
}
|
|
171
|
+
// Reinstall to create the symlinks
|
|
172
|
+
await execa('pnpm', ['install', '--no-frozen-lockfile'], {
|
|
173
|
+
cwd: projectDir,
|
|
174
|
+
env: { ...process.env, FORCE_COLOR: '0' },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Wait for a server to be ready by polling a health endpoint
|
|
179
|
+
*/
|
|
180
|
+
export async function waitForServerReady(url, timeout) {
|
|
181
|
+
const start = Date.now();
|
|
182
|
+
let lastError = null;
|
|
183
|
+
let attempts = 0;
|
|
184
|
+
console.log(`Waiting for server at ${url}...`);
|
|
185
|
+
while (Date.now() - start < timeout) {
|
|
186
|
+
attempts++;
|
|
187
|
+
try {
|
|
188
|
+
const response = await fetch(url, {
|
|
189
|
+
signal: AbortSignal.timeout(5000), // 5 second timeout per request
|
|
190
|
+
});
|
|
191
|
+
if (response.ok) {
|
|
192
|
+
console.log(`✓ Server ready at ${url} after ${attempts} attempts (${Date.now() - start}ms)`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
lastError = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
196
|
+
// Log status every 10 attempts in CI
|
|
197
|
+
if (process.env.CI && attempts % 10 === 0) {
|
|
198
|
+
console.log(`Still waiting for ${url}... (attempt ${attempts}, ${Math.floor((Date.now() - start) / 1000)}s elapsed)`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
// Server not ready yet or connection refused
|
|
203
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
204
|
+
// Log connection errors periodically in CI
|
|
205
|
+
if (process.env.CI && attempts % 15 === 0) {
|
|
206
|
+
console.log(`Connection attempt ${attempts} failed: ${lastError.message}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Wait before next attempt (exponential backoff up to 5s)
|
|
210
|
+
const waitTime = Math.min(1000 + attempts * 100, 5000);
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
212
|
+
}
|
|
213
|
+
// Timeout reached - provide detailed error
|
|
214
|
+
const elapsed = Date.now() - start;
|
|
215
|
+
const errorDetails = lastError ? `: ${lastError.message}` : '';
|
|
216
|
+
throw new Error(`Server not ready at ${url} after ${elapsed}ms (${attempts} attempts)${errorDetails}`);
|
|
217
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { cloneTemplate, replaceContentInFiles, replaceObjectProperties, } from '../templates';
|
|
5
|
+
// Mock dependencies
|
|
6
|
+
vi.mock('fs-extra');
|
|
7
|
+
vi.mock('degit', () => ({
|
|
8
|
+
default: vi.fn(() => ({
|
|
9
|
+
clone: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
})),
|
|
11
|
+
}));
|
|
12
|
+
describe('Template Content Replacement', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
// Setup default fs-extra mocks
|
|
16
|
+
vi.mocked(fs.pathExists).mockResolvedValue(true);
|
|
17
|
+
vi.mocked(fs.readFile).mockResolvedValue('');
|
|
18
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
19
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
20
|
+
});
|
|
21
|
+
describe('replaceObjectProperties', () => {
|
|
22
|
+
it('should replace a simple object property', async () => {
|
|
23
|
+
const content = `export const myProject = project({
|
|
24
|
+
id: 'test-project',
|
|
25
|
+
models: {
|
|
26
|
+
base: {
|
|
27
|
+
model: 'gpt-4o-mini',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});`;
|
|
31
|
+
const replacement = {
|
|
32
|
+
base: {
|
|
33
|
+
model: 'anthropic/claude-3-5-haiku-20241022',
|
|
34
|
+
},
|
|
35
|
+
summarizer: {
|
|
36
|
+
model: 'anthropic/claude-3-5-haiku-20241022',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const result = await replaceObjectProperties(content, { models: replacement });
|
|
40
|
+
expect(result).toContain("'model': 'anthropic/claude-3-5-haiku-20241022'");
|
|
41
|
+
expect(result).toContain("'summarizer'");
|
|
42
|
+
expect(result).not.toContain('gpt-4o-mini');
|
|
43
|
+
});
|
|
44
|
+
it('should handle nested objects correctly', async () => {
|
|
45
|
+
const content = `export const config = {
|
|
46
|
+
models: {
|
|
47
|
+
base: {
|
|
48
|
+
model: 'old-model',
|
|
49
|
+
temperature: 0.5,
|
|
50
|
+
},
|
|
51
|
+
structured: {
|
|
52
|
+
model: 'old-structured',
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
other: 'value'
|
|
56
|
+
};`;
|
|
57
|
+
const replacement = {
|
|
58
|
+
base: {
|
|
59
|
+
model: 'new-model',
|
|
60
|
+
temperature: 0.8,
|
|
61
|
+
},
|
|
62
|
+
structured: {
|
|
63
|
+
model: 'new-structured',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const result = await replaceObjectProperties(content, { models: replacement });
|
|
67
|
+
expect(result).toContain("'model': 'new-model'");
|
|
68
|
+
expect(result).toContain("'model': 'new-structured'");
|
|
69
|
+
expect(result).toContain("'temperature': 0.8");
|
|
70
|
+
expect(result).toContain("other: 'value'");
|
|
71
|
+
expect(result).not.toContain('old-model');
|
|
72
|
+
});
|
|
73
|
+
it('should preserve code structure and formatting', async () => {
|
|
74
|
+
const content = `const project = {
|
|
75
|
+
id: 'test',
|
|
76
|
+
models: {
|
|
77
|
+
base: { model: 'old' }
|
|
78
|
+
},
|
|
79
|
+
description: 'test project'
|
|
80
|
+
};`;
|
|
81
|
+
const replacement = { base: { model: 'new' } };
|
|
82
|
+
const result = await replaceObjectProperties(content, { models: replacement });
|
|
83
|
+
expect(result).toContain("id: 'test'");
|
|
84
|
+
expect(result).toContain("description: 'test project'");
|
|
85
|
+
expect(result).toContain("'model': 'new'");
|
|
86
|
+
});
|
|
87
|
+
it('should handle multiple property replacements', async () => {
|
|
88
|
+
const content = `export const config = {
|
|
89
|
+
models: { base: { model: 'old1' } },
|
|
90
|
+
tools: { search: { enabled: false } },
|
|
91
|
+
data: 'keep this'
|
|
92
|
+
};`;
|
|
93
|
+
const replacements = {
|
|
94
|
+
models: { base: { model: 'new1' } },
|
|
95
|
+
tools: { search: { enabled: true }, newTool: { type: 'api' } },
|
|
96
|
+
};
|
|
97
|
+
const result = await replaceObjectProperties(content, replacements);
|
|
98
|
+
expect(result).toContain("'model': 'new1'");
|
|
99
|
+
expect(result).toContain("'enabled': true");
|
|
100
|
+
expect(result).toContain("'newTool'");
|
|
101
|
+
expect(result).toContain("data: 'keep this'");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('replaceContentInFiles', () => {
|
|
105
|
+
it('should process multiple files with replacements', async () => {
|
|
106
|
+
const replacements = [
|
|
107
|
+
{
|
|
108
|
+
filePath: 'index.ts',
|
|
109
|
+
replacements: {
|
|
110
|
+
models: { base: { model: 'new-model' } },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
filePath: 'config.ts',
|
|
115
|
+
replacements: {
|
|
116
|
+
settings: { debug: true },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
const indexContent = `export const project = { models: { base: { model: 'old' } } };`;
|
|
121
|
+
const configContent = `export const config = { settings: { debug: false } };`;
|
|
122
|
+
vi.mocked(fs.readFile)
|
|
123
|
+
.mockResolvedValueOnce(indexContent)
|
|
124
|
+
.mockResolvedValueOnce(configContent);
|
|
125
|
+
await replaceContentInFiles('/target/path', replacements);
|
|
126
|
+
expect(fs.readFile).toHaveBeenCalledTimes(2);
|
|
127
|
+
expect(fs.readFile).toHaveBeenCalledWith(path.join('/target/path', 'index.ts'), 'utf-8');
|
|
128
|
+
expect(fs.readFile).toHaveBeenCalledWith(path.join('/target/path', 'config.ts'), 'utf-8');
|
|
129
|
+
expect(fs.writeFile).toHaveBeenCalledTimes(2);
|
|
130
|
+
// Verify the content contains the replacements
|
|
131
|
+
const writeCalls = vi.mocked(fs.writeFile).mock.calls;
|
|
132
|
+
expect(writeCalls[0][1]).toContain("'model': 'new-model'");
|
|
133
|
+
expect(writeCalls[1][1]).toContain("'debug': true");
|
|
134
|
+
});
|
|
135
|
+
it('should warn and skip non-existent files', async () => {
|
|
136
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
137
|
+
vi.mocked(fs.pathExists).mockResolvedValue(false);
|
|
138
|
+
const replacements = [
|
|
139
|
+
{
|
|
140
|
+
filePath: 'non-existent.ts',
|
|
141
|
+
replacements: { test: 'value' },
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
await replaceContentInFiles('/target/path', replacements);
|
|
145
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('non-existent.ts'));
|
|
146
|
+
expect(fs.readFile).not.toHaveBeenCalled();
|
|
147
|
+
expect(fs.writeFile).not.toHaveBeenCalled();
|
|
148
|
+
consoleSpy.mockRestore();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe('cloneTemplate with replacements', () => {
|
|
152
|
+
it('should clone template and apply replacements', async () => {
|
|
153
|
+
const degit = await import('degit');
|
|
154
|
+
const mockEmitter = { clone: vi.fn().mockResolvedValue(undefined) };
|
|
155
|
+
vi.mocked(degit.default).mockReturnValue(mockEmitter);
|
|
156
|
+
const replacements = [
|
|
157
|
+
{
|
|
158
|
+
filePath: 'index.ts',
|
|
159
|
+
replacements: {
|
|
160
|
+
models: { base: { model: 'new-model' } },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
const fileContent = `export const project = { models: { base: { model: 'old' } } };`;
|
|
165
|
+
vi.mocked(fs.readFile).mockResolvedValue(fileContent);
|
|
166
|
+
await cloneTemplate('https://github.com/test/repo', '/target/path', replacements);
|
|
167
|
+
expect(mockEmitter.clone).toHaveBeenCalledWith('/target/path');
|
|
168
|
+
expect(fs.readFile).toHaveBeenCalledWith(path.join('/target/path', 'index.ts'), 'utf-8');
|
|
169
|
+
expect(fs.writeFile).toHaveBeenCalledWith(path.join('/target/path', 'index.ts'), expect.stringContaining("'model': 'new-model'"), 'utf-8');
|
|
170
|
+
});
|
|
171
|
+
it('should clone template without replacements when none provided', async () => {
|
|
172
|
+
const degit = await import('degit');
|
|
173
|
+
const mockEmitter = { clone: vi.fn().mockResolvedValue(undefined) };
|
|
174
|
+
vi.mocked(degit.default).mockReturnValue(mockEmitter);
|
|
175
|
+
await cloneTemplate('https://github.com/test/repo', '/target/path');
|
|
176
|
+
expect(mockEmitter.clone).toHaveBeenCalledWith('/target/path');
|
|
177
|
+
expect(fs.readFile).not.toHaveBeenCalled();
|
|
178
|
+
expect(fs.writeFile).not.toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
it('should handle clone errors gracefully', async () => {
|
|
181
|
+
const degit = await import('degit');
|
|
182
|
+
const mockEmitter = { clone: vi.fn().mockRejectedValue(new Error('Clone failed')) };
|
|
183
|
+
vi.mocked(degit.default).mockReturnValue(mockEmitter);
|
|
184
|
+
const processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
185
|
+
throw new Error('process.exit called');
|
|
186
|
+
});
|
|
187
|
+
await expect(cloneTemplate('https://github.com/test/repo', '/target/path')).rejects.toThrow('process.exit called');
|
|
188
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
189
|
+
processExitSpy.mockRestore();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe('Integration tests', () => {
|
|
193
|
+
it('should handle real-world TypeScript project structure', async () => {
|
|
194
|
+
const projectContent = `import { project } from '@inkeep/agents-sdk';
|
|
195
|
+
import { weatherAgent } from './agent/weather-agent';
|
|
196
|
+
|
|
197
|
+
export const myProject = project({
|
|
198
|
+
id: 'weather-project',
|
|
199
|
+
name: 'Weather Project',
|
|
200
|
+
description: 'Weather project template',
|
|
201
|
+
agent: () => [weatherAgent],
|
|
202
|
+
models: {
|
|
203
|
+
base: {
|
|
204
|
+
model: 'gpt-4o-mini',
|
|
205
|
+
},
|
|
206
|
+
structuredOutput: {
|
|
207
|
+
model: 'gpt-4o-mini',
|
|
208
|
+
},
|
|
209
|
+
summarizer: {
|
|
210
|
+
model: 'gpt-4o-mini',
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
});`;
|
|
214
|
+
const newModels = {
|
|
215
|
+
base: {
|
|
216
|
+
model: 'anthropic/claude-3-5-haiku-20241022',
|
|
217
|
+
},
|
|
218
|
+
structuredOutput: {
|
|
219
|
+
model: 'anthropic/claude-3-5-haiku-20241022',
|
|
220
|
+
},
|
|
221
|
+
summarizer: {
|
|
222
|
+
model: 'anthropic/claude-3-5-haiku-20241022',
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
const result = await replaceObjectProperties(projectContent, { models: newModels });
|
|
226
|
+
// Should preserve imports and structure
|
|
227
|
+
expect(result).toContain("import { project } from '@inkeep/agents-sdk'");
|
|
228
|
+
expect(result).toContain("import { weatherAgent } from './agent/weather-agent'");
|
|
229
|
+
expect(result).toContain("id: 'weather-project'");
|
|
230
|
+
expect(result).toContain("name: 'Weather Project'");
|
|
231
|
+
expect(result).toContain('agent: () => [weatherAgent]');
|
|
232
|
+
// Should replace all model configurations
|
|
233
|
+
expect(result).toContain('anthropic/claude-3-5-haiku-20241022');
|
|
234
|
+
expect(result).not.toContain('gpt-4o-mini');
|
|
235
|
+
// Should maintain proper structure
|
|
236
|
+
expect(result).toContain("'base': {");
|
|
237
|
+
expect(result).toContain("'structuredOutput': {");
|
|
238
|
+
expect(result).toContain("'summarizer': {");
|
|
239
|
+
});
|
|
240
|
+
it('should handle edge cases in TypeScript syntax', async () => {
|
|
241
|
+
const content = `export const config = {
|
|
242
|
+
models: {
|
|
243
|
+
base: { model: "double-quotes" },
|
|
244
|
+
other: {
|
|
245
|
+
model: 'single-quotes',
|
|
246
|
+
nested: { value: true }
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
// Comment here
|
|
250
|
+
data: 'preserve me'
|
|
251
|
+
};`;
|
|
252
|
+
const replacement = {
|
|
253
|
+
base: { model: 'new-base' },
|
|
254
|
+
other: { model: 'new-other', nested: { value: false } },
|
|
255
|
+
};
|
|
256
|
+
const result = await replaceObjectProperties(content, { models: replacement });
|
|
257
|
+
expect(result).toContain("'model': 'new-base'");
|
|
258
|
+
expect(result).toContain("'model': 'new-other'");
|
|
259
|
+
expect(result).toContain("'value': false");
|
|
260
|
+
expect(result).toContain('// Comment here');
|
|
261
|
+
expect(result).toContain("data: 'preserve me'");
|
|
262
|
+
});
|
|
263
|
+
it('should inject models property when it does not exist', async () => {
|
|
264
|
+
const content = `export const myProject = project({
|
|
265
|
+
id: 'test-project',
|
|
266
|
+
name: 'Test Project',
|
|
267
|
+
description: 'A test project without models',
|
|
268
|
+
agent: () => [],
|
|
269
|
+
});`;
|
|
270
|
+
const replacement = {
|
|
271
|
+
base: { model: 'anthropic/claude-3-5-haiku-20241022' },
|
|
272
|
+
structuredOutput: { model: 'anthropic/claude-3-5-haiku-20241022' },
|
|
273
|
+
};
|
|
274
|
+
const result = await replaceObjectProperties(content, { models: replacement });
|
|
275
|
+
expect(result).toContain('models:');
|
|
276
|
+
expect(result).toContain("'base': {");
|
|
277
|
+
expect(result).toContain("'model': 'anthropic/claude-3-5-haiku-20241022'");
|
|
278
|
+
expect(result).toContain("'structuredOutput': {");
|
|
279
|
+
expect(result).toContain("id: 'test-project'");
|
|
280
|
+
expect(result).toContain('agent: () => []');
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|