@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,789 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Extractor Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for extracting data from Claude:
|
|
5
|
+
* - System prompts (from projects)
|
|
6
|
+
* - Project knowledge documents
|
|
7
|
+
* - Project files (attachments)
|
|
8
|
+
* - Artifacts
|
|
9
|
+
* - Conversations
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { rm, mkdir, writeFile, readFile, readdir } from 'node:fs/promises';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { ClaudeExtractor } from '../claude.js';
|
|
17
|
+
describe('ClaudeExtractor', () => {
|
|
18
|
+
let testDir;
|
|
19
|
+
let exportDir;
|
|
20
|
+
let workDir;
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
// Create unique temp directories for each test
|
|
23
|
+
const testId = `savestate-claude-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
24
|
+
testDir = join(tmpdir(), testId);
|
|
25
|
+
exportDir = join(testDir, 'export');
|
|
26
|
+
workDir = join(testDir, 'work');
|
|
27
|
+
await mkdir(exportDir, { recursive: true });
|
|
28
|
+
await mkdir(workDir, { recursive: true });
|
|
29
|
+
});
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
// Clean up test directories
|
|
32
|
+
if (existsSync(testDir)) {
|
|
33
|
+
await rm(testDir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
// ─── Helper Functions ────────────────────────────────────────
|
|
38
|
+
async function createMockExport(options = {}) {
|
|
39
|
+
const { conversations = true, projects = true, files = false, conversationCount = 3, includeArtifacts = false, includeAttachments = false, } = options;
|
|
40
|
+
// Create conversations.json
|
|
41
|
+
if (conversations) {
|
|
42
|
+
const convs = [];
|
|
43
|
+
for (let i = 0; i < conversationCount; i++) {
|
|
44
|
+
const chatMessages = [];
|
|
45
|
+
// Create message chain
|
|
46
|
+
for (let j = 0; j < 5; j++) {
|
|
47
|
+
const isHuman = j % 2 === 0;
|
|
48
|
+
const message = {
|
|
49
|
+
uuid: `msg_${i}_${j}`,
|
|
50
|
+
text: `Test message ${j} from ${isHuman ? 'human' : 'assistant'} in conversation ${i + 1}`,
|
|
51
|
+
sender: isHuman ? 'human' : 'assistant',
|
|
52
|
+
created_at: new Date(Date.now() + j * 60000).toISOString(),
|
|
53
|
+
updated_at: new Date(Date.now() + j * 60000).toISOString(),
|
|
54
|
+
};
|
|
55
|
+
// Add artifact to last assistant message
|
|
56
|
+
if (includeArtifacts && !isHuman && j === 3) {
|
|
57
|
+
message.content = [
|
|
58
|
+
{ type: 'text', text: 'Here is the code:' },
|
|
59
|
+
{
|
|
60
|
+
type: 'artifact',
|
|
61
|
+
artifact: {
|
|
62
|
+
id: `artifact_${i}`,
|
|
63
|
+
type: 'application/vnd.ant.code',
|
|
64
|
+
title: `Example Code ${i}`,
|
|
65
|
+
content: `function example${i}() {\n return "hello";\n}`,
|
|
66
|
+
language: 'javascript',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
// Add attachments to human messages
|
|
72
|
+
if (includeAttachments && isHuman && j === 0) {
|
|
73
|
+
message.attachments = [
|
|
74
|
+
{
|
|
75
|
+
id: `attach_${i}`,
|
|
76
|
+
file_name: `document_${i}.pdf`,
|
|
77
|
+
file_size: 1024 * (i + 1),
|
|
78
|
+
file_type: 'application/pdf',
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
chatMessages.push(message);
|
|
83
|
+
}
|
|
84
|
+
convs.push({
|
|
85
|
+
uuid: `conv_${i + 1}`,
|
|
86
|
+
name: `Test Conversation ${i + 1}`,
|
|
87
|
+
created_at: new Date(Date.now() - i * 86400000).toISOString(),
|
|
88
|
+
updated_at: new Date(Date.now() - i * 86400000 + 3600000).toISOString(),
|
|
89
|
+
project_uuid: projects ? `proj_${(i % 2) + 1}` : undefined,
|
|
90
|
+
chat_messages: chatMessages,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
await writeFile(join(exportDir, 'conversations.json'), JSON.stringify(convs));
|
|
94
|
+
}
|
|
95
|
+
// Create projects.json
|
|
96
|
+
if (projects) {
|
|
97
|
+
const projs = [
|
|
98
|
+
{
|
|
99
|
+
id: 'proj_1',
|
|
100
|
+
name: 'My First Project',
|
|
101
|
+
description: 'A test project for development',
|
|
102
|
+
prompt_template: 'You are a helpful coding assistant. Be concise and technical.',
|
|
103
|
+
created_at: '2024-01-01T10:00:00Z',
|
|
104
|
+
updated_at: '2024-01-02T10:00:00Z',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'proj_2',
|
|
108
|
+
name: 'Research Project',
|
|
109
|
+
description: 'For academic research',
|
|
110
|
+
prompt_template: 'You are a research assistant. Cite sources when possible. Be thorough.',
|
|
111
|
+
created_at: '2024-01-03T10:00:00Z',
|
|
112
|
+
updated_at: '2024-01-04T10:00:00Z',
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
await writeFile(join(exportDir, 'projects.json'), JSON.stringify(projs));
|
|
116
|
+
}
|
|
117
|
+
// Create files directory
|
|
118
|
+
if (files) {
|
|
119
|
+
const filesDir = join(exportDir, 'files');
|
|
120
|
+
await mkdir(filesDir, { recursive: true });
|
|
121
|
+
await writeFile(join(filesDir, 'readme.md'), '# Project Readme\n\nThis is a test file.');
|
|
122
|
+
await writeFile(join(filesDir, 'config.json'), JSON.stringify({ setting: 'value', enabled: true }));
|
|
123
|
+
await writeFile(join(filesDir, 'data.csv'), 'name,value\ntest,123\nexample,456');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// ─── Basic Functionality ─────────────────────────────────────
|
|
127
|
+
describe('constructor and basic properties', () => {
|
|
128
|
+
it('should have correct platform and version', () => {
|
|
129
|
+
const extractor = new ClaudeExtractor();
|
|
130
|
+
expect(extractor.platform).toBe('claude');
|
|
131
|
+
expect(extractor.version).toBe('1.0.0');
|
|
132
|
+
});
|
|
133
|
+
it('should initialize with default config', () => {
|
|
134
|
+
const extractor = new ClaudeExtractor();
|
|
135
|
+
expect(extractor.getProgress()).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
it('should accept custom config', () => {
|
|
138
|
+
const extractor = new ClaudeExtractor({
|
|
139
|
+
exportPath: '/path/to/export',
|
|
140
|
+
maxProjects: 5,
|
|
141
|
+
projectIds: ['proj_1', 'proj_2'],
|
|
142
|
+
});
|
|
143
|
+
expect(extractor.platform).toBe('claude');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
// ─── canExtract ──────────────────────────────────────────────
|
|
147
|
+
describe('canExtract', () => {
|
|
148
|
+
it('should return true when export path exists', async () => {
|
|
149
|
+
await createMockExport();
|
|
150
|
+
const extractor = new ClaudeExtractor({
|
|
151
|
+
exportPath: exportDir,
|
|
152
|
+
});
|
|
153
|
+
const canExtract = await extractor.canExtract();
|
|
154
|
+
expect(canExtract).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
it('should return false when export path does not exist', async () => {
|
|
157
|
+
const extractor = new ClaudeExtractor({
|
|
158
|
+
exportPath: '/nonexistent/path',
|
|
159
|
+
});
|
|
160
|
+
const canExtract = await extractor.canExtract();
|
|
161
|
+
expect(canExtract).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
it('should return false when no config provided', async () => {
|
|
164
|
+
const extractor = new ClaudeExtractor();
|
|
165
|
+
const canExtract = await extractor.canExtract();
|
|
166
|
+
expect(canExtract).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
// ─── Conversation Extraction ─────────────────────────────────
|
|
170
|
+
describe('conversation extraction', () => {
|
|
171
|
+
it('should extract conversations from export', async () => {
|
|
172
|
+
await createMockExport({ conversations: true, conversationCount: 3 });
|
|
173
|
+
const extractor = new ClaudeExtractor({
|
|
174
|
+
exportPath: exportDir,
|
|
175
|
+
});
|
|
176
|
+
const bundle = await extractor.extract({
|
|
177
|
+
workDir,
|
|
178
|
+
include: ['conversations'],
|
|
179
|
+
});
|
|
180
|
+
expect(bundle.contents.conversations).toBeDefined();
|
|
181
|
+
expect(bundle.contents.conversations.count).toBe(3);
|
|
182
|
+
expect(bundle.contents.conversations.path).toBe('conversations/');
|
|
183
|
+
// Check that conversation files were created
|
|
184
|
+
const convDir = join(workDir, 'conversations');
|
|
185
|
+
expect(existsSync(convDir)).toBe(true);
|
|
186
|
+
const files = await readdir(convDir);
|
|
187
|
+
expect(files.length).toBe(3);
|
|
188
|
+
});
|
|
189
|
+
it('should extract message content correctly', async () => {
|
|
190
|
+
await createMockExport({ conversations: true, conversationCount: 1 });
|
|
191
|
+
const extractor = new ClaudeExtractor({
|
|
192
|
+
exportPath: exportDir,
|
|
193
|
+
});
|
|
194
|
+
const bundle = await extractor.extract({
|
|
195
|
+
workDir,
|
|
196
|
+
include: ['conversations'],
|
|
197
|
+
});
|
|
198
|
+
expect(bundle.contents.conversations.messageCount).toBeGreaterThan(0);
|
|
199
|
+
expect(bundle.contents.conversations.summaries).toBeDefined();
|
|
200
|
+
expect(bundle.contents.conversations.summaries.length).toBe(1);
|
|
201
|
+
expect(bundle.contents.conversations.summaries[0].title).toBe('Test Conversation 1');
|
|
202
|
+
});
|
|
203
|
+
it('should extract artifacts from conversations', async () => {
|
|
204
|
+
await createMockExport({
|
|
205
|
+
conversations: true,
|
|
206
|
+
conversationCount: 2,
|
|
207
|
+
includeArtifacts: true,
|
|
208
|
+
});
|
|
209
|
+
const extractor = new ClaudeExtractor({
|
|
210
|
+
exportPath: exportDir,
|
|
211
|
+
});
|
|
212
|
+
const bundle = await extractor.extract({
|
|
213
|
+
workDir,
|
|
214
|
+
include: ['conversations', 'files'],
|
|
215
|
+
});
|
|
216
|
+
// Artifacts should be extracted as files
|
|
217
|
+
expect(bundle.contents.files).toBeDefined();
|
|
218
|
+
expect(bundle.contents.files.count).toBeGreaterThan(0);
|
|
219
|
+
// Check that artifact files were created
|
|
220
|
+
const artifactsDir = join(workDir, 'artifacts');
|
|
221
|
+
expect(existsSync(artifactsDir)).toBe(true);
|
|
222
|
+
const files = await readdir(artifactsDir);
|
|
223
|
+
expect(files.length).toBe(2); // One artifact per conversation
|
|
224
|
+
expect(files.some((f) => f.endsWith('.js'))).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
it('should handle conversations with attachments', async () => {
|
|
227
|
+
await createMockExport({
|
|
228
|
+
conversations: true,
|
|
229
|
+
conversationCount: 1,
|
|
230
|
+
includeAttachments: true,
|
|
231
|
+
});
|
|
232
|
+
const extractor = new ClaudeExtractor({
|
|
233
|
+
exportPath: exportDir,
|
|
234
|
+
});
|
|
235
|
+
const bundle = await extractor.extract({
|
|
236
|
+
workDir,
|
|
237
|
+
include: ['conversations'],
|
|
238
|
+
});
|
|
239
|
+
// Read the conversation file and check for attachments
|
|
240
|
+
const convDir = join(workDir, 'conversations');
|
|
241
|
+
const files = await readdir(convDir);
|
|
242
|
+
const convContent = await readFile(join(convDir, files[0]), 'utf-8');
|
|
243
|
+
const conv = JSON.parse(convContent);
|
|
244
|
+
// Find a message with attachments
|
|
245
|
+
const hasAttachments = conv.messages.some((m) => m.attachments && m.attachments.length > 0);
|
|
246
|
+
expect(hasAttachments).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
// ─── Project Extraction ──────────────────────────────────────
|
|
250
|
+
describe('project extraction', () => {
|
|
251
|
+
it('should extract projects as customBots', async () => {
|
|
252
|
+
await createMockExport({ projects: true, conversations: false });
|
|
253
|
+
const extractor = new ClaudeExtractor({
|
|
254
|
+
exportPath: exportDir,
|
|
255
|
+
});
|
|
256
|
+
const bundle = await extractor.extract({
|
|
257
|
+
workDir,
|
|
258
|
+
include: ['customBots'],
|
|
259
|
+
});
|
|
260
|
+
expect(bundle.contents.customBots).toBeDefined();
|
|
261
|
+
expect(bundle.contents.customBots.count).toBe(2);
|
|
262
|
+
expect(bundle.contents.customBots.bots.length).toBe(2);
|
|
263
|
+
const bot1 = bundle.contents.customBots.bots.find((b) => b.id === 'proj_1');
|
|
264
|
+
expect(bot1).toBeDefined();
|
|
265
|
+
expect(bot1.name).toBe('My First Project');
|
|
266
|
+
expect(bot1.instructions).toContain('helpful coding assistant');
|
|
267
|
+
});
|
|
268
|
+
it('should combine project system prompts into instructions', async () => {
|
|
269
|
+
await createMockExport({ projects: true, conversations: false });
|
|
270
|
+
const extractor = new ClaudeExtractor({
|
|
271
|
+
exportPath: exportDir,
|
|
272
|
+
});
|
|
273
|
+
const bundle = await extractor.extract({
|
|
274
|
+
workDir,
|
|
275
|
+
include: ['customBots', 'instructions'],
|
|
276
|
+
});
|
|
277
|
+
expect(bundle.contents.instructions).toBeDefined();
|
|
278
|
+
expect(bundle.contents.instructions.content).toContain('My First Project');
|
|
279
|
+
expect(bundle.contents.instructions.content).toContain('Research Project');
|
|
280
|
+
expect(bundle.contents.instructions.sections).toBeDefined();
|
|
281
|
+
expect(bundle.contents.instructions.sections.length).toBeGreaterThan(0);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
// ─── File Extraction ─────────────────────────────────────────
|
|
285
|
+
describe('file extraction', () => {
|
|
286
|
+
it('should extract files from files directory', async () => {
|
|
287
|
+
await createMockExport({ files: true, conversations: false, projects: false });
|
|
288
|
+
const extractor = new ClaudeExtractor({
|
|
289
|
+
exportPath: exportDir,
|
|
290
|
+
});
|
|
291
|
+
const bundle = await extractor.extract({
|
|
292
|
+
workDir,
|
|
293
|
+
include: ['files'],
|
|
294
|
+
});
|
|
295
|
+
expect(bundle.contents.files).toBeDefined();
|
|
296
|
+
expect(bundle.contents.files.count).toBe(3);
|
|
297
|
+
expect(bundle.contents.files.totalSize).toBeGreaterThan(0);
|
|
298
|
+
// Check files were copied
|
|
299
|
+
const filesDir = join(workDir, 'files');
|
|
300
|
+
expect(existsSync(filesDir)).toBe(true);
|
|
301
|
+
const files = await readdir(filesDir);
|
|
302
|
+
expect(files).toContain('readme.md');
|
|
303
|
+
expect(files).toContain('config.json');
|
|
304
|
+
expect(files).toContain('data.csv');
|
|
305
|
+
});
|
|
306
|
+
it('should correctly identify MIME types', async () => {
|
|
307
|
+
await createMockExport({ files: true, conversations: false, projects: false });
|
|
308
|
+
const extractor = new ClaudeExtractor({
|
|
309
|
+
exportPath: exportDir,
|
|
310
|
+
});
|
|
311
|
+
const bundle = await extractor.extract({
|
|
312
|
+
workDir,
|
|
313
|
+
include: ['files'],
|
|
314
|
+
});
|
|
315
|
+
const mdFile = bundle.contents.files.files.find((f) => f.filename === 'readme.md');
|
|
316
|
+
expect(mdFile).toBeDefined();
|
|
317
|
+
expect(mdFile.mimeType).toBe('text/markdown');
|
|
318
|
+
const jsonFile = bundle.contents.files.files.find((f) => f.filename === 'config.json');
|
|
319
|
+
expect(jsonFile).toBeDefined();
|
|
320
|
+
expect(jsonFile.mimeType).toBe('application/json');
|
|
321
|
+
const csvFile = bundle.contents.files.files.find((f) => f.filename === 'data.csv');
|
|
322
|
+
expect(csvFile).toBeDefined();
|
|
323
|
+
expect(csvFile.mimeType).toBe('text/csv');
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
// ─── Progress Reporting ──────────────────────────────────────
|
|
327
|
+
describe('progress reporting', () => {
|
|
328
|
+
it('should report progress during extraction', async () => {
|
|
329
|
+
await createMockExport({ conversations: true, conversationCount: 5 });
|
|
330
|
+
const extractor = new ClaudeExtractor({
|
|
331
|
+
exportPath: exportDir,
|
|
332
|
+
});
|
|
333
|
+
const progressUpdates = [];
|
|
334
|
+
await extractor.extract({
|
|
335
|
+
workDir,
|
|
336
|
+
onProgress: (progress, message) => {
|
|
337
|
+
progressUpdates.push({ progress, message });
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
expect(progressUpdates.length).toBeGreaterThan(0);
|
|
341
|
+
// Progress should start low and end at 1.0
|
|
342
|
+
expect(progressUpdates[0].progress).toBeLessThan(0.5);
|
|
343
|
+
expect(progressUpdates[progressUpdates.length - 1].progress).toBe(1.0);
|
|
344
|
+
// Final message should indicate completion
|
|
345
|
+
expect(progressUpdates[progressUpdates.length - 1].message).toContain('complete');
|
|
346
|
+
});
|
|
347
|
+
it('should update getProgress during extraction', async () => {
|
|
348
|
+
await createMockExport({ conversations: true, conversationCount: 3 });
|
|
349
|
+
const extractor = new ClaudeExtractor({
|
|
350
|
+
exportPath: exportDir,
|
|
351
|
+
});
|
|
352
|
+
// Progress should be 0 before extraction
|
|
353
|
+
expect(extractor.getProgress()).toBe(0);
|
|
354
|
+
await extractor.extract({
|
|
355
|
+
workDir,
|
|
356
|
+
onProgress: () => {
|
|
357
|
+
// During extraction, progress should be updating
|
|
358
|
+
// (hard to test exact values due to async nature)
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
// Progress should be 100 after extraction
|
|
362
|
+
expect(extractor.getProgress()).toBe(100);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
// ─── Bundle Format ───────────────────────────────────────────
|
|
366
|
+
describe('bundle format', () => {
|
|
367
|
+
it('should produce valid MigrationBundle structure', async () => {
|
|
368
|
+
await createMockExport({
|
|
369
|
+
conversations: true,
|
|
370
|
+
projects: true,
|
|
371
|
+
files: true,
|
|
372
|
+
});
|
|
373
|
+
const extractor = new ClaudeExtractor({
|
|
374
|
+
exportPath: exportDir,
|
|
375
|
+
});
|
|
376
|
+
const bundle = await extractor.extract({ workDir });
|
|
377
|
+
// Check required fields
|
|
378
|
+
expect(bundle.version).toBe('1.0');
|
|
379
|
+
expect(bundle.id).toMatch(/^bundle_[a-f0-9]+$/);
|
|
380
|
+
expect(bundle.source).toBeDefined();
|
|
381
|
+
expect(bundle.source.platform).toBe('claude');
|
|
382
|
+
expect(bundle.source.extractedAt).toBeDefined();
|
|
383
|
+
expect(bundle.source.extractorVersion).toBe('1.0.0');
|
|
384
|
+
expect(bundle.contents).toBeDefined();
|
|
385
|
+
expect(bundle.metadata).toBeDefined();
|
|
386
|
+
});
|
|
387
|
+
it('should have correct metadata counts', async () => {
|
|
388
|
+
await createMockExport({
|
|
389
|
+
conversations: true,
|
|
390
|
+
conversationCount: 3,
|
|
391
|
+
projects: true,
|
|
392
|
+
files: true,
|
|
393
|
+
});
|
|
394
|
+
const extractor = new ClaudeExtractor({
|
|
395
|
+
exportPath: exportDir,
|
|
396
|
+
});
|
|
397
|
+
const bundle = await extractor.extract({ workDir });
|
|
398
|
+
expect(bundle.metadata.itemCounts).toBeDefined();
|
|
399
|
+
expect(bundle.metadata.itemCounts.conversations).toBe(3);
|
|
400
|
+
expect(bundle.metadata.itemCounts.customBots).toBe(2);
|
|
401
|
+
expect(bundle.metadata.itemCounts.files).toBeGreaterThan(0);
|
|
402
|
+
expect(bundle.metadata.totalItems).toBe(bundle.metadata.itemCounts.instructions +
|
|
403
|
+
bundle.metadata.itemCounts.memories +
|
|
404
|
+
bundle.metadata.itemCounts.conversations +
|
|
405
|
+
bundle.metadata.itemCounts.files +
|
|
406
|
+
bundle.metadata.itemCounts.customBots);
|
|
407
|
+
});
|
|
408
|
+
it('should include warnings and errors arrays', async () => {
|
|
409
|
+
await createMockExport({ conversations: true });
|
|
410
|
+
const extractor = new ClaudeExtractor({
|
|
411
|
+
exportPath: exportDir,
|
|
412
|
+
});
|
|
413
|
+
const bundle = await extractor.extract({ workDir });
|
|
414
|
+
expect(bundle.metadata.warnings).toBeDefined();
|
|
415
|
+
expect(Array.isArray(bundle.metadata.warnings)).toBe(true);
|
|
416
|
+
expect(bundle.metadata.errors).toBeDefined();
|
|
417
|
+
expect(Array.isArray(bundle.metadata.errors)).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
// ─── Error Handling ──────────────────────────────────────────
|
|
421
|
+
describe('error handling', () => {
|
|
422
|
+
it('should handle missing conversations gracefully', async () => {
|
|
423
|
+
// Create export with only projects
|
|
424
|
+
await createMockExport({ conversations: false, projects: true });
|
|
425
|
+
const extractor = new ClaudeExtractor({
|
|
426
|
+
exportPath: exportDir,
|
|
427
|
+
});
|
|
428
|
+
const bundle = await extractor.extract({
|
|
429
|
+
workDir,
|
|
430
|
+
include: ['conversations', 'customBots'],
|
|
431
|
+
});
|
|
432
|
+
// Should not have conversations
|
|
433
|
+
expect(bundle.contents.conversations).toBeUndefined();
|
|
434
|
+
// Should still have projects
|
|
435
|
+
expect(bundle.contents.customBots).toBeDefined();
|
|
436
|
+
});
|
|
437
|
+
it('should handle malformed JSON gracefully', async () => {
|
|
438
|
+
await mkdir(exportDir, { recursive: true });
|
|
439
|
+
await writeFile(join(exportDir, 'conversations.json'), '{ invalid json }');
|
|
440
|
+
const extractor = new ClaudeExtractor({
|
|
441
|
+
exportPath: exportDir,
|
|
442
|
+
});
|
|
443
|
+
const bundle = await extractor.extract({
|
|
444
|
+
workDir,
|
|
445
|
+
include: ['conversations'],
|
|
446
|
+
});
|
|
447
|
+
// Should record error but not throw
|
|
448
|
+
expect(bundle.metadata.errors.length).toBeGreaterThan(0);
|
|
449
|
+
});
|
|
450
|
+
it('should handle empty export directory', async () => {
|
|
451
|
+
// exportDir is already empty
|
|
452
|
+
const extractor = new ClaudeExtractor({
|
|
453
|
+
exportPath: exportDir,
|
|
454
|
+
});
|
|
455
|
+
const bundle = await extractor.extract({ workDir });
|
|
456
|
+
expect(bundle.contents).toBeDefined();
|
|
457
|
+
expect(bundle.metadata.totalItems).toBe(0);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
// ─── Include/Exclude Filtering ───────────────────────────────
|
|
461
|
+
describe('include filtering', () => {
|
|
462
|
+
it('should only extract specified content types', async () => {
|
|
463
|
+
await createMockExport({
|
|
464
|
+
conversations: true,
|
|
465
|
+
projects: true,
|
|
466
|
+
files: true,
|
|
467
|
+
});
|
|
468
|
+
const extractor = new ClaudeExtractor({
|
|
469
|
+
exportPath: exportDir,
|
|
470
|
+
});
|
|
471
|
+
const bundle = await extractor.extract({
|
|
472
|
+
workDir,
|
|
473
|
+
include: ['conversations'],
|
|
474
|
+
});
|
|
475
|
+
expect(bundle.contents.conversations).toBeDefined();
|
|
476
|
+
// customBots might still be extracted as they're processed in a different path
|
|
477
|
+
// but files and other explicit includes should be filtered
|
|
478
|
+
});
|
|
479
|
+
it('should extract all types when include is not specified', async () => {
|
|
480
|
+
await createMockExport({
|
|
481
|
+
conversations: true,
|
|
482
|
+
projects: true,
|
|
483
|
+
files: true,
|
|
484
|
+
});
|
|
485
|
+
const extractor = new ClaudeExtractor({
|
|
486
|
+
exportPath: exportDir,
|
|
487
|
+
});
|
|
488
|
+
const bundle = await extractor.extract({ workDir });
|
|
489
|
+
expect(bundle.contents.conversations).toBeDefined();
|
|
490
|
+
expect(bundle.contents.customBots).toBeDefined();
|
|
491
|
+
expect(bundle.contents.files).toBeDefined();
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
// ─── Filename Sanitization ───────────────────────────────────
|
|
495
|
+
describe('filename sanitization', () => {
|
|
496
|
+
it('should sanitize filenames with special characters', async () => {
|
|
497
|
+
await createMockExport({ conversations: false, projects: false });
|
|
498
|
+
// Create file with special characters
|
|
499
|
+
const filesDir = join(exportDir, 'files');
|
|
500
|
+
await mkdir(filesDir, { recursive: true });
|
|
501
|
+
await writeFile(join(filesDir, 'normal_file.txt'), 'content');
|
|
502
|
+
const extractor = new ClaudeExtractor({
|
|
503
|
+
exportPath: exportDir,
|
|
504
|
+
});
|
|
505
|
+
const bundle = await extractor.extract({
|
|
506
|
+
workDir,
|
|
507
|
+
include: ['files'],
|
|
508
|
+
});
|
|
509
|
+
// All extracted files should have safe names
|
|
510
|
+
for (const file of bundle.contents.files?.files || []) {
|
|
511
|
+
expect(file.filename).not.toMatch(/[/\\:*?"<>|]/);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
// ─── Key Points Extraction ───────────────────────────────────
|
|
516
|
+
describe('key points extraction', () => {
|
|
517
|
+
it('should extract key points from assistant messages', async () => {
|
|
518
|
+
const convWithConclusion = [
|
|
519
|
+
{
|
|
520
|
+
uuid: 'conv_1',
|
|
521
|
+
name: 'Conclusion Conversation',
|
|
522
|
+
created_at: new Date().toISOString(),
|
|
523
|
+
updated_at: new Date().toISOString(),
|
|
524
|
+
chat_messages: [
|
|
525
|
+
{
|
|
526
|
+
uuid: 'msg_1',
|
|
527
|
+
text: 'What should I do?',
|
|
528
|
+
sender: 'human',
|
|
529
|
+
created_at: new Date().toISOString(),
|
|
530
|
+
updated_at: new Date().toISOString(),
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
uuid: 'msg_2',
|
|
534
|
+
text: 'In summary, you should focus on three main areas: testing, documentation, and code review.',
|
|
535
|
+
sender: 'assistant',
|
|
536
|
+
created_at: new Date().toISOString(),
|
|
537
|
+
updated_at: new Date().toISOString(),
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
},
|
|
541
|
+
];
|
|
542
|
+
await writeFile(join(exportDir, 'conversations.json'), JSON.stringify(convWithConclusion));
|
|
543
|
+
const extractor = new ClaudeExtractor({
|
|
544
|
+
exportPath: exportDir,
|
|
545
|
+
});
|
|
546
|
+
const bundle = await extractor.extract({
|
|
547
|
+
workDir,
|
|
548
|
+
include: ['conversations'],
|
|
549
|
+
});
|
|
550
|
+
const summary = bundle.contents.conversations?.summaries?.[0];
|
|
551
|
+
expect(summary).toBeDefined();
|
|
552
|
+
expect(summary?.keyPoints).toBeDefined();
|
|
553
|
+
expect(summary?.keyPoints?.length).toBeGreaterThan(0);
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
// ─── API-based Extraction Tests (Mocked) ─────────────────────
|
|
558
|
+
describe('ClaudeExtractor API mode', () => {
|
|
559
|
+
let testDir;
|
|
560
|
+
let workDir;
|
|
561
|
+
const mockFetch = vi.fn();
|
|
562
|
+
beforeEach(async () => {
|
|
563
|
+
const testId = `savestate-claude-api-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
564
|
+
testDir = join(tmpdir(), testId);
|
|
565
|
+
workDir = join(testDir, 'work');
|
|
566
|
+
await mkdir(workDir, { recursive: true });
|
|
567
|
+
// Mock global fetch
|
|
568
|
+
global.fetch = mockFetch;
|
|
569
|
+
});
|
|
570
|
+
afterEach(async () => {
|
|
571
|
+
if (existsSync(testDir)) {
|
|
572
|
+
await rm(testDir, { recursive: true, force: true });
|
|
573
|
+
}
|
|
574
|
+
vi.restoreAllMocks();
|
|
575
|
+
});
|
|
576
|
+
function mockApiResponses() {
|
|
577
|
+
mockFetch.mockImplementation(async (url, options) => {
|
|
578
|
+
const path = new URL(url).pathname;
|
|
579
|
+
const method = options?.method || 'GET';
|
|
580
|
+
// List projects
|
|
581
|
+
if (method === 'GET' && path === '/v1/projects') {
|
|
582
|
+
return new Response(JSON.stringify({
|
|
583
|
+
data: [
|
|
584
|
+
{
|
|
585
|
+
id: 'proj_api_1',
|
|
586
|
+
name: 'API Project 1',
|
|
587
|
+
description: 'First project',
|
|
588
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
589
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
id: 'proj_api_2',
|
|
593
|
+
name: 'API Project 2',
|
|
594
|
+
description: 'Second project',
|
|
595
|
+
created_at: '2024-01-03T00:00:00Z',
|
|
596
|
+
updated_at: '2024-01-04T00:00:00Z',
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
600
|
+
}
|
|
601
|
+
// Get project details
|
|
602
|
+
if (method === 'GET' && path.match(/^\/v1\/projects\/proj_api_\d+$/)) {
|
|
603
|
+
const projId = path.split('/').pop();
|
|
604
|
+
return new Response(JSON.stringify({
|
|
605
|
+
id: projId,
|
|
606
|
+
name: projId === 'proj_api_1' ? 'API Project 1' : 'API Project 2',
|
|
607
|
+
description: 'Test project',
|
|
608
|
+
prompt_template: `System prompt for ${projId}`,
|
|
609
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
610
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
611
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
612
|
+
}
|
|
613
|
+
// List project documents
|
|
614
|
+
if (method === 'GET' && path.match(/^\/v1\/projects\/proj_api_\d+\/docs$/)) {
|
|
615
|
+
return new Response(JSON.stringify({
|
|
616
|
+
data: [
|
|
617
|
+
{
|
|
618
|
+
id: 'doc_1',
|
|
619
|
+
name: 'knowledge.md',
|
|
620
|
+
content: '# Knowledge\n\nImportant info here.',
|
|
621
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
622
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
623
|
+
},
|
|
624
|
+
],
|
|
625
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
626
|
+
}
|
|
627
|
+
// Get document details
|
|
628
|
+
if (method === 'GET' && path.match(/^\/v1\/projects\/proj_api_\d+\/docs\/doc_\d+$/)) {
|
|
629
|
+
return new Response(JSON.stringify({
|
|
630
|
+
id: 'doc_1',
|
|
631
|
+
name: 'knowledge.md',
|
|
632
|
+
content: '# Knowledge\n\nImportant info here.',
|
|
633
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
634
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
635
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
636
|
+
}
|
|
637
|
+
// List project files
|
|
638
|
+
if (method === 'GET' && path.match(/^\/v1\/projects\/proj_api_\d+\/files$/)) {
|
|
639
|
+
return new Response(JSON.stringify({
|
|
640
|
+
data: [
|
|
641
|
+
{
|
|
642
|
+
id: 'file_1',
|
|
643
|
+
name: 'data.csv',
|
|
644
|
+
content_type: 'text/csv',
|
|
645
|
+
size: 256,
|
|
646
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
647
|
+
},
|
|
648
|
+
],
|
|
649
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
650
|
+
}
|
|
651
|
+
// Download file
|
|
652
|
+
if (method === 'GET' && path.match(/^\/v1\/projects\/proj_api_\d+\/files\/file_\d+\/content$/)) {
|
|
653
|
+
return new Response('name,value\ntest,123', {
|
|
654
|
+
status: 200,
|
|
655
|
+
headers: { 'Content-Type': 'text/csv' },
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
return new Response('Not found', { status: 404 });
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
it('should verify API access', async () => {
|
|
662
|
+
mockApiResponses();
|
|
663
|
+
const extractor = new ClaudeExtractor({
|
|
664
|
+
apiKey: 'test-api-key',
|
|
665
|
+
});
|
|
666
|
+
const canExtract = await extractor.canExtract();
|
|
667
|
+
expect(canExtract).toBe(true);
|
|
668
|
+
});
|
|
669
|
+
it('should extract projects via API', async () => {
|
|
670
|
+
mockApiResponses();
|
|
671
|
+
const extractor = new ClaudeExtractor({
|
|
672
|
+
apiKey: 'test-api-key',
|
|
673
|
+
});
|
|
674
|
+
const bundle = await extractor.extract({
|
|
675
|
+
workDir,
|
|
676
|
+
include: ['customBots'],
|
|
677
|
+
});
|
|
678
|
+
expect(bundle.contents.customBots).toBeDefined();
|
|
679
|
+
expect(bundle.contents.customBots.count).toBe(2);
|
|
680
|
+
const proj1 = bundle.contents.customBots.bots.find((b) => b.id === 'proj_api_1');
|
|
681
|
+
expect(proj1).toBeDefined();
|
|
682
|
+
expect(proj1.name).toBe('API Project 1');
|
|
683
|
+
expect(proj1.instructions).toContain('System prompt');
|
|
684
|
+
});
|
|
685
|
+
it('should extract project knowledge documents', async () => {
|
|
686
|
+
mockApiResponses();
|
|
687
|
+
const extractor = new ClaudeExtractor({
|
|
688
|
+
apiKey: 'test-api-key',
|
|
689
|
+
});
|
|
690
|
+
const bundle = await extractor.extract({
|
|
691
|
+
workDir,
|
|
692
|
+
include: ['customBots', 'memories'],
|
|
693
|
+
});
|
|
694
|
+
// Knowledge docs should be extracted as memories
|
|
695
|
+
expect(bundle.contents.memories).toBeDefined();
|
|
696
|
+
expect(bundle.contents.memories.count).toBeGreaterThan(0);
|
|
697
|
+
const knowledgeEntry = bundle.contents.memories.entries.find((e) => e.content.includes('Important info'));
|
|
698
|
+
expect(knowledgeEntry).toBeDefined();
|
|
699
|
+
expect(knowledgeEntry.source).toBe('claude-project-knowledge');
|
|
700
|
+
});
|
|
701
|
+
it('should extract and download project files', async () => {
|
|
702
|
+
mockApiResponses();
|
|
703
|
+
const extractor = new ClaudeExtractor({
|
|
704
|
+
apiKey: 'test-api-key',
|
|
705
|
+
});
|
|
706
|
+
const bundle = await extractor.extract({
|
|
707
|
+
workDir,
|
|
708
|
+
include: ['customBots', 'files'],
|
|
709
|
+
});
|
|
710
|
+
expect(bundle.contents.files).toBeDefined();
|
|
711
|
+
expect(bundle.contents.files.count).toBeGreaterThan(0);
|
|
712
|
+
const csvFile = bundle.contents.files.files.find((f) => f.filename === 'data.csv');
|
|
713
|
+
expect(csvFile).toBeDefined();
|
|
714
|
+
// Verify file was downloaded
|
|
715
|
+
const filePath = join(workDir, csvFile.path);
|
|
716
|
+
expect(existsSync(filePath)).toBe(true);
|
|
717
|
+
const content = await readFile(filePath, 'utf-8');
|
|
718
|
+
expect(content).toContain('name,value');
|
|
719
|
+
});
|
|
720
|
+
it('should filter by project IDs', async () => {
|
|
721
|
+
mockApiResponses();
|
|
722
|
+
const extractor = new ClaudeExtractor({
|
|
723
|
+
apiKey: 'test-api-key',
|
|
724
|
+
projectIds: ['proj_api_1'],
|
|
725
|
+
});
|
|
726
|
+
const bundle = await extractor.extract({
|
|
727
|
+
workDir,
|
|
728
|
+
include: ['customBots'],
|
|
729
|
+
});
|
|
730
|
+
expect(bundle.contents.customBots).toBeDefined();
|
|
731
|
+
expect(bundle.contents.customBots.count).toBe(1);
|
|
732
|
+
expect(bundle.contents.customBots.bots[0].id).toBe('proj_api_1');
|
|
733
|
+
});
|
|
734
|
+
it('should respect maxProjects limit', async () => {
|
|
735
|
+
mockApiResponses();
|
|
736
|
+
const extractor = new ClaudeExtractor({
|
|
737
|
+
apiKey: 'test-api-key',
|
|
738
|
+
maxProjects: 1,
|
|
739
|
+
});
|
|
740
|
+
const bundle = await extractor.extract({
|
|
741
|
+
workDir,
|
|
742
|
+
include: ['customBots'],
|
|
743
|
+
});
|
|
744
|
+
expect(bundle.contents.customBots).toBeDefined();
|
|
745
|
+
expect(bundle.contents.customBots.count).toBe(1);
|
|
746
|
+
expect(bundle.metadata.warnings).toContain('Limited to 1 projects (2 available)');
|
|
747
|
+
});
|
|
748
|
+
it('should handle API errors gracefully', async () => {
|
|
749
|
+
mockFetch.mockImplementation(async () => {
|
|
750
|
+
return new Response('Unauthorized', { status: 401 });
|
|
751
|
+
});
|
|
752
|
+
const extractor = new ClaudeExtractor({
|
|
753
|
+
apiKey: 'invalid-key',
|
|
754
|
+
});
|
|
755
|
+
const canExtract = await extractor.canExtract();
|
|
756
|
+
expect(canExtract).toBe(false);
|
|
757
|
+
});
|
|
758
|
+
it('should handle rate limiting with retries', async () => {
|
|
759
|
+
let attempts = 0;
|
|
760
|
+
mockFetch.mockImplementation(async (url) => {
|
|
761
|
+
const path = new URL(url).pathname;
|
|
762
|
+
if (path === '/v1/projects') {
|
|
763
|
+
attempts++;
|
|
764
|
+
if (attempts < 2) {
|
|
765
|
+
return new Response('Rate limited', {
|
|
766
|
+
status: 429,
|
|
767
|
+
headers: { 'retry-after': '1' },
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
return new Response(JSON.stringify({ data: [] }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
771
|
+
}
|
|
772
|
+
return new Response('Not found', { status: 404 });
|
|
773
|
+
});
|
|
774
|
+
const extractor = new ClaudeExtractor({
|
|
775
|
+
apiKey: 'test-api-key',
|
|
776
|
+
rateLimit: {
|
|
777
|
+
maxRetries: 3,
|
|
778
|
+
initialBackoffMs: 100,
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
const bundle = await extractor.extract({
|
|
782
|
+
workDir,
|
|
783
|
+
include: ['customBots'],
|
|
784
|
+
});
|
|
785
|
+
expect(attempts).toBeGreaterThan(1);
|
|
786
|
+
expect(bundle).toBeDefined();
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
//# sourceMappingURL=claude.test.js.map
|