@savestate/cli 0.7.0 → 0.8.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/dist/cli/__tests__/progress.test.d.ts +5 -0
- package/dist/cli/__tests__/progress.test.d.ts.map +1 -0
- package/dist/cli/__tests__/progress.test.js +212 -0
- package/dist/cli/__tests__/progress.test.js.map +1 -0
- package/dist/cli/__tests__/signal-handler.test.d.ts +5 -0
- package/dist/cli/__tests__/signal-handler.test.d.ts.map +1 -0
- package/dist/cli/__tests__/signal-handler.test.js +99 -0
- package/dist/cli/__tests__/signal-handler.test.js.map +1 -0
- package/dist/cli/__tests__/summary.test.d.ts +5 -0
- package/dist/cli/__tests__/summary.test.d.ts.map +1 -0
- package/dist/cli/__tests__/summary.test.js +242 -0
- package/dist/cli/__tests__/summary.test.js.map +1 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/progress.d.ts +86 -0
- package/dist/cli/progress.d.ts.map +1 -0
- package/dist/cli/progress.js +205 -0
- package/dist/cli/progress.js.map +1 -0
- package/dist/cli/prompts.d.ts +49 -0
- package/dist/cli/prompts.d.ts.map +1 -0
- package/dist/cli/prompts.js +266 -0
- package/dist/cli/prompts.js.map +1 -0
- package/dist/cli/signal-handler.d.ts +63 -0
- package/dist/cli/signal-handler.d.ts.map +1 -0
- package/dist/cli/signal-handler.js +165 -0
- package/dist/cli/signal-handler.js.map +1 -0
- package/dist/cli/summary.d.ts +33 -0
- package/dist/cli/summary.d.ts.map +1 -0
- package/dist/cli/summary.js +296 -0
- package/dist/cli/summary.js.map +1 -0
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/migrate.d.ts +18 -4
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +324 -163
- package/dist/commands/migrate.js.map +1 -1
- package/dist/migrate/__tests__/capabilities.test.d.ts +7 -0
- package/dist/migrate/__tests__/capabilities.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/capabilities.test.js +90 -0
- package/dist/migrate/__tests__/capabilities.test.js.map +1 -0
- package/dist/migrate/__tests__/chatgpt-loader.test.d.ts +7 -0
- package/dist/migrate/__tests__/chatgpt-loader.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/chatgpt-loader.test.js +889 -0
- package/dist/migrate/__tests__/chatgpt-loader.test.js.map +1 -0
- package/dist/migrate/__tests__/edge-cases.test.d.ts +7 -0
- package/dist/migrate/__tests__/edge-cases.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/edge-cases.test.js +787 -0
- package/dist/migrate/__tests__/edge-cases.test.js.map +1 -0
- package/dist/migrate/__tests__/error-recovery.test.d.ts +7 -0
- package/dist/migrate/__tests__/error-recovery.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/error-recovery.test.js +461 -0
- package/dist/migrate/__tests__/error-recovery.test.js.map +1 -0
- package/dist/migrate/__tests__/integration.test.d.ts +7 -0
- package/dist/migrate/__tests__/integration.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/integration.test.js +536 -0
- package/dist/migrate/__tests__/integration.test.js.map +1 -0
- package/dist/migrate/__tests__/performance.test.d.ts +7 -0
- package/dist/migrate/__tests__/performance.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/performance.test.js +478 -0
- package/dist/migrate/__tests__/performance.test.js.map +1 -0
- package/dist/migrate/__tests__/registry.test.d.ts +7 -0
- package/dist/migrate/__tests__/registry.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/registry.test.js +167 -0
- package/dist/migrate/__tests__/registry.test.js.map +1 -0
- package/dist/migrate/compatibility.d.ts +47 -0
- package/dist/migrate/compatibility.d.ts.map +1 -0
- package/dist/migrate/compatibility.js +468 -0
- package/dist/migrate/compatibility.js.map +1 -0
- package/dist/migrate/extractors/__tests__/claude.test.d.ts +12 -0
- package/dist/migrate/extractors/__tests__/claude.test.d.ts.map +1 -0
- package/dist/migrate/extractors/__tests__/claude.test.js +789 -0
- package/dist/migrate/extractors/__tests__/claude.test.js.map +1 -0
- package/dist/migrate/extractors/claude.d.ts +69 -0
- package/dist/migrate/extractors/claude.d.ts.map +1 -0
- package/dist/migrate/extractors/claude.js +1136 -0
- package/dist/migrate/extractors/claude.js.map +1 -0
- package/dist/migrate/extractors/registry.js +3 -2
- package/dist/migrate/extractors/registry.js.map +1 -1
- package/dist/migrate/loaders/chatgpt.d.ts +72 -0
- package/dist/migrate/loaders/chatgpt.d.ts.map +1 -0
- package/dist/migrate/loaders/chatgpt.js +691 -0
- package/dist/migrate/loaders/chatgpt.js.map +1 -0
- package/dist/migrate/loaders/registry.js +3 -2
- package/dist/migrate/loaders/registry.js.map +1 -1
- package/dist/migrate/types.d.ts +2 -0
- package/dist/migrate/types.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT Loader Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the ChatGPT loader implementation.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
+
import { mkdir, writeFile, rm, readFile } from 'node:fs/promises';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { ChatGPTLoader } from '../loaders/chatgpt.js';
|
|
12
|
+
// Mock fetch globally
|
|
13
|
+
const mockFetch = vi.fn();
|
|
14
|
+
global.fetch = mockFetch;
|
|
15
|
+
describe('ChatGPTLoader', () => {
|
|
16
|
+
let testWorkDir;
|
|
17
|
+
let loader;
|
|
18
|
+
// Helper to create a test bundle
|
|
19
|
+
function createTestBundle(overrides = {}) {
|
|
20
|
+
return {
|
|
21
|
+
version: '1.0',
|
|
22
|
+
id: 'test-bundle-123',
|
|
23
|
+
source: {
|
|
24
|
+
platform: 'claude',
|
|
25
|
+
extractedAt: '2026-02-10T10:00:00Z',
|
|
26
|
+
extractorVersion: '1.0.0',
|
|
27
|
+
},
|
|
28
|
+
target: {
|
|
29
|
+
platform: 'chatgpt',
|
|
30
|
+
transformedAt: '2026-02-10T11:00:00Z',
|
|
31
|
+
transformerVersion: '1.0.0',
|
|
32
|
+
},
|
|
33
|
+
contents: {
|
|
34
|
+
instructions: {
|
|
35
|
+
content: 'You are a helpful assistant. Be concise and accurate.',
|
|
36
|
+
length: 49,
|
|
37
|
+
},
|
|
38
|
+
memories: {
|
|
39
|
+
entries: [
|
|
40
|
+
{ id: 'm1', content: 'User prefers dark mode', createdAt: '2026-01-01T00:00:00Z', category: 'Preferences' },
|
|
41
|
+
{ id: 'm2', content: 'User works in tech industry', createdAt: '2026-01-02T00:00:00Z', category: 'Work' },
|
|
42
|
+
{ id: 'm3', content: 'User likes TypeScript', createdAt: '2026-01-03T00:00:00Z', category: 'Preferences' },
|
|
43
|
+
],
|
|
44
|
+
count: 3,
|
|
45
|
+
},
|
|
46
|
+
...overrides.contents,
|
|
47
|
+
},
|
|
48
|
+
metadata: {
|
|
49
|
+
totalItems: 4,
|
|
50
|
+
itemCounts: {
|
|
51
|
+
instructions: 1,
|
|
52
|
+
memories: 3,
|
|
53
|
+
conversations: 0,
|
|
54
|
+
files: 0,
|
|
55
|
+
customBots: 0,
|
|
56
|
+
},
|
|
57
|
+
warnings: [],
|
|
58
|
+
errors: [],
|
|
59
|
+
...overrides.metadata,
|
|
60
|
+
},
|
|
61
|
+
...overrides,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Helper to mock successful API responses
|
|
65
|
+
function mockApiSuccess() {
|
|
66
|
+
mockFetch.mockImplementation(async (url, options) => {
|
|
67
|
+
const path = new URL(url).pathname;
|
|
68
|
+
const method = options.method || 'GET';
|
|
69
|
+
// List files
|
|
70
|
+
if (method === 'GET' && path === '/v1/files') {
|
|
71
|
+
return new Response(JSON.stringify({ data: [] }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
72
|
+
}
|
|
73
|
+
// Upload file
|
|
74
|
+
if (method === 'POST' && path === '/v1/files') {
|
|
75
|
+
return new Response(JSON.stringify({
|
|
76
|
+
id: `file_${Date.now()}`,
|
|
77
|
+
object: 'file',
|
|
78
|
+
bytes: 100,
|
|
79
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
80
|
+
filename: 'test.txt',
|
|
81
|
+
purpose: 'assistants',
|
|
82
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
83
|
+
}
|
|
84
|
+
// Delete file
|
|
85
|
+
if (method === 'DELETE' && path.match(/\/v1\/files\/[^/]+$/)) {
|
|
86
|
+
return new Response(JSON.stringify({ deleted: true }), {
|
|
87
|
+
status: 200,
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return new Response(JSON.stringify({ error: { message: 'Not found' } }), { status: 404 });
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
beforeEach(async () => {
|
|
95
|
+
testWorkDir = join(tmpdir(), `chatgpt-loader-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
96
|
+
await mkdir(testWorkDir, { recursive: true });
|
|
97
|
+
mockFetch.mockReset();
|
|
98
|
+
loader = new ChatGPTLoader({
|
|
99
|
+
apiKey: 'test-api-key',
|
|
100
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
101
|
+
outputDir: testWorkDir,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
afterEach(async () => {
|
|
105
|
+
if (existsSync(testWorkDir)) {
|
|
106
|
+
await rm(testWorkDir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
describe('canLoad', () => {
|
|
110
|
+
it('should return true even without API key (generates guidance files)', async () => {
|
|
111
|
+
const loaderNoKey = new ChatGPTLoader({ apiKey: '' });
|
|
112
|
+
const result = await loaderNoKey.canLoad();
|
|
113
|
+
expect(result).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
it('should return true with API key', async () => {
|
|
116
|
+
const result = await loader.canLoad();
|
|
117
|
+
expect(result).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('load - success cases', () => {
|
|
121
|
+
it('should create migration output directory with all components', async () => {
|
|
122
|
+
mockApiSuccess();
|
|
123
|
+
const bundle = createTestBundle();
|
|
124
|
+
const result = await loader.load(bundle, {});
|
|
125
|
+
expect(result.success).toBe(true);
|
|
126
|
+
expect(result.loaded.instructions).toBe(true);
|
|
127
|
+
expect(result.loaded.memories).toBe(3);
|
|
128
|
+
expect(result.created?.projectId).toBeDefined();
|
|
129
|
+
expect(result.errors).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
it('should generate custom instructions file', async () => {
|
|
132
|
+
mockApiSuccess();
|
|
133
|
+
const bundle = createTestBundle();
|
|
134
|
+
await loader.load(bundle, {});
|
|
135
|
+
const instructionsPath = join(testWorkDir, 'custom-instructions.txt');
|
|
136
|
+
expect(existsSync(instructionsPath)).toBe(true);
|
|
137
|
+
const content = await readFile(instructionsPath, 'utf-8');
|
|
138
|
+
expect(content).toBe(bundle.contents.instructions?.content);
|
|
139
|
+
});
|
|
140
|
+
it('should generate memories file with formatted content', async () => {
|
|
141
|
+
mockApiSuccess();
|
|
142
|
+
const bundle = createTestBundle();
|
|
143
|
+
await loader.load(bundle, {});
|
|
144
|
+
const memoriesPath = join(testWorkDir, 'memories.md');
|
|
145
|
+
expect(existsSync(memoriesPath)).toBe(true);
|
|
146
|
+
const content = await readFile(memoriesPath, 'utf-8');
|
|
147
|
+
expect(content).toContain('ChatGPT Memories to Add');
|
|
148
|
+
expect(content).toContain('User prefers dark mode');
|
|
149
|
+
expect(content).toContain('User works in tech industry');
|
|
150
|
+
});
|
|
151
|
+
it('should handle bundle without instructions', async () => {
|
|
152
|
+
mockApiSuccess();
|
|
153
|
+
const bundle = createTestBundle({
|
|
154
|
+
contents: {
|
|
155
|
+
memories: {
|
|
156
|
+
entries: [{ id: 'm1', content: 'Test memory', createdAt: '2026-01-01T00:00:00Z' }],
|
|
157
|
+
count: 1,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
const result = await loader.load(bundle, {});
|
|
162
|
+
expect(result.success).toBe(true);
|
|
163
|
+
expect(result.loaded.instructions).toBe(false);
|
|
164
|
+
expect(result.loaded.memories).toBe(1);
|
|
165
|
+
});
|
|
166
|
+
it('should handle bundle without memories', async () => {
|
|
167
|
+
mockApiSuccess();
|
|
168
|
+
const bundle = createTestBundle({
|
|
169
|
+
contents: {
|
|
170
|
+
instructions: { content: 'Test instructions', length: 17 },
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
const result = await loader.load(bundle, {});
|
|
174
|
+
expect(result.success).toBe(true);
|
|
175
|
+
expect(result.loaded.instructions).toBe(true);
|
|
176
|
+
expect(result.loaded.memories).toBe(0);
|
|
177
|
+
});
|
|
178
|
+
it('should track progress during load', async () => {
|
|
179
|
+
mockApiSuccess();
|
|
180
|
+
const bundle = createTestBundle();
|
|
181
|
+
const progressUpdates = [];
|
|
182
|
+
await loader.load(bundle, {
|
|
183
|
+
onProgress: (progress, message) => {
|
|
184
|
+
progressUpdates.push({ progress, message });
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
expect(progressUpdates.length).toBeGreaterThan(0);
|
|
188
|
+
// Progress should increase monotonically
|
|
189
|
+
let lastProgress = 0;
|
|
190
|
+
for (const update of progressUpdates) {
|
|
191
|
+
expect(update.progress).toBeGreaterThanOrEqual(lastProgress);
|
|
192
|
+
lastProgress = update.progress;
|
|
193
|
+
}
|
|
194
|
+
// Should reach 100%
|
|
195
|
+
expect(progressUpdates[progressUpdates.length - 1].progress).toBe(1.0);
|
|
196
|
+
});
|
|
197
|
+
it('should provide manual steps in result', async () => {
|
|
198
|
+
mockApiSuccess();
|
|
199
|
+
const bundle = createTestBundle();
|
|
200
|
+
const result = await loader.load(bundle, {});
|
|
201
|
+
expect(result.manualSteps).toBeDefined();
|
|
202
|
+
expect(result.manualSteps.length).toBeGreaterThan(0);
|
|
203
|
+
expect(result.manualSteps.some(s => s.includes('Custom Instructions'))).toBe(true);
|
|
204
|
+
expect(result.manualSteps.some(s => s.includes('memories'))).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('load - file uploads', () => {
|
|
208
|
+
it('should upload files to OpenAI when API key is provided', async () => {
|
|
209
|
+
mockApiSuccess();
|
|
210
|
+
// Create a test file
|
|
211
|
+
const testFilePath = join(testWorkDir, 'test-document.txt');
|
|
212
|
+
await writeFile(testFilePath, 'Hello, this is test content');
|
|
213
|
+
const bundle = createTestBundle({
|
|
214
|
+
source: {
|
|
215
|
+
platform: 'claude',
|
|
216
|
+
extractedAt: '2026-02-10T10:00:00Z',
|
|
217
|
+
extractorVersion: '1.0.0',
|
|
218
|
+
bundlePath: testWorkDir, // Required for path validation
|
|
219
|
+
},
|
|
220
|
+
contents: {
|
|
221
|
+
instructions: { content: 'Test', length: 4 },
|
|
222
|
+
files: {
|
|
223
|
+
files: [
|
|
224
|
+
{
|
|
225
|
+
id: 'f1',
|
|
226
|
+
filename: 'test-document.txt',
|
|
227
|
+
mimeType: 'text/plain',
|
|
228
|
+
size: 27,
|
|
229
|
+
path: 'test-document.txt', // Relative path within bundle
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
count: 1,
|
|
233
|
+
totalSize: 27,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
const result = await loader.load(bundle, {});
|
|
238
|
+
expect(result.success).toBe(true);
|
|
239
|
+
expect(result.loaded.files).toBe(1);
|
|
240
|
+
// Verify file upload was called
|
|
241
|
+
const fileCalls = mockFetch.mock.calls.filter((call) => {
|
|
242
|
+
const [url] = call;
|
|
243
|
+
return url.includes('/files');
|
|
244
|
+
});
|
|
245
|
+
expect(fileCalls.length).toBe(1);
|
|
246
|
+
});
|
|
247
|
+
it('should skip files exceeding size limit', async () => {
|
|
248
|
+
mockApiSuccess();
|
|
249
|
+
const testFilePath = join(testWorkDir, 'large-file.txt');
|
|
250
|
+
await writeFile(testFilePath, 'x'.repeat(100));
|
|
251
|
+
const bundle = createTestBundle({
|
|
252
|
+
contents: {
|
|
253
|
+
instructions: { content: 'Test', length: 4 },
|
|
254
|
+
files: {
|
|
255
|
+
files: [
|
|
256
|
+
{
|
|
257
|
+
id: 'f1',
|
|
258
|
+
filename: 'large-file.txt',
|
|
259
|
+
mimeType: 'text/plain',
|
|
260
|
+
size: 600 * 1024 * 1024, // 600MB (exceeds 512MB limit)
|
|
261
|
+
path: testFilePath,
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
count: 1,
|
|
265
|
+
totalSize: 600 * 1024 * 1024,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
const result = await loader.load(bundle, {});
|
|
270
|
+
expect(result.success).toBe(true);
|
|
271
|
+
expect(result.loaded.files).toBe(0);
|
|
272
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('exceeds size limit'));
|
|
273
|
+
});
|
|
274
|
+
it('should warn about missing files within bundle directory', async () => {
|
|
275
|
+
mockApiSuccess();
|
|
276
|
+
const bundle = createTestBundle({
|
|
277
|
+
source: {
|
|
278
|
+
platform: 'claude',
|
|
279
|
+
extractedAt: '2026-02-10T10:00:00Z',
|
|
280
|
+
extractorVersion: '1.0.0',
|
|
281
|
+
bundlePath: testWorkDir,
|
|
282
|
+
},
|
|
283
|
+
contents: {
|
|
284
|
+
instructions: { content: 'Test', length: 4 },
|
|
285
|
+
files: {
|
|
286
|
+
files: [
|
|
287
|
+
{
|
|
288
|
+
id: 'f1',
|
|
289
|
+
filename: 'missing-file.txt',
|
|
290
|
+
mimeType: 'text/plain',
|
|
291
|
+
size: 100,
|
|
292
|
+
path: 'missing-file.txt', // File doesn't exist but path is valid
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
count: 1,
|
|
296
|
+
totalSize: 100,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
const result = await loader.load(bundle, {});
|
|
301
|
+
expect(result.success).toBe(true);
|
|
302
|
+
expect(result.loaded.files).toBe(0);
|
|
303
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('not found'));
|
|
304
|
+
});
|
|
305
|
+
it('should reject files with absolute paths outside bundle (path traversal)', async () => {
|
|
306
|
+
mockApiSuccess();
|
|
307
|
+
const bundle = createTestBundle({
|
|
308
|
+
source: {
|
|
309
|
+
platform: 'claude',
|
|
310
|
+
extractedAt: '2026-02-10T10:00:00Z',
|
|
311
|
+
extractorVersion: '1.0.0',
|
|
312
|
+
bundlePath: testWorkDir,
|
|
313
|
+
},
|
|
314
|
+
contents: {
|
|
315
|
+
instructions: { content: 'Test', length: 4 },
|
|
316
|
+
files: {
|
|
317
|
+
files: [
|
|
318
|
+
{
|
|
319
|
+
id: 'f1',
|
|
320
|
+
filename: 'passwd',
|
|
321
|
+
mimeType: 'text/plain',
|
|
322
|
+
size: 100,
|
|
323
|
+
path: '/etc/passwd', // Absolute path outside bundle
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
count: 1,
|
|
327
|
+
totalSize: 100,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
const result = await loader.load(bundle, {});
|
|
332
|
+
expect(result.success).toBe(true);
|
|
333
|
+
expect(result.loaded.files).toBe(0);
|
|
334
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('Invalid file path'));
|
|
335
|
+
});
|
|
336
|
+
it('should generate file manifest when no API key provided', async () => {
|
|
337
|
+
const loaderNoKey = new ChatGPTLoader({
|
|
338
|
+
apiKey: '',
|
|
339
|
+
outputDir: testWorkDir,
|
|
340
|
+
});
|
|
341
|
+
const testFilePath = join(testWorkDir, 'test-file.txt');
|
|
342
|
+
await writeFile(testFilePath, 'Hello world');
|
|
343
|
+
const bundle = createTestBundle({
|
|
344
|
+
source: {
|
|
345
|
+
platform: 'claude',
|
|
346
|
+
extractedAt: '2026-02-10T10:00:00Z',
|
|
347
|
+
extractorVersion: '1.0.0',
|
|
348
|
+
bundlePath: testWorkDir, // Required for path validation
|
|
349
|
+
},
|
|
350
|
+
contents: {
|
|
351
|
+
instructions: { content: 'Test', length: 4 },
|
|
352
|
+
files: {
|
|
353
|
+
files: [
|
|
354
|
+
{
|
|
355
|
+
id: 'f1',
|
|
356
|
+
filename: 'test-file.txt',
|
|
357
|
+
mimeType: 'text/plain',
|
|
358
|
+
size: 11,
|
|
359
|
+
path: 'test-file.txt', // Relative path within bundle
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
count: 1,
|
|
363
|
+
totalSize: 11,
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
const result = await loaderNoKey.load(bundle, {});
|
|
368
|
+
expect(result.success).toBe(true);
|
|
369
|
+
expect(result.loaded.files).toBe(0); // No files uploaded without API key
|
|
370
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('No API key'));
|
|
371
|
+
const manifestPath = join(testWorkDir, 'files-manifest.md');
|
|
372
|
+
expect(existsSync(manifestPath)).toBe(true);
|
|
373
|
+
const content = await readFile(manifestPath, 'utf-8');
|
|
374
|
+
expect(content).toContain('test-file.txt');
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
describe('load - dry run', () => {
|
|
378
|
+
it('should not create any files in dry run mode', async () => {
|
|
379
|
+
const bundle = createTestBundle();
|
|
380
|
+
const result = await loader.load(bundle, { dryRun: true });
|
|
381
|
+
expect(result.success).toBe(true);
|
|
382
|
+
expect(result.warnings).toContain('Dry run - no changes made');
|
|
383
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
384
|
+
// Should not create output files
|
|
385
|
+
const instructionsPath = join(testWorkDir, 'custom-instructions.txt');
|
|
386
|
+
expect(existsSync(instructionsPath)).toBe(false);
|
|
387
|
+
});
|
|
388
|
+
it('should report potential issues in dry run', async () => {
|
|
389
|
+
const bundle = createTestBundle({
|
|
390
|
+
contents: {
|
|
391
|
+
instructions: { content: 'x'.repeat(2000), length: 2000 }, // Exceeds 1500 char limit
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
const result = await loader.load(bundle, { dryRun: true });
|
|
395
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('would be truncated'));
|
|
396
|
+
});
|
|
397
|
+
it('should report memory limit issues in dry run', async () => {
|
|
398
|
+
const entries = Array.from({ length: 150 }, (_, i) => ({
|
|
399
|
+
id: `m${i}`,
|
|
400
|
+
content: `Memory ${i}`,
|
|
401
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
402
|
+
}));
|
|
403
|
+
const bundle = createTestBundle({
|
|
404
|
+
contents: {
|
|
405
|
+
instructions: { content: 'Test', length: 4 },
|
|
406
|
+
memories: {
|
|
407
|
+
entries,
|
|
408
|
+
count: 150,
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
const result = await loader.load(bundle, { dryRun: true });
|
|
413
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('would be truncated'));
|
|
414
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('150 > 100'));
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
describe('load - error handling', () => {
|
|
418
|
+
it('should handle API errors gracefully for file upload', async () => {
|
|
419
|
+
mockFetch.mockResolvedValue(new Response(JSON.stringify({ error: { message: 'Invalid API key' } }), {
|
|
420
|
+
status: 401,
|
|
421
|
+
}));
|
|
422
|
+
const testFilePath = join(testWorkDir, 'test.txt');
|
|
423
|
+
await writeFile(testFilePath, 'Hello');
|
|
424
|
+
const bundle = createTestBundle({
|
|
425
|
+
source: {
|
|
426
|
+
platform: 'claude',
|
|
427
|
+
extractedAt: '2026-02-10T10:00:00Z',
|
|
428
|
+
extractorVersion: '1.0.0',
|
|
429
|
+
bundlePath: testWorkDir, // Required for path validation
|
|
430
|
+
},
|
|
431
|
+
contents: {
|
|
432
|
+
instructions: { content: 'Test', length: 4 },
|
|
433
|
+
files: {
|
|
434
|
+
files: [
|
|
435
|
+
{
|
|
436
|
+
id: 'f1',
|
|
437
|
+
filename: 'test.txt',
|
|
438
|
+
mimeType: 'text/plain',
|
|
439
|
+
size: 5,
|
|
440
|
+
path: 'test.txt', // Relative path within bundle
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
count: 1,
|
|
444
|
+
totalSize: 5,
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
const result = await loader.load(bundle, {});
|
|
449
|
+
expect(result.success).toBe(true); // Overall success (instructions/memories still work)
|
|
450
|
+
expect(result.loaded.files).toBe(0);
|
|
451
|
+
expect(result.errors).toContainEqual(expect.stringContaining('Failed to upload'));
|
|
452
|
+
});
|
|
453
|
+
it('should retry on rate limit errors', async () => {
|
|
454
|
+
let callCount = 0;
|
|
455
|
+
mockFetch.mockImplementation(async (url) => {
|
|
456
|
+
// Only count and handle file upload requests
|
|
457
|
+
if (url.includes('/files')) {
|
|
458
|
+
callCount++;
|
|
459
|
+
// First 2 calls return 429, third succeeds
|
|
460
|
+
if (callCount <= 2) {
|
|
461
|
+
return new Response(JSON.stringify({ error: { message: 'Rate limited' } }), {
|
|
462
|
+
status: 429,
|
|
463
|
+
headers: { 'retry-after': '0' }, // Immediate retry for test
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return new Response(JSON.stringify({
|
|
467
|
+
id: `file_success_${Date.now()}`,
|
|
468
|
+
object: 'file',
|
|
469
|
+
bytes: 5,
|
|
470
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
471
|
+
filename: 'test.txt',
|
|
472
|
+
purpose: 'assistants',
|
|
473
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
474
|
+
}
|
|
475
|
+
return new Response(JSON.stringify({}), { status: 200 });
|
|
476
|
+
});
|
|
477
|
+
const testFilePath = join(testWorkDir, 'test.txt');
|
|
478
|
+
await writeFile(testFilePath, 'Hello');
|
|
479
|
+
const bundle = createTestBundle({
|
|
480
|
+
source: {
|
|
481
|
+
platform: 'claude',
|
|
482
|
+
extractedAt: '2026-02-10T10:00:00Z',
|
|
483
|
+
extractorVersion: '1.0.0',
|
|
484
|
+
bundlePath: testWorkDir, // Required for path validation
|
|
485
|
+
},
|
|
486
|
+
contents: {
|
|
487
|
+
instructions: { content: 'Test', length: 4 },
|
|
488
|
+
files: {
|
|
489
|
+
files: [
|
|
490
|
+
{
|
|
491
|
+
id: 'f1',
|
|
492
|
+
filename: 'test.txt',
|
|
493
|
+
mimeType: 'text/plain',
|
|
494
|
+
size: 5,
|
|
495
|
+
path: 'test.txt', // Relative path within bundle
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
count: 1,
|
|
499
|
+
totalSize: 5,
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
const loaderWithRetry = new ChatGPTLoader({
|
|
504
|
+
apiKey: 'test-key',
|
|
505
|
+
retryDelayMs: 10,
|
|
506
|
+
maxRetries: 5, // Allow more retries
|
|
507
|
+
outputDir: testWorkDir,
|
|
508
|
+
});
|
|
509
|
+
const result = await loaderWithRetry.load(bundle, {});
|
|
510
|
+
// Verify retries happened - we should have at least 3 calls (2 failures + 1 success)
|
|
511
|
+
expect(callCount).toBeGreaterThanOrEqual(3);
|
|
512
|
+
// After retries, the upload should succeed
|
|
513
|
+
expect(result.success).toBe(true);
|
|
514
|
+
expect(result.loaded.files).toBe(1);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
describe('load - bundle validation', () => {
|
|
518
|
+
it('should reject bundles not transformed for ChatGPT', async () => {
|
|
519
|
+
const bundle = createTestBundle({
|
|
520
|
+
target: {
|
|
521
|
+
platform: 'claude',
|
|
522
|
+
transformedAt: '2026-02-10T11:00:00Z',
|
|
523
|
+
transformerVersion: '1.0.0',
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
await expect(loader.load(bundle, {})).rejects.toThrow('not transformed for ChatGPT');
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
describe('load - custom GPTs handling', () => {
|
|
530
|
+
it('should generate GPT configuration guides', async () => {
|
|
531
|
+
mockApiSuccess();
|
|
532
|
+
const bundle = createTestBundle({
|
|
533
|
+
contents: {
|
|
534
|
+
instructions: { content: 'Test', length: 4 },
|
|
535
|
+
customBots: {
|
|
536
|
+
bots: [
|
|
537
|
+
{
|
|
538
|
+
id: 'gpt1',
|
|
539
|
+
name: 'My Custom GPT',
|
|
540
|
+
description: 'A helpful assistant',
|
|
541
|
+
instructions: 'Be helpful and concise',
|
|
542
|
+
capabilities: ['Web browsing', 'Code interpreter'],
|
|
543
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
count: 1,
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
const result = await loader.load(bundle, {});
|
|
551
|
+
expect(result.success).toBe(true);
|
|
552
|
+
expect(result.loaded.customBots).toBe(0); // Always 0 - requires manual creation
|
|
553
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('GPT configurations exported'));
|
|
554
|
+
// Check GPT guide was created
|
|
555
|
+
const gptFile = join(testWorkDir, 'gpts', 'my_custom_gpt.md');
|
|
556
|
+
expect(existsSync(gptFile)).toBe(true);
|
|
557
|
+
const content = await readFile(gptFile, 'utf-8');
|
|
558
|
+
expect(content).toContain('My Custom GPT');
|
|
559
|
+
expect(content).toContain('Be helpful and concise');
|
|
560
|
+
expect(content).toContain('Web browsing');
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
describe('load state management', () => {
|
|
564
|
+
it('should track and expose load state for checkpointing', async () => {
|
|
565
|
+
mockApiSuccess();
|
|
566
|
+
const bundle = createTestBundle();
|
|
567
|
+
await loader.load(bundle, {});
|
|
568
|
+
const state = loader.getLoadState();
|
|
569
|
+
expect(state.instructionsProcessed).toBe(true);
|
|
570
|
+
expect(state.memoriesProcessed).toBe(true);
|
|
571
|
+
expect(state.outputDir).toBeDefined();
|
|
572
|
+
});
|
|
573
|
+
it('should allow setting load state for resume', async () => {
|
|
574
|
+
const previousState = {
|
|
575
|
+
instructionsProcessed: true,
|
|
576
|
+
memoriesProcessed: true,
|
|
577
|
+
uploadedFileIds: ['file_1', 'file_2'],
|
|
578
|
+
uploadedFilenames: ['a.txt', 'b.txt'],
|
|
579
|
+
lastFileIndex: 1,
|
|
580
|
+
outputDir: '/test/path',
|
|
581
|
+
};
|
|
582
|
+
loader.setLoadState(previousState);
|
|
583
|
+
const state = loader.getLoadState();
|
|
584
|
+
expect(state.instructionsProcessed).toBe(true);
|
|
585
|
+
expect(state.uploadedFileIds).toEqual(['file_1', 'file_2']);
|
|
586
|
+
expect(state.lastFileIndex).toBe(1);
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
describe('getProgress', () => {
|
|
590
|
+
it('should return current progress', async () => {
|
|
591
|
+
mockApiSuccess();
|
|
592
|
+
const bundle = createTestBundle();
|
|
593
|
+
// Before load
|
|
594
|
+
expect(loader.getProgress()).toBe(0);
|
|
595
|
+
await loader.load(bundle, {});
|
|
596
|
+
// After load
|
|
597
|
+
expect(loader.getProgress()).toBe(100);
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
describe('security - path traversal prevention', () => {
|
|
601
|
+
it('should reject file paths with path traversal attempts', async () => {
|
|
602
|
+
mockApiSuccess();
|
|
603
|
+
const bundle = createTestBundle({
|
|
604
|
+
source: {
|
|
605
|
+
platform: 'claude',
|
|
606
|
+
extractedAt: '2026-02-10T10:00:00Z',
|
|
607
|
+
extractorVersion: '1.0.0',
|
|
608
|
+
bundlePath: testWorkDir,
|
|
609
|
+
},
|
|
610
|
+
contents: {
|
|
611
|
+
instructions: { content: 'Test', length: 4 },
|
|
612
|
+
files: {
|
|
613
|
+
files: [
|
|
614
|
+
{
|
|
615
|
+
id: 'f1',
|
|
616
|
+
filename: 'secret.txt',
|
|
617
|
+
mimeType: 'text/plain',
|
|
618
|
+
size: 100,
|
|
619
|
+
path: '../../../etc/passwd', // Path traversal attempt
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
count: 1,
|
|
623
|
+
totalSize: 100,
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
const result = await loader.load(bundle, {});
|
|
628
|
+
expect(result.success).toBe(true);
|
|
629
|
+
expect(result.loaded.files).toBe(0);
|
|
630
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('Invalid file path'));
|
|
631
|
+
});
|
|
632
|
+
it('should reject absolute paths outside bundle directory', async () => {
|
|
633
|
+
mockApiSuccess();
|
|
634
|
+
const bundle = createTestBundle({
|
|
635
|
+
source: {
|
|
636
|
+
platform: 'claude',
|
|
637
|
+
extractedAt: '2026-02-10T10:00:00Z',
|
|
638
|
+
extractorVersion: '1.0.0',
|
|
639
|
+
bundlePath: testWorkDir,
|
|
640
|
+
},
|
|
641
|
+
contents: {
|
|
642
|
+
instructions: { content: 'Test', length: 4 },
|
|
643
|
+
files: {
|
|
644
|
+
files: [
|
|
645
|
+
{
|
|
646
|
+
id: 'f1',
|
|
647
|
+
filename: 'secret.txt',
|
|
648
|
+
mimeType: 'text/plain',
|
|
649
|
+
size: 100,
|
|
650
|
+
path: '/etc/passwd', // Absolute path outside bundle
|
|
651
|
+
},
|
|
652
|
+
],
|
|
653
|
+
count: 1,
|
|
654
|
+
totalSize: 100,
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
const result = await loader.load(bundle, {});
|
|
659
|
+
expect(result.success).toBe(true);
|
|
660
|
+
expect(result.loaded.files).toBe(0);
|
|
661
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('Invalid file path'));
|
|
662
|
+
});
|
|
663
|
+
it('should accept valid file paths within bundle directory', async () => {
|
|
664
|
+
mockApiSuccess();
|
|
665
|
+
// Create a test file inside the work directory
|
|
666
|
+
const testFilePath = join(testWorkDir, 'valid-file.txt');
|
|
667
|
+
await writeFile(testFilePath, 'Valid file content');
|
|
668
|
+
const bundle = createTestBundle({
|
|
669
|
+
source: {
|
|
670
|
+
platform: 'claude',
|
|
671
|
+
extractedAt: '2026-02-10T10:00:00Z',
|
|
672
|
+
extractorVersion: '1.0.0',
|
|
673
|
+
bundlePath: testWorkDir,
|
|
674
|
+
},
|
|
675
|
+
contents: {
|
|
676
|
+
instructions: { content: 'Test', length: 4 },
|
|
677
|
+
files: {
|
|
678
|
+
files: [
|
|
679
|
+
{
|
|
680
|
+
id: 'f1',
|
|
681
|
+
filename: 'valid-file.txt',
|
|
682
|
+
mimeType: 'text/plain',
|
|
683
|
+
size: 18,
|
|
684
|
+
path: 'valid-file.txt', // Relative path within bundle
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
count: 1,
|
|
688
|
+
totalSize: 18,
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
const result = await loader.load(bundle, {});
|
|
693
|
+
expect(result.success).toBe(true);
|
|
694
|
+
expect(result.loaded.files).toBe(1);
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
describe('security - output directory sanitization', () => {
|
|
698
|
+
it('should sanitize output directory with path traversal in projectName', async () => {
|
|
699
|
+
mockApiSuccess();
|
|
700
|
+
// No outputDir in config - tests projectName sanitization
|
|
701
|
+
const loaderCustom = new ChatGPTLoader({
|
|
702
|
+
apiKey: 'test-api-key',
|
|
703
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
704
|
+
});
|
|
705
|
+
const bundle = createTestBundle();
|
|
706
|
+
const result = await loaderCustom.load(bundle, {
|
|
707
|
+
projectName: '../../../tmp/evil-directory', // Path traversal attempt
|
|
708
|
+
});
|
|
709
|
+
expect(result.success).toBe(true);
|
|
710
|
+
// The output directory should be sanitized to just the basename
|
|
711
|
+
expect(result.created?.projectId).toBe('evil-directory');
|
|
712
|
+
expect(result.created?.projectId).not.toContain('..');
|
|
713
|
+
});
|
|
714
|
+
it('should sanitize output directory with absolute path in projectName', async () => {
|
|
715
|
+
mockApiSuccess();
|
|
716
|
+
// No outputDir in config - tests projectName sanitization
|
|
717
|
+
const loaderCustom = new ChatGPTLoader({
|
|
718
|
+
apiKey: 'test-api-key',
|
|
719
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
720
|
+
});
|
|
721
|
+
const bundle = createTestBundle();
|
|
722
|
+
const result = await loaderCustom.load(bundle, {
|
|
723
|
+
projectName: '/tmp/absolute-path-attempt', // Absolute path attempt
|
|
724
|
+
});
|
|
725
|
+
expect(result.success).toBe(true);
|
|
726
|
+
// The output directory should be sanitized to just the basename
|
|
727
|
+
expect(result.created?.projectId).toBe('absolute-path-attempt');
|
|
728
|
+
expect(result.created?.projectId).not.toContain('/tmp');
|
|
729
|
+
});
|
|
730
|
+
it('should fallback to default name when projectName is empty after sanitization', async () => {
|
|
731
|
+
mockApiSuccess();
|
|
732
|
+
// No outputDir in config - tests projectName sanitization
|
|
733
|
+
const loaderCustom = new ChatGPTLoader({
|
|
734
|
+
apiKey: 'test-api-key',
|
|
735
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
736
|
+
});
|
|
737
|
+
const bundle = createTestBundle();
|
|
738
|
+
const result = await loaderCustom.load(bundle, {
|
|
739
|
+
projectName: '/', // Edge case: just a slash
|
|
740
|
+
});
|
|
741
|
+
expect(result.success).toBe(true);
|
|
742
|
+
expect(result.created?.projectId).toBe('chatgpt-migration');
|
|
743
|
+
});
|
|
744
|
+
it('should respect config.outputDir when set (trusted developer input)', async () => {
|
|
745
|
+
mockApiSuccess();
|
|
746
|
+
// When outputDir is configured, it should be used as-is (trusted)
|
|
747
|
+
const loaderWithConfig = new ChatGPTLoader({
|
|
748
|
+
apiKey: 'test-api-key',
|
|
749
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
750
|
+
outputDir: testWorkDir, // Developer-configured path
|
|
751
|
+
});
|
|
752
|
+
const bundle = createTestBundle();
|
|
753
|
+
const result = await loaderWithConfig.load(bundle, {
|
|
754
|
+
projectName: '../../../should-be-ignored', // Should be ignored
|
|
755
|
+
});
|
|
756
|
+
expect(result.success).toBe(true);
|
|
757
|
+
expect(result.created?.projectId).toBe(testWorkDir);
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
describe('instructions truncation', () => {
|
|
761
|
+
it('should truncate instructions exceeding limit and warn', async () => {
|
|
762
|
+
mockApiSuccess();
|
|
763
|
+
const longInstructions = 'x'.repeat(2000); // Exceeds 1500 char limit
|
|
764
|
+
const bundle = createTestBundle({
|
|
765
|
+
contents: {
|
|
766
|
+
instructions: { content: longInstructions, length: 2000 },
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
const result = await loader.load(bundle, {});
|
|
770
|
+
expect(result.success).toBe(true);
|
|
771
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('truncated'));
|
|
772
|
+
// Verify truncated content was written
|
|
773
|
+
const instructionsPath = join(testWorkDir, 'custom-instructions.txt');
|
|
774
|
+
const content = await readFile(instructionsPath, 'utf-8');
|
|
775
|
+
expect(content.length).toBeLessThanOrEqual(1500);
|
|
776
|
+
// Full content should also be saved
|
|
777
|
+
const fullPath = join(testWorkDir, 'custom-instructions-full.txt');
|
|
778
|
+
expect(existsSync(fullPath)).toBe(true);
|
|
779
|
+
});
|
|
780
|
+
it('should use intelligent truncation', async () => {
|
|
781
|
+
mockApiSuccess();
|
|
782
|
+
// Create content with examples that will exceed the 1500 char limit
|
|
783
|
+
const contentWithExamples = `
|
|
784
|
+
You are a helpful assistant.
|
|
785
|
+
|
|
786
|
+
Example: This is a long example that should be removed.
|
|
787
|
+
\`\`\`
|
|
788
|
+
function example() {
|
|
789
|
+
console.log('This is code that might be removed');
|
|
790
|
+
}
|
|
791
|
+
\`\`\`
|
|
792
|
+
|
|
793
|
+
Be concise and accurate.
|
|
794
|
+
|
|
795
|
+
Note: This is a verbose note that might be removed.
|
|
796
|
+
|
|
797
|
+
Always respond in a professional manner.
|
|
798
|
+
|
|
799
|
+
Here are some guidelines:
|
|
800
|
+
- Be helpful and informative
|
|
801
|
+
- Answer questions accurately
|
|
802
|
+
- Stay on topic
|
|
803
|
+
- Be respectful and professional
|
|
804
|
+
`.repeat(10); // Repeat 10x to definitely exceed 1500 char limit
|
|
805
|
+
expect(contentWithExamples.length).toBeGreaterThan(1500); // Verify it exceeds limit
|
|
806
|
+
const bundle = createTestBundle({
|
|
807
|
+
contents: {
|
|
808
|
+
instructions: { content: contentWithExamples, length: contentWithExamples.length },
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
const result = await loader.load(bundle, {});
|
|
812
|
+
expect(result.success).toBe(true);
|
|
813
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('truncated'));
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
describe('memories processing', () => {
|
|
817
|
+
it('should group memories by category', async () => {
|
|
818
|
+
mockApiSuccess();
|
|
819
|
+
const bundle = createTestBundle({
|
|
820
|
+
contents: {
|
|
821
|
+
instructions: { content: 'Test', length: 4 },
|
|
822
|
+
memories: {
|
|
823
|
+
entries: [
|
|
824
|
+
{ id: 'm1', content: 'Memory 1', createdAt: '2026-01-01T00:00:00Z', category: 'Work' },
|
|
825
|
+
{ id: 'm2', content: 'Memory 2', createdAt: '2026-01-02T00:00:00Z', category: 'Preferences' },
|
|
826
|
+
{ id: 'm3', content: 'Memory 3', createdAt: '2026-01-03T00:00:00Z', category: 'Work' },
|
|
827
|
+
],
|
|
828
|
+
count: 3,
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
await loader.load(bundle, {});
|
|
833
|
+
const memoriesPath = join(testWorkDir, 'memories.md');
|
|
834
|
+
const content = await readFile(memoriesPath, 'utf-8');
|
|
835
|
+
expect(content).toContain('## Work');
|
|
836
|
+
expect(content).toContain('## Preferences');
|
|
837
|
+
expect(content).toContain('- Memory 1');
|
|
838
|
+
expect(content).toContain('- Memory 2');
|
|
839
|
+
expect(content).toContain('- Memory 3');
|
|
840
|
+
});
|
|
841
|
+
it('should truncate memories exceeding limit', async () => {
|
|
842
|
+
mockApiSuccess();
|
|
843
|
+
const entries = Array.from({ length: 150 }, (_, i) => ({
|
|
844
|
+
id: `m${i}`,
|
|
845
|
+
content: `Memory ${i}`,
|
|
846
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
847
|
+
}));
|
|
848
|
+
const bundle = createTestBundle({
|
|
849
|
+
contents: {
|
|
850
|
+
instructions: { content: 'Test', length: 4 },
|
|
851
|
+
memories: {
|
|
852
|
+
entries,
|
|
853
|
+
count: 150,
|
|
854
|
+
},
|
|
855
|
+
},
|
|
856
|
+
});
|
|
857
|
+
const result = await loader.load(bundle, {});
|
|
858
|
+
expect(result.success).toBe(true);
|
|
859
|
+
expect(result.loaded.memories).toBe(100); // Truncated to limit
|
|
860
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('Memories truncated'));
|
|
861
|
+
});
|
|
862
|
+
it('should also write memories as JSON', async () => {
|
|
863
|
+
mockApiSuccess();
|
|
864
|
+
const bundle = createTestBundle();
|
|
865
|
+
await loader.load(bundle, {});
|
|
866
|
+
const jsonPath = join(testWorkDir, 'memories.json');
|
|
867
|
+
expect(existsSync(jsonPath)).toBe(true);
|
|
868
|
+
const content = await readFile(jsonPath, 'utf-8');
|
|
869
|
+
const parsed = JSON.parse(content);
|
|
870
|
+
expect(parsed).toBeInstanceOf(Array);
|
|
871
|
+
expect(parsed.length).toBe(3);
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
describe('README generation', () => {
|
|
875
|
+
it('should generate README with migration summary', async () => {
|
|
876
|
+
mockApiSuccess();
|
|
877
|
+
const bundle = createTestBundle();
|
|
878
|
+
await loader.load(bundle, {});
|
|
879
|
+
const readmePath = join(testWorkDir, 'README.md');
|
|
880
|
+
expect(existsSync(readmePath)).toBe(true);
|
|
881
|
+
const content = await readFile(readmePath, 'utf-8');
|
|
882
|
+
expect(content).toContain('ChatGPT Migration Package');
|
|
883
|
+
expect(content).toContain('Custom Instructions');
|
|
884
|
+
expect(content).toContain('Memories');
|
|
885
|
+
expect(content).toContain('Manual Steps Required');
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
//# sourceMappingURL=chatgpt-loader.test.js.map
|