claude-session-share 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +313 -0
- package/dist/__tests__/e2e.test.js +532 -0
- package/dist/__tests__/gist-client.test.js +341 -0
- package/dist/__tests__/index.test.js +16 -0
- package/dist/__tests__/mcp-integration.test.js +403 -0
- package/dist/__tests__/path-encoding.test.js +77 -0
- package/dist/__tests__/pipeline.test.js +342 -0
- package/dist/__tests__/redactor.test.js +162 -0
- package/dist/__tests__/sanitizer.test.js +345 -0
- package/dist/__tests__/session-importer.test.js +216 -0
- package/dist/__tests__/session-reader.test.js +298 -0
- package/dist/__tests__/session-uploader.test.js +216 -0
- package/dist/__tests__/session-writer.test.js +286 -0
- package/dist/__tests__/uuid-mapper.test.js +249 -0
- package/dist/gist/client.js +199 -0
- package/dist/gist/types.js +7 -0
- package/dist/index.js +214 -0
- package/dist/sanitization/pipeline.js +48 -0
- package/dist/sanitization/redactor.js +48 -0
- package/dist/sanitization/sanitizer.js +87 -0
- package/dist/services/session-importer.js +88 -0
- package/dist/services/session-uploader.js +64 -0
- package/dist/session/finder.js +65 -0
- package/dist/session/metadata.js +55 -0
- package/dist/session/reader.js +101 -0
- package/dist/session/types.js +11 -0
- package/dist/session/writer.js +74 -0
- package/dist/utils/path-encoding.js +54 -0
- package/dist/utils/uuid-mapper.js +73 -0
- package/package.json +54 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for session writer with local JSONL storage
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
5
|
+
import { writeSessionToLocal, SessionWriteError } from '../session/writer.js';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
// Mock fs/promises
|
|
9
|
+
vi.mock('fs/promises', () => ({
|
|
10
|
+
mkdir: vi.fn(),
|
|
11
|
+
writeFile: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
// Mock crypto for predictable UUID generation
|
|
14
|
+
vi.mock('crypto', () => ({
|
|
15
|
+
randomUUID: vi.fn(() => 'test-session-uuid-123'),
|
|
16
|
+
}));
|
|
17
|
+
describe('writeSessionToLocal', () => {
|
|
18
|
+
let mockMkdir;
|
|
19
|
+
let mockWriteFile;
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
// Get mocked functions
|
|
22
|
+
const fs = await import('fs/promises');
|
|
23
|
+
mockMkdir = fs.mkdir;
|
|
24
|
+
mockWriteFile = fs.writeFile;
|
|
25
|
+
// Reset mocks
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
// Default successful behavior
|
|
28
|
+
mockMkdir.mockResolvedValue(undefined);
|
|
29
|
+
mockWriteFile.mockResolvedValue(undefined);
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
33
|
+
});
|
|
34
|
+
it('should write session messages to correct path', async () => {
|
|
35
|
+
const messages = [
|
|
36
|
+
{
|
|
37
|
+
type: 'user',
|
|
38
|
+
uuid: 'uuid-1',
|
|
39
|
+
sessionId: 'session-123',
|
|
40
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
41
|
+
parentUuid: null,
|
|
42
|
+
message: { role: 'user', content: 'Test message' },
|
|
43
|
+
cwd: '/Users/name/project',
|
|
44
|
+
version: '1.0.0',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
const projectPath = '/Users/name/project';
|
|
48
|
+
const result = await writeSessionToLocal(messages, projectPath);
|
|
49
|
+
// Verify directory creation
|
|
50
|
+
const expectedDir = join(homedir(), '.claude', 'projects', 'Users-name-project');
|
|
51
|
+
expect(mockMkdir).toHaveBeenCalledWith(expectedDir, { recursive: true });
|
|
52
|
+
// Verify file writing
|
|
53
|
+
const expectedPath = join(expectedDir, 'test-session-uuid-123.jsonl');
|
|
54
|
+
expect(mockWriteFile).toHaveBeenCalledWith(expectedPath, expect.any(String), { encoding: 'utf-8' });
|
|
55
|
+
// Verify result
|
|
56
|
+
expect(result.filePath).toBe(expectedPath);
|
|
57
|
+
expect(result.sessionId).toBe('test-session-uuid-123');
|
|
58
|
+
});
|
|
59
|
+
it('should format messages as JSONL with trailing newline', async () => {
|
|
60
|
+
const messages = [
|
|
61
|
+
{
|
|
62
|
+
type: 'user',
|
|
63
|
+
uuid: 'uuid-1',
|
|
64
|
+
sessionId: 'session-123',
|
|
65
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
66
|
+
parentUuid: null,
|
|
67
|
+
message: { role: 'user', content: 'First' },
|
|
68
|
+
cwd: '/Users/name/project',
|
|
69
|
+
version: '1.0.0',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'user',
|
|
73
|
+
uuid: 'uuid-2',
|
|
74
|
+
sessionId: 'session-123',
|
|
75
|
+
timestamp: '2024-01-01T00:01:00Z',
|
|
76
|
+
parentUuid: 'uuid-1',
|
|
77
|
+
message: { role: 'user', content: 'Second' },
|
|
78
|
+
cwd: '/Users/name/project',
|
|
79
|
+
version: '1.0.0',
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
await writeSessionToLocal(messages, '/Users/name/project');
|
|
83
|
+
const writtenContent = mockWriteFile.mock.calls[0][1];
|
|
84
|
+
const lines = writtenContent.split('\n');
|
|
85
|
+
// Should have 3 lines: 2 messages + trailing newline (which creates empty string)
|
|
86
|
+
expect(lines.length).toBe(3);
|
|
87
|
+
expect(lines[2]).toBe(''); // Trailing newline creates empty last element
|
|
88
|
+
// Each line should be valid JSON
|
|
89
|
+
expect(() => JSON.parse(lines[0])).not.toThrow();
|
|
90
|
+
expect(() => JSON.parse(lines[1])).not.toThrow();
|
|
91
|
+
// Verify content
|
|
92
|
+
const msg1 = JSON.parse(lines[0]);
|
|
93
|
+
const msg2 = JSON.parse(lines[1]);
|
|
94
|
+
expect(msg1.uuid).toBe('uuid-1');
|
|
95
|
+
expect(msg2.uuid).toBe('uuid-2');
|
|
96
|
+
});
|
|
97
|
+
it('should handle multiple message types', async () => {
|
|
98
|
+
const messages = [
|
|
99
|
+
{
|
|
100
|
+
type: 'user',
|
|
101
|
+
uuid: 'uuid-1',
|
|
102
|
+
sessionId: 'session-123',
|
|
103
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
104
|
+
parentUuid: null,
|
|
105
|
+
message: { role: 'user', content: 'Question' },
|
|
106
|
+
cwd: '/Users/name/project',
|
|
107
|
+
version: '1.0.0',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: 'assistant',
|
|
111
|
+
uuid: 'uuid-2',
|
|
112
|
+
sessionId: 'session-123',
|
|
113
|
+
timestamp: '2024-01-01T00:01:00Z',
|
|
114
|
+
parentUuid: 'uuid-1',
|
|
115
|
+
messageId: 'msg-1',
|
|
116
|
+
snapshot: {
|
|
117
|
+
thinking: 'Let me help',
|
|
118
|
+
messages: [{ role: 'assistant', content: 'Answer' }],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
await writeSessionToLocal(messages, '/Users/name/project');
|
|
123
|
+
const writtenContent = mockWriteFile.mock.calls[0][1];
|
|
124
|
+
const lines = writtenContent.split('\n');
|
|
125
|
+
const msg1 = JSON.parse(lines[0]);
|
|
126
|
+
const msg2 = JSON.parse(lines[1]);
|
|
127
|
+
expect(msg1.type).toBe('user');
|
|
128
|
+
expect(msg2.type).toBe('assistant');
|
|
129
|
+
expect(msg2.snapshot.thinking).toBe('Let me help');
|
|
130
|
+
});
|
|
131
|
+
it('should encode project path correctly', async () => {
|
|
132
|
+
const messages = [
|
|
133
|
+
{
|
|
134
|
+
type: 'user',
|
|
135
|
+
uuid: 'uuid-1',
|
|
136
|
+
sessionId: 'session-123',
|
|
137
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
138
|
+
parentUuid: null,
|
|
139
|
+
message: { role: 'user', content: 'Test' },
|
|
140
|
+
cwd: '/opt/code/my-app',
|
|
141
|
+
version: '1.0.0',
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
await writeSessionToLocal(messages, '/opt/code/my-app');
|
|
145
|
+
const expectedDir = join(homedir(), '.claude', 'projects', 'opt-code-my-app');
|
|
146
|
+
expect(mockMkdir).toHaveBeenCalledWith(expectedDir, { recursive: true });
|
|
147
|
+
});
|
|
148
|
+
it('should create directory with recursive flag', async () => {
|
|
149
|
+
const messages = [
|
|
150
|
+
{
|
|
151
|
+
type: 'user',
|
|
152
|
+
uuid: 'uuid-1',
|
|
153
|
+
sessionId: 'session-123',
|
|
154
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
155
|
+
parentUuid: null,
|
|
156
|
+
message: { role: 'user', content: 'Test' },
|
|
157
|
+
cwd: '/Users/name/project',
|
|
158
|
+
version: '1.0.0',
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
await writeSessionToLocal(messages, '/Users/name/project');
|
|
162
|
+
expect(mockMkdir).toHaveBeenCalledWith(expect.any(String), { recursive: true });
|
|
163
|
+
});
|
|
164
|
+
it('should throw SessionWriteError on EACCES (permissions)', async () => {
|
|
165
|
+
const messages = [
|
|
166
|
+
{
|
|
167
|
+
type: 'user',
|
|
168
|
+
uuid: 'uuid-1',
|
|
169
|
+
sessionId: 'session-123',
|
|
170
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
171
|
+
parentUuid: null,
|
|
172
|
+
message: { role: 'user', content: 'Test' },
|
|
173
|
+
cwd: '/Users/name/project',
|
|
174
|
+
version: '1.0.0',
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
mockMkdir.mockRejectedValue({
|
|
178
|
+
code: 'EACCES',
|
|
179
|
+
message: 'Permission denied',
|
|
180
|
+
});
|
|
181
|
+
await expect(writeSessionToLocal(messages, '/Users/name/project')).rejects.toThrow(SessionWriteError);
|
|
182
|
+
await expect(writeSessionToLocal(messages, '/Users/name/project')).rejects.toThrow(/Permission denied/);
|
|
183
|
+
});
|
|
184
|
+
it('should throw SessionWriteError on ENOSPC (disk full)', async () => {
|
|
185
|
+
const messages = [
|
|
186
|
+
{
|
|
187
|
+
type: 'user',
|
|
188
|
+
uuid: 'uuid-1',
|
|
189
|
+
sessionId: 'session-123',
|
|
190
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
191
|
+
parentUuid: null,
|
|
192
|
+
message: { role: 'user', content: 'Test' },
|
|
193
|
+
cwd: '/Users/name/project',
|
|
194
|
+
version: '1.0.0',
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
mockWriteFile.mockRejectedValue({
|
|
198
|
+
code: 'ENOSPC',
|
|
199
|
+
message: 'No space left on device',
|
|
200
|
+
});
|
|
201
|
+
await expect(writeSessionToLocal(messages, '/Users/name/project')).rejects.toThrow(SessionWriteError);
|
|
202
|
+
await expect(writeSessionToLocal(messages, '/Users/name/project')).rejects.toThrow(/Disk full/);
|
|
203
|
+
});
|
|
204
|
+
it('should throw SessionWriteError on other filesystem errors', async () => {
|
|
205
|
+
const messages = [
|
|
206
|
+
{
|
|
207
|
+
type: 'user',
|
|
208
|
+
uuid: 'uuid-1',
|
|
209
|
+
sessionId: 'session-123',
|
|
210
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
211
|
+
parentUuid: null,
|
|
212
|
+
message: { role: 'user', content: 'Test' },
|
|
213
|
+
cwd: '/Users/name/project',
|
|
214
|
+
version: '1.0.0',
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
mockWriteFile.mockRejectedValue({
|
|
218
|
+
code: 'UNKNOWN',
|
|
219
|
+
message: 'Something went wrong',
|
|
220
|
+
});
|
|
221
|
+
await expect(writeSessionToLocal(messages, '/Users/name/project')).rejects.toThrow(SessionWriteError);
|
|
222
|
+
await expect(writeSessionToLocal(messages, '/Users/name/project')).rejects.toThrow(/Failed to write session/);
|
|
223
|
+
});
|
|
224
|
+
it('should include error code in SessionWriteError', async () => {
|
|
225
|
+
const messages = [
|
|
226
|
+
{
|
|
227
|
+
type: 'user',
|
|
228
|
+
uuid: 'uuid-1',
|
|
229
|
+
sessionId: 'session-123',
|
|
230
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
231
|
+
parentUuid: null,
|
|
232
|
+
message: { role: 'user', content: 'Test' },
|
|
233
|
+
cwd: '/Users/name/project',
|
|
234
|
+
version: '1.0.0',
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
mockMkdir.mockRejectedValue({
|
|
238
|
+
code: 'EACCES',
|
|
239
|
+
message: 'Permission denied',
|
|
240
|
+
});
|
|
241
|
+
try {
|
|
242
|
+
await writeSessionToLocal(messages, '/Users/name/project');
|
|
243
|
+
expect.fail('Should have thrown');
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
expect(error).toBeInstanceOf(SessionWriteError);
|
|
247
|
+
expect(error.code).toBe('EACCES');
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
it('should write empty messages array', async () => {
|
|
251
|
+
const messages = [];
|
|
252
|
+
await writeSessionToLocal(messages, '/Users/name/project');
|
|
253
|
+
const writtenContent = mockWriteFile.mock.calls[0][1];
|
|
254
|
+
expect(writtenContent).toBe('\n'); // Just trailing newline
|
|
255
|
+
});
|
|
256
|
+
it('should preserve all message fields in written JSON', async () => {
|
|
257
|
+
const messages = [
|
|
258
|
+
{
|
|
259
|
+
type: 'user',
|
|
260
|
+
uuid: 'uuid-1',
|
|
261
|
+
sessionId: 'session-123',
|
|
262
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
263
|
+
parentUuid: null,
|
|
264
|
+
message: { role: 'user', content: 'Test message' },
|
|
265
|
+
cwd: '/Users/name/project',
|
|
266
|
+
version: '1.0.0',
|
|
267
|
+
gitBranch: 'main',
|
|
268
|
+
isSidechain: true,
|
|
269
|
+
isMeta: false,
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
await writeSessionToLocal(messages, '/Users/name/project');
|
|
273
|
+
const writtenContent = mockWriteFile.mock.calls[0][1];
|
|
274
|
+
const parsed = JSON.parse(writtenContent.split('\n')[0]);
|
|
275
|
+
expect(parsed.type).toBe('user');
|
|
276
|
+
expect(parsed.uuid).toBe('uuid-1');
|
|
277
|
+
expect(parsed.sessionId).toBe('session-123');
|
|
278
|
+
expect(parsed.timestamp).toBe('2024-01-01T00:00:00Z');
|
|
279
|
+
expect(parsed.parentUuid).toBeNull();
|
|
280
|
+
expect(parsed.cwd).toBe('/Users/name/project');
|
|
281
|
+
expect(parsed.version).toBe('1.0.0');
|
|
282
|
+
expect(parsed.gitBranch).toBe('main');
|
|
283
|
+
expect(parsed.isSidechain).toBe(true);
|
|
284
|
+
expect(parsed.isMeta).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for UUID remapper with collision avoidance
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
5
|
+
import { UUIDMapper } from '../utils/uuid-mapper.js';
|
|
6
|
+
describe('UUIDMapper', () => {
|
|
7
|
+
let mapper;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mapper = new UUIDMapper();
|
|
10
|
+
});
|
|
11
|
+
describe('remap', () => {
|
|
12
|
+
it('should generate a new UUID for a given input', () => {
|
|
13
|
+
const original = 'abc-123-def';
|
|
14
|
+
const remapped = mapper.remap(original);
|
|
15
|
+
expect(remapped).toBeTruthy();
|
|
16
|
+
expect(remapped).not.toBe(original);
|
|
17
|
+
expect(typeof remapped).toBe('string');
|
|
18
|
+
});
|
|
19
|
+
it('should return the same UUID for the same input (consistency)', () => {
|
|
20
|
+
const original = 'abc-123-def';
|
|
21
|
+
const remapped1 = mapper.remap(original);
|
|
22
|
+
const remapped2 = mapper.remap(original);
|
|
23
|
+
const remapped3 = mapper.remap(original);
|
|
24
|
+
expect(remapped1).toBe(remapped2);
|
|
25
|
+
expect(remapped2).toBe(remapped3);
|
|
26
|
+
});
|
|
27
|
+
it('should generate different UUIDs for different inputs', () => {
|
|
28
|
+
const uuid1 = mapper.remap('original-1');
|
|
29
|
+
const uuid2 = mapper.remap('original-2');
|
|
30
|
+
const uuid3 = mapper.remap('original-3');
|
|
31
|
+
expect(uuid1).not.toBe(uuid2);
|
|
32
|
+
expect(uuid2).not.toBe(uuid3);
|
|
33
|
+
expect(uuid1).not.toBe(uuid3);
|
|
34
|
+
});
|
|
35
|
+
it('should handle null input by returning null', () => {
|
|
36
|
+
const result = mapper.remap(null);
|
|
37
|
+
expect(result).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
it('should always return null for null input (multiple calls)', () => {
|
|
40
|
+
const result1 = mapper.remap(null);
|
|
41
|
+
const result2 = mapper.remap(null);
|
|
42
|
+
const result3 = mapper.remap(null);
|
|
43
|
+
expect(result1).toBeNull();
|
|
44
|
+
expect(result2).toBeNull();
|
|
45
|
+
expect(result3).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
it('should generate valid UUID v4 format', () => {
|
|
48
|
+
const remapped = mapper.remap('test-uuid');
|
|
49
|
+
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
50
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
51
|
+
expect(remapped).toMatch(uuidRegex);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('remapMessage', () => {
|
|
55
|
+
it('should remap UUIDs in a user message', () => {
|
|
56
|
+
const original = {
|
|
57
|
+
type: 'user',
|
|
58
|
+
uuid: 'user-uuid-1',
|
|
59
|
+
sessionId: 'session-123',
|
|
60
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
61
|
+
parentUuid: 'parent-uuid-1',
|
|
62
|
+
message: {
|
|
63
|
+
role: 'user',
|
|
64
|
+
content: 'Hello, world!',
|
|
65
|
+
},
|
|
66
|
+
cwd: '/home/user/project',
|
|
67
|
+
version: '1.0.0',
|
|
68
|
+
gitBranch: 'main',
|
|
69
|
+
isSidechain: false,
|
|
70
|
+
};
|
|
71
|
+
const remapped = mapper.remapMessage(original);
|
|
72
|
+
// UUIDs should be different
|
|
73
|
+
expect(remapped.uuid).not.toBe(original.uuid);
|
|
74
|
+
expect(remapped.sessionId).not.toBe(original.sessionId);
|
|
75
|
+
expect(remapped.parentUuid).not.toBe(original.parentUuid);
|
|
76
|
+
// Other fields should be preserved
|
|
77
|
+
expect(remapped.type).toBe(original.type);
|
|
78
|
+
expect(remapped.timestamp).toBe(original.timestamp);
|
|
79
|
+
expect(remapped.message).toEqual(original.message);
|
|
80
|
+
expect(remapped.cwd).toBe(original.cwd);
|
|
81
|
+
expect(remapped.version).toBe(original.version);
|
|
82
|
+
expect(remapped.gitBranch).toBe(original.gitBranch);
|
|
83
|
+
expect(remapped.isSidechain).toBe(original.isSidechain);
|
|
84
|
+
});
|
|
85
|
+
it('should remap UUIDs in an assistant message', () => {
|
|
86
|
+
const original = {
|
|
87
|
+
type: 'assistant',
|
|
88
|
+
uuid: 'assistant-uuid-1',
|
|
89
|
+
sessionId: 'session-123',
|
|
90
|
+
timestamp: '2024-01-01T00:01:00Z',
|
|
91
|
+
parentUuid: 'parent-uuid-2',
|
|
92
|
+
messageId: 'msg-123',
|
|
93
|
+
snapshot: {
|
|
94
|
+
thinking: 'Let me help you',
|
|
95
|
+
messages: [
|
|
96
|
+
{ role: 'assistant', content: 'Here is the answer' },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const remapped = mapper.remapMessage(original);
|
|
101
|
+
// UUIDs should be different
|
|
102
|
+
expect(remapped.uuid).not.toBe(original.uuid);
|
|
103
|
+
expect(remapped.sessionId).not.toBe(original.sessionId);
|
|
104
|
+
expect(remapped.parentUuid).not.toBe(original.parentUuid);
|
|
105
|
+
// Other fields should be preserved
|
|
106
|
+
expect(remapped.type).toBe(original.type);
|
|
107
|
+
expect(remapped.timestamp).toBe(original.timestamp);
|
|
108
|
+
expect(remapped.messageId).toBe(original.messageId);
|
|
109
|
+
expect(remapped.snapshot).toEqual(original.snapshot);
|
|
110
|
+
});
|
|
111
|
+
it('should remap UUIDs in a file-history-snapshot message', () => {
|
|
112
|
+
const original = {
|
|
113
|
+
type: 'file-history-snapshot',
|
|
114
|
+
uuid: 'snapshot-uuid-1',
|
|
115
|
+
sessionId: 'session-123',
|
|
116
|
+
timestamp: '2024-01-01T00:02:00Z',
|
|
117
|
+
parentUuid: null,
|
|
118
|
+
isSnapshotUpdate: true,
|
|
119
|
+
snapshot: {
|
|
120
|
+
files: [
|
|
121
|
+
{ path: '/home/user/project/file.ts' },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
const remapped = mapper.remapMessage(original);
|
|
126
|
+
// UUIDs should be different (except parentUuid which is null)
|
|
127
|
+
expect(remapped.uuid).not.toBe(original.uuid);
|
|
128
|
+
expect(remapped.sessionId).not.toBe(original.sessionId);
|
|
129
|
+
expect(remapped.parentUuid).toBeNull();
|
|
130
|
+
// Other fields should be preserved
|
|
131
|
+
expect(remapped.type).toBe(original.type);
|
|
132
|
+
expect(remapped.timestamp).toBe(original.timestamp);
|
|
133
|
+
expect(remapped.isSnapshotUpdate).toBe(original.isSnapshotUpdate);
|
|
134
|
+
expect(remapped.snapshot).toEqual(original.snapshot);
|
|
135
|
+
});
|
|
136
|
+
it('should not mutate the original message (immutability)', () => {
|
|
137
|
+
const original = {
|
|
138
|
+
type: 'user',
|
|
139
|
+
uuid: 'user-uuid-1',
|
|
140
|
+
sessionId: 'session-123',
|
|
141
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
142
|
+
parentUuid: 'parent-uuid-1',
|
|
143
|
+
message: {
|
|
144
|
+
role: 'user',
|
|
145
|
+
content: 'Test message',
|
|
146
|
+
},
|
|
147
|
+
cwd: '/home/user/project',
|
|
148
|
+
version: '1.0.0',
|
|
149
|
+
};
|
|
150
|
+
const originalCopy = { ...original };
|
|
151
|
+
const remapped = mapper.remapMessage(original);
|
|
152
|
+
// Original should be unchanged
|
|
153
|
+
expect(original).toEqual(originalCopy);
|
|
154
|
+
expect(original.uuid).toBe('user-uuid-1');
|
|
155
|
+
expect(original.sessionId).toBe('session-123');
|
|
156
|
+
expect(original.parentUuid).toBe('parent-uuid-1');
|
|
157
|
+
// Remapped should be different
|
|
158
|
+
expect(remapped.uuid).not.toBe(original.uuid);
|
|
159
|
+
});
|
|
160
|
+
it('should preserve parent-child relationships through consistent remapping', () => {
|
|
161
|
+
const parentMessage = {
|
|
162
|
+
type: 'user',
|
|
163
|
+
uuid: 'parent-uuid',
|
|
164
|
+
sessionId: 'session-123',
|
|
165
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
166
|
+
parentUuid: null,
|
|
167
|
+
message: { role: 'user', content: 'Parent' },
|
|
168
|
+
cwd: '/home/user/project',
|
|
169
|
+
version: '1.0.0',
|
|
170
|
+
};
|
|
171
|
+
const childMessage = {
|
|
172
|
+
type: 'assistant',
|
|
173
|
+
uuid: 'child-uuid',
|
|
174
|
+
sessionId: 'session-123',
|
|
175
|
+
timestamp: '2024-01-01T00:01:00Z',
|
|
176
|
+
parentUuid: 'parent-uuid', // References parent
|
|
177
|
+
messageId: 'msg-1',
|
|
178
|
+
snapshot: {
|
|
179
|
+
thinking: null,
|
|
180
|
+
messages: [],
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
const remappedParent = mapper.remapMessage(parentMessage);
|
|
184
|
+
const remappedChild = mapper.remapMessage(childMessage);
|
|
185
|
+
// Child's parentUuid should equal parent's uuid after remapping
|
|
186
|
+
expect(remappedChild.parentUuid).toBe(remappedParent.uuid);
|
|
187
|
+
// Both should have same sessionId after remapping
|
|
188
|
+
expect(remappedChild.sessionId).toBe(remappedParent.sessionId);
|
|
189
|
+
});
|
|
190
|
+
it('should handle multiple messages with consistent session ID remapping', () => {
|
|
191
|
+
const messages = [
|
|
192
|
+
{
|
|
193
|
+
type: 'user',
|
|
194
|
+
uuid: 'uuid-1',
|
|
195
|
+
sessionId: 'session-abc',
|
|
196
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
197
|
+
parentUuid: null,
|
|
198
|
+
message: { role: 'user', content: 'Message 1' },
|
|
199
|
+
cwd: '/home/user/project',
|
|
200
|
+
version: '1.0.0',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
type: 'user',
|
|
204
|
+
uuid: 'uuid-2',
|
|
205
|
+
sessionId: 'session-abc',
|
|
206
|
+
timestamp: '2024-01-01T00:01:00Z',
|
|
207
|
+
parentUuid: 'uuid-1',
|
|
208
|
+
message: { role: 'user', content: 'Message 2' },
|
|
209
|
+
cwd: '/home/user/project',
|
|
210
|
+
version: '1.0.0',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
type: 'user',
|
|
214
|
+
uuid: 'uuid-3',
|
|
215
|
+
sessionId: 'session-abc',
|
|
216
|
+
timestamp: '2024-01-01T00:02:00Z',
|
|
217
|
+
parentUuid: 'uuid-2',
|
|
218
|
+
message: { role: 'user', content: 'Message 3' },
|
|
219
|
+
cwd: '/home/user/project',
|
|
220
|
+
version: '1.0.0',
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
const remapped = messages.map((msg) => mapper.remapMessage(msg));
|
|
224
|
+
// All should have the same remapped sessionId
|
|
225
|
+
expect(remapped[0].sessionId).toBe(remapped[1].sessionId);
|
|
226
|
+
expect(remapped[1].sessionId).toBe(remapped[2].sessionId);
|
|
227
|
+
// Parent-child chain should be preserved
|
|
228
|
+
expect(remapped[1].parentUuid).toBe(remapped[0].uuid);
|
|
229
|
+
expect(remapped[2].parentUuid).toBe(remapped[1].uuid);
|
|
230
|
+
});
|
|
231
|
+
it('should handle null parentUuid correctly', () => {
|
|
232
|
+
const message = {
|
|
233
|
+
type: 'user',
|
|
234
|
+
uuid: 'root-uuid',
|
|
235
|
+
sessionId: 'session-123',
|
|
236
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
237
|
+
parentUuid: null, // Root message
|
|
238
|
+
message: { role: 'user', content: 'Root message' },
|
|
239
|
+
cwd: '/home/user/project',
|
|
240
|
+
version: '1.0.0',
|
|
241
|
+
};
|
|
242
|
+
const remapped = mapper.remapMessage(message);
|
|
243
|
+
// parentUuid should remain null
|
|
244
|
+
expect(remapped.parentUuid).toBeNull();
|
|
245
|
+
expect(remapped.uuid).not.toBe(message.uuid);
|
|
246
|
+
expect(remapped.sessionId).not.toBe(message.sessionId);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|