@aaronsb/google-workspace-mcp 2.0.0 → 2.2.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/build/__tests__/executor/workspace.test.js +39 -13
- package/build/__tests__/executor/workspace.test.js.map +1 -1
- package/build/__tests__/server/queue.test.js +5 -0
- package/build/__tests__/server/queue.test.js.map +1 -1
- package/build/executor/file-output.d.ts +12 -1
- package/build/executor/file-output.js +52 -1
- package/build/executor/file-output.js.map +1 -1
- package/build/executor/workspace.d.ts +8 -2
- package/build/executor/workspace.js +22 -3
- package/build/executor/workspace.js.map +1 -1
- package/build/factory/manifest.yaml +26 -0
- package/build/server/formatting/markdown.d.ts +15 -0
- package/build/server/formatting/markdown.js +1 -1
- package/build/server/formatting/markdown.js.map +1 -1
- package/build/server/formatting/next-steps.js +19 -0
- package/build/server/formatting/next-steps.js.map +1 -1
- package/build/server/handler.d.ts +4 -0
- package/build/server/handler.js +17 -1
- package/build/server/handler.js.map +1 -1
- package/build/server/handlers/workspace.d.ts +1 -1
- package/build/server/handlers/workspace.js +160 -23
- package/build/server/handlers/workspace.js.map +1 -1
- package/build/server/queue.js +2 -0
- package/build/server/queue.js.map +1 -1
- package/build/server/scratchpad/__tests__/json-path.test.d.ts +4 -0
- package/build/server/scratchpad/__tests__/json-path.test.js +121 -0
- package/build/server/scratchpad/__tests__/json-path.test.js.map +1 -0
- package/build/server/scratchpad/__tests__/manager.test.d.ts +5 -0
- package/build/server/scratchpad/__tests__/manager.test.js +886 -0
- package/build/server/scratchpad/__tests__/manager.test.js.map +1 -0
- package/build/server/scratchpad/__tests__/validate.test.d.ts +4 -0
- package/build/server/scratchpad/__tests__/validate.test.js +112 -0
- package/build/server/scratchpad/__tests__/validate.test.js.map +1 -0
- package/build/server/scratchpad/adapters/import-doc.d.ts +16 -0
- package/build/server/scratchpad/adapters/import-doc.js +123 -0
- package/build/server/scratchpad/adapters/import-doc.js.map +1 -0
- package/build/server/scratchpad/adapters/import-drive.d.ts +12 -0
- package/build/server/scratchpad/adapters/import-drive.js +52 -0
- package/build/server/scratchpad/adapters/import-drive.js.map +1 -0
- package/build/server/scratchpad/adapters/import-email.d.ts +13 -0
- package/build/server/scratchpad/adapters/import-email.js +87 -0
- package/build/server/scratchpad/adapters/import-email.js.map +1 -0
- package/build/server/scratchpad/adapters/import-meet.d.ts +12 -0
- package/build/server/scratchpad/adapters/import-meet.js +109 -0
- package/build/server/scratchpad/adapters/import-meet.js.map +1 -0
- package/build/server/scratchpad/adapters/import-sheet.d.ts +12 -0
- package/build/server/scratchpad/adapters/import-sheet.js +55 -0
- package/build/server/scratchpad/adapters/import-sheet.js.map +1 -0
- package/build/server/scratchpad/adapters/index.d.ts +15 -0
- package/build/server/scratchpad/adapters/index.js +18 -0
- package/build/server/scratchpad/adapters/index.js.map +1 -0
- package/build/server/scratchpad/adapters/send-calendar.d.ts +15 -0
- package/build/server/scratchpad/adapters/send-calendar.js +50 -0
- package/build/server/scratchpad/adapters/send-calendar.js.map +1 -0
- package/build/server/scratchpad/adapters/send-doc.d.ts +18 -0
- package/build/server/scratchpad/adapters/send-doc.js +77 -0
- package/build/server/scratchpad/adapters/send-doc.js.map +1 -0
- package/build/server/scratchpad/adapters/send-email-draft.d.ts +12 -0
- package/build/server/scratchpad/adapters/send-email-draft.js +52 -0
- package/build/server/scratchpad/adapters/send-email-draft.js.map +1 -0
- package/build/server/scratchpad/adapters/send-email.d.ts +15 -0
- package/build/server/scratchpad/adapters/send-email.js +73 -0
- package/build/server/scratchpad/adapters/send-email.js.map +1 -0
- package/build/server/scratchpad/adapters/send-sheet.d.ts +13 -0
- package/build/server/scratchpad/adapters/send-sheet.js +71 -0
- package/build/server/scratchpad/adapters/send-sheet.js.map +1 -0
- package/build/server/scratchpad/adapters/send-task.d.ts +12 -0
- package/build/server/scratchpad/adapters/send-task.js +53 -0
- package/build/server/scratchpad/adapters/send-task.js.map +1 -0
- package/build/server/scratchpad/adapters/send-workspace.d.ts +11 -0
- package/build/server/scratchpad/adapters/send-workspace.js +69 -0
- package/build/server/scratchpad/adapters/send-workspace.js.map +1 -0
- package/build/server/scratchpad/handler.d.ts +9 -0
- package/build/server/scratchpad/handler.js +476 -0
- package/build/server/scratchpad/handler.js.map +1 -0
- package/build/server/scratchpad/json-path.d.ts +12 -0
- package/build/server/scratchpad/json-path.js +75 -0
- package/build/server/scratchpad/json-path.js.map +1 -0
- package/build/server/scratchpad/manager.d.ts +140 -0
- package/build/server/scratchpad/manager.js +561 -0
- package/build/server/scratchpad/manager.js.map +1 -0
- package/build/server/scratchpad/validate.d.ts +7 -0
- package/build/server/scratchpad/validate.js +96 -0
- package/build/server/scratchpad/validate.js.map +1 -0
- package/build/server/server.js +7 -3
- package/build/server/server.js.map +1 -1
- package/build/server/tools.js +55 -5
- package/build/server/tools.js.map +1 -1
- package/build/services/drive/patch.js +47 -1
- package/build/services/drive/patch.js.map +1 -1
- package/build/services/gmail/attachments.d.ts +9 -10
- package/build/services/gmail/attachments.js +39 -17
- package/build/services/gmail/attachments.js.map +1 -1
- package/build/services/gmail/patch.js +2 -1
- package/build/services/gmail/patch.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ScratchpadManager — line-addressed content authoring buffer.
|
|
3
|
+
* See ADR-301.
|
|
4
|
+
*/
|
|
5
|
+
import { ScratchpadManager } from '../manager.js';
|
|
6
|
+
// ── Mock getEpoch ──────────────────────────────────────────
|
|
7
|
+
// The manager imports getEpoch from '../handler.js' which resolves
|
|
8
|
+
// to '../../handler.js' from the manager's location.
|
|
9
|
+
let mockEpoch = 0;
|
|
10
|
+
jest.mock('../../handler.js', () => ({
|
|
11
|
+
getEpoch: () => mockEpoch,
|
|
12
|
+
}));
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockEpoch = 0;
|
|
15
|
+
});
|
|
16
|
+
// ── Helpers ────────────────────────────────────────────────
|
|
17
|
+
function createManager() {
|
|
18
|
+
return new ScratchpadManager();
|
|
19
|
+
}
|
|
20
|
+
// ── 1. create() ────────────────────────────────────────────
|
|
21
|
+
describe('ScratchpadManager', () => {
|
|
22
|
+
describe('create()', () => {
|
|
23
|
+
it('creates an empty scratchpad with defaults', () => {
|
|
24
|
+
const mgr = createManager();
|
|
25
|
+
const id = mgr.create();
|
|
26
|
+
expect(id).toMatch(/^sp-/);
|
|
27
|
+
const sp = mgr.get(id);
|
|
28
|
+
expect(sp).not.toBeNull();
|
|
29
|
+
expect(sp.lines).toEqual([]);
|
|
30
|
+
expect(sp.format).toBe('text');
|
|
31
|
+
expect(sp.label).toBeUndefined();
|
|
32
|
+
expect(sp.attachments.size).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
it('creates a scratchpad with content', () => {
|
|
35
|
+
const mgr = createManager();
|
|
36
|
+
const id = mgr.create({ content: 'hello\nworld' });
|
|
37
|
+
const sp = mgr.get(id);
|
|
38
|
+
expect(sp.lines).toEqual(['hello', 'world']);
|
|
39
|
+
});
|
|
40
|
+
it('creates a scratchpad with a specific format', () => {
|
|
41
|
+
const mgr = createManager();
|
|
42
|
+
const id = mgr.create({ format: 'json' });
|
|
43
|
+
expect(mgr.get(id).format).toBe('json');
|
|
44
|
+
});
|
|
45
|
+
it('creates a scratchpad with a label', () => {
|
|
46
|
+
const mgr = createManager();
|
|
47
|
+
const id = mgr.create({ label: 'Draft email' });
|
|
48
|
+
expect(mgr.get(id).label).toBe('Draft email');
|
|
49
|
+
});
|
|
50
|
+
it('creates a scratchpad with all options combined', () => {
|
|
51
|
+
const mgr = createManager();
|
|
52
|
+
const id = mgr.create({
|
|
53
|
+
content: '{"key": "value"}',
|
|
54
|
+
format: 'json',
|
|
55
|
+
label: 'Config',
|
|
56
|
+
});
|
|
57
|
+
const sp = mgr.get(id);
|
|
58
|
+
expect(sp.lines).toEqual(['{"key": "value"}']);
|
|
59
|
+
expect(sp.format).toBe('json');
|
|
60
|
+
expect(sp.label).toBe('Config');
|
|
61
|
+
});
|
|
62
|
+
it('generates unique IDs for each scratchpad', () => {
|
|
63
|
+
const mgr = createManager();
|
|
64
|
+
const ids = new Set();
|
|
65
|
+
for (let i = 0; i < 50; i++) {
|
|
66
|
+
ids.add(mgr.create());
|
|
67
|
+
}
|
|
68
|
+
expect(ids.size).toBe(50);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
// ── 2. view() ──────────────────────────────────────────────
|
|
72
|
+
describe('view()', () => {
|
|
73
|
+
it('returns full view with header and line numbers', () => {
|
|
74
|
+
const mgr = createManager();
|
|
75
|
+
const id = mgr.create({ content: 'line one\nline two\nline three' });
|
|
76
|
+
const view = mgr.view(id);
|
|
77
|
+
expect(view).toContain(id);
|
|
78
|
+
expect(view).toContain('text');
|
|
79
|
+
expect(view).toContain('3 lines');
|
|
80
|
+
expect(view).toContain('1 | line one');
|
|
81
|
+
expect(view).toContain('2 | line two');
|
|
82
|
+
expect(view).toContain('3 | line three');
|
|
83
|
+
});
|
|
84
|
+
it('returns windowed view for a line range', () => {
|
|
85
|
+
const mgr = createManager();
|
|
86
|
+
const id = mgr.create({ content: 'a\nb\nc\nd\ne' });
|
|
87
|
+
const view = mgr.view(id, 2, 4);
|
|
88
|
+
expect(view).toContain('2 | b');
|
|
89
|
+
expect(view).toContain('3 | c');
|
|
90
|
+
expect(view).toContain('4 | d');
|
|
91
|
+
expect(view).not.toContain('1 | a');
|
|
92
|
+
expect(view).not.toContain('5 | e');
|
|
93
|
+
});
|
|
94
|
+
it('displays empty buffer message', () => {
|
|
95
|
+
const mgr = createManager();
|
|
96
|
+
const id = mgr.create();
|
|
97
|
+
const view = mgr.view(id);
|
|
98
|
+
expect(view).toContain('(empty buffer)');
|
|
99
|
+
});
|
|
100
|
+
it('returns null for unknown id', () => {
|
|
101
|
+
const mgr = createManager();
|
|
102
|
+
expect(mgr.view('sp-nonexistent')).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
it('clamps window boundaries', () => {
|
|
105
|
+
const mgr = createManager();
|
|
106
|
+
const id = mgr.create({ content: 'a\nb\nc' });
|
|
107
|
+
// startLine below 1 gets clamped to 1, endLine above length clamped
|
|
108
|
+
const view = mgr.view(id, -5, 100);
|
|
109
|
+
expect(view).toContain('1 | a');
|
|
110
|
+
expect(view).toContain('3 | c');
|
|
111
|
+
});
|
|
112
|
+
it('includes label in header when present', () => {
|
|
113
|
+
const mgr = createManager();
|
|
114
|
+
const id = mgr.create({ content: 'x', label: 'My Label' });
|
|
115
|
+
const view = mgr.view(id);
|
|
116
|
+
expect(view).toContain('"My Label"');
|
|
117
|
+
});
|
|
118
|
+
it('includes attachment count in header', () => {
|
|
119
|
+
const mgr = createManager();
|
|
120
|
+
const id = mgr.create({ content: 'x' });
|
|
121
|
+
mgr.attach(id, {
|
|
122
|
+
source: 'drive',
|
|
123
|
+
filename: 'file.pdf',
|
|
124
|
+
mimeType: 'application/pdf',
|
|
125
|
+
size: 1024,
|
|
126
|
+
location: 'drive://abc',
|
|
127
|
+
});
|
|
128
|
+
const view = mgr.view(id);
|
|
129
|
+
expect(view).toContain('1 attachment(s)');
|
|
130
|
+
});
|
|
131
|
+
it('includes binding info in header', () => {
|
|
132
|
+
const mgr = createManager();
|
|
133
|
+
const id = mgr.create({ content: 'x' });
|
|
134
|
+
mgr.setBinding(id, { service: 'docs', resourceId: 'doc123', account: 'a@b.com' });
|
|
135
|
+
const view = mgr.view(id);
|
|
136
|
+
expect(view).toContain('bound: docs/doc123');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
// ── 3. Line operations ─────────────────────────────────────
|
|
140
|
+
describe('insertLines()', () => {
|
|
141
|
+
it('inserts after a given line', () => {
|
|
142
|
+
const mgr = createManager();
|
|
143
|
+
const id = mgr.create({ content: 'a\nc' });
|
|
144
|
+
const result = mgr.insertLines(id, 1, 'b');
|
|
145
|
+
expect(result.message).toContain('Inserted 1 line(s)');
|
|
146
|
+
expect(mgr.getContent(id)).toBe('a\nb\nc');
|
|
147
|
+
});
|
|
148
|
+
it('prepends when afterLine=0', () => {
|
|
149
|
+
const mgr = createManager();
|
|
150
|
+
const id = mgr.create({ content: 'b\nc' });
|
|
151
|
+
mgr.insertLines(id, 0, 'a');
|
|
152
|
+
expect(mgr.getContent(id)).toBe('a\nb\nc');
|
|
153
|
+
});
|
|
154
|
+
it('inserts multi-line content', () => {
|
|
155
|
+
const mgr = createManager();
|
|
156
|
+
const id = mgr.create({ content: 'a\nd' });
|
|
157
|
+
mgr.insertLines(id, 1, 'b\nc');
|
|
158
|
+
expect(mgr.getContent(id)).toBe('a\nb\nc\nd');
|
|
159
|
+
});
|
|
160
|
+
it('returns error for afterLine out of range (negative)', () => {
|
|
161
|
+
const mgr = createManager();
|
|
162
|
+
const id = mgr.create({ content: 'a' });
|
|
163
|
+
const result = mgr.insertLines(id, -1, 'x');
|
|
164
|
+
expect(result.message).toContain('Error');
|
|
165
|
+
expect(result.message).toContain('out of range');
|
|
166
|
+
});
|
|
167
|
+
it('returns error for afterLine out of range (too high)', () => {
|
|
168
|
+
const mgr = createManager();
|
|
169
|
+
const id = mgr.create({ content: 'a' });
|
|
170
|
+
const result = mgr.insertLines(id, 5, 'x');
|
|
171
|
+
expect(result.message).toContain('Error');
|
|
172
|
+
});
|
|
173
|
+
it('returns null for unknown scratchpad', () => {
|
|
174
|
+
const mgr = createManager();
|
|
175
|
+
expect(mgr.insertLines('sp-nope', 0, 'x')).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('appendLines()', () => {
|
|
179
|
+
it('appends to the end of the buffer', () => {
|
|
180
|
+
const mgr = createManager();
|
|
181
|
+
const id = mgr.create({ content: 'a' });
|
|
182
|
+
const result = mgr.appendLines(id, 'b\nc');
|
|
183
|
+
expect(result.message).toContain('Appended 2 line(s)');
|
|
184
|
+
expect(mgr.getContent(id)).toBe('a\nb\nc');
|
|
185
|
+
});
|
|
186
|
+
it('appends to an empty buffer', () => {
|
|
187
|
+
const mgr = createManager();
|
|
188
|
+
const id = mgr.create();
|
|
189
|
+
mgr.appendLines(id, 'first');
|
|
190
|
+
expect(mgr.getContent(id)).toBe('first');
|
|
191
|
+
});
|
|
192
|
+
it('returns null for unknown scratchpad', () => {
|
|
193
|
+
const mgr = createManager();
|
|
194
|
+
expect(mgr.appendLines('sp-nope', 'x')).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
describe('replaceLines()', () => {
|
|
198
|
+
it('replaces a single line', () => {
|
|
199
|
+
const mgr = createManager();
|
|
200
|
+
const id = mgr.create({ content: 'a\nb\nc' });
|
|
201
|
+
const result = mgr.replaceLines(id, 2, 2, 'B');
|
|
202
|
+
expect(result.message).toContain('Replaced lines 2-2');
|
|
203
|
+
expect(mgr.getContent(id)).toBe('a\nB\nc');
|
|
204
|
+
});
|
|
205
|
+
it('replaces a range with more lines', () => {
|
|
206
|
+
const mgr = createManager();
|
|
207
|
+
const id = mgr.create({ content: 'a\nb\nc' });
|
|
208
|
+
mgr.replaceLines(id, 2, 2, 'x\ny\nz');
|
|
209
|
+
expect(mgr.getContent(id)).toBe('a\nx\ny\nz\nc');
|
|
210
|
+
});
|
|
211
|
+
it('replaces a range with fewer lines', () => {
|
|
212
|
+
const mgr = createManager();
|
|
213
|
+
const id = mgr.create({ content: 'a\nb\nc\nd' });
|
|
214
|
+
mgr.replaceLines(id, 2, 3, 'X');
|
|
215
|
+
expect(mgr.getContent(id)).toBe('a\nX\nd');
|
|
216
|
+
});
|
|
217
|
+
it('returns error for startLine out of range', () => {
|
|
218
|
+
const mgr = createManager();
|
|
219
|
+
const id = mgr.create({ content: 'a' });
|
|
220
|
+
const result = mgr.replaceLines(id, 0, 1, 'x');
|
|
221
|
+
expect(result.message).toContain('Error');
|
|
222
|
+
});
|
|
223
|
+
it('returns error for endLine out of range', () => {
|
|
224
|
+
const mgr = createManager();
|
|
225
|
+
const id = mgr.create({ content: 'a\nb' });
|
|
226
|
+
const result = mgr.replaceLines(id, 1, 5, 'x');
|
|
227
|
+
expect(result.message).toContain('Error');
|
|
228
|
+
});
|
|
229
|
+
it('returns error when endLine < startLine', () => {
|
|
230
|
+
const mgr = createManager();
|
|
231
|
+
const id = mgr.create({ content: 'a\nb\nc' });
|
|
232
|
+
const result = mgr.replaceLines(id, 3, 1, 'x');
|
|
233
|
+
expect(result.message).toContain('Error');
|
|
234
|
+
});
|
|
235
|
+
it('returns null for unknown scratchpad', () => {
|
|
236
|
+
const mgr = createManager();
|
|
237
|
+
expect(mgr.replaceLines('sp-nope', 1, 1, 'x')).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
describe('removeLines()', () => {
|
|
241
|
+
it('removes a single line', () => {
|
|
242
|
+
const mgr = createManager();
|
|
243
|
+
const id = mgr.create({ content: 'a\nb\nc' });
|
|
244
|
+
const result = mgr.removeLines(id, 2);
|
|
245
|
+
expect(result.message).toContain('Removed 1 line(s)');
|
|
246
|
+
expect(mgr.getContent(id)).toBe('a\nc');
|
|
247
|
+
});
|
|
248
|
+
it('removes a range of lines', () => {
|
|
249
|
+
const mgr = createManager();
|
|
250
|
+
const id = mgr.create({ content: 'a\nb\nc\nd' });
|
|
251
|
+
mgr.removeLines(id, 2, 3);
|
|
252
|
+
expect(mgr.getContent(id)).toBe('a\nd');
|
|
253
|
+
});
|
|
254
|
+
it('removes all lines', () => {
|
|
255
|
+
const mgr = createManager();
|
|
256
|
+
const id = mgr.create({ content: 'a\nb' });
|
|
257
|
+
mgr.removeLines(id, 1, 2);
|
|
258
|
+
expect(mgr.getContent(id)).toBe('');
|
|
259
|
+
});
|
|
260
|
+
it('returns error for startLine out of range', () => {
|
|
261
|
+
const mgr = createManager();
|
|
262
|
+
const id = mgr.create({ content: 'a' });
|
|
263
|
+
const result = mgr.removeLines(id, 0);
|
|
264
|
+
expect(result.message).toContain('Error');
|
|
265
|
+
});
|
|
266
|
+
it('returns error for endLine out of range', () => {
|
|
267
|
+
const mgr = createManager();
|
|
268
|
+
const id = mgr.create({ content: 'a\nb' });
|
|
269
|
+
const result = mgr.removeLines(id, 1, 5);
|
|
270
|
+
expect(result.message).toContain('Error');
|
|
271
|
+
});
|
|
272
|
+
it('returns context showing buffer now empty', () => {
|
|
273
|
+
const mgr = createManager();
|
|
274
|
+
const id = mgr.create({ content: 'only' });
|
|
275
|
+
const result = mgr.removeLines(id, 1);
|
|
276
|
+
expect(result.context).toContain('(buffer now empty)');
|
|
277
|
+
});
|
|
278
|
+
it('returns null for unknown scratchpad', () => {
|
|
279
|
+
const mgr = createManager();
|
|
280
|
+
expect(mgr.removeLines('sp-nope', 1)).toBeNull();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
// ── 4. copyLines() ────────────────────────────────────────
|
|
284
|
+
describe('copyLines()', () => {
|
|
285
|
+
it('copies lines between scratchpads', () => {
|
|
286
|
+
const mgr = createManager();
|
|
287
|
+
const src = mgr.create({ content: 'alpha\nbeta\ngamma' });
|
|
288
|
+
const tgt = mgr.create({ content: 'one\ntwo' });
|
|
289
|
+
const result = mgr.copyLines(tgt, src, 1, 2, 1);
|
|
290
|
+
expect(result.message).toContain('Copied 2 line(s)');
|
|
291
|
+
expect(mgr.getContent(tgt)).toBe('one\nalpha\nbeta\ntwo');
|
|
292
|
+
});
|
|
293
|
+
it('does not modify the source scratchpad', () => {
|
|
294
|
+
const mgr = createManager();
|
|
295
|
+
const src = mgr.create({ content: 'a\nb\nc' });
|
|
296
|
+
const tgt = mgr.create();
|
|
297
|
+
mgr.copyLines(tgt, src, 1, 3, 0);
|
|
298
|
+
expect(mgr.getContent(src)).toBe('a\nb\nc');
|
|
299
|
+
});
|
|
300
|
+
it('returns error for invalid source range', () => {
|
|
301
|
+
const mgr = createManager();
|
|
302
|
+
const src = mgr.create({ content: 'a' });
|
|
303
|
+
const tgt = mgr.create();
|
|
304
|
+
const result = mgr.copyLines(tgt, src, 0, 1, 0);
|
|
305
|
+
expect(result.message).toContain('Error');
|
|
306
|
+
});
|
|
307
|
+
it('returns error for source endLine out of range', () => {
|
|
308
|
+
const mgr = createManager();
|
|
309
|
+
const src = mgr.create({ content: 'a' });
|
|
310
|
+
const tgt = mgr.create();
|
|
311
|
+
const result = mgr.copyLines(tgt, src, 1, 5, 0);
|
|
312
|
+
expect(result.message).toContain('Error');
|
|
313
|
+
});
|
|
314
|
+
it('returns error for target afterLine out of range', () => {
|
|
315
|
+
const mgr = createManager();
|
|
316
|
+
const src = mgr.create({ content: 'a' });
|
|
317
|
+
const tgt = mgr.create({ content: 'b' });
|
|
318
|
+
const result = mgr.copyLines(tgt, src, 1, 1, 99);
|
|
319
|
+
expect(result.message).toContain('Error');
|
|
320
|
+
});
|
|
321
|
+
it('returns error when source scratchpad does not exist', () => {
|
|
322
|
+
const mgr = createManager();
|
|
323
|
+
const tgt = mgr.create();
|
|
324
|
+
const result = mgr.copyLines(tgt, 'sp-nonexistent', 1, 1, 0);
|
|
325
|
+
expect(result.message).toContain('Error');
|
|
326
|
+
expect(result.message).toContain('not found');
|
|
327
|
+
});
|
|
328
|
+
it('returns null when target scratchpad does not exist', () => {
|
|
329
|
+
const mgr = createManager();
|
|
330
|
+
const src = mgr.create({ content: 'a' });
|
|
331
|
+
expect(mgr.copyLines('sp-nope', src, 1, 1, 0)).toBeNull();
|
|
332
|
+
});
|
|
333
|
+
it('copies to afterLine=0 (prepend)', () => {
|
|
334
|
+
const mgr = createManager();
|
|
335
|
+
const src = mgr.create({ content: 'x' });
|
|
336
|
+
const tgt = mgr.create({ content: 'y' });
|
|
337
|
+
mgr.copyLines(tgt, src, 1, 1, 0);
|
|
338
|
+
expect(mgr.getContent(tgt)).toBe('x\ny');
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
// ── 5. JSON path operations ────────────────────────────────
|
|
342
|
+
describe('jsonGet()', () => {
|
|
343
|
+
it('retrieves a value at a path', () => {
|
|
344
|
+
const mgr = createManager();
|
|
345
|
+
const id = mgr.create({ content: '{"a": {"b": 42}}', format: 'json' });
|
|
346
|
+
const result = mgr.jsonGet(id, '$.a.b');
|
|
347
|
+
expect('value' in result).toBe(true);
|
|
348
|
+
if ('value' in result) {
|
|
349
|
+
expect(result.value).toBe(42);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
it('retrieves nested objects', () => {
|
|
353
|
+
const mgr = createManager();
|
|
354
|
+
const id = mgr.create({ content: '{"a": {"b": {"c": true}}}', format: 'json' });
|
|
355
|
+
const result = mgr.jsonGet(id, '$.a.b');
|
|
356
|
+
if ('value' in result) {
|
|
357
|
+
expect(result.value).toEqual({ c: true });
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
it('retrieves array elements', () => {
|
|
361
|
+
const mgr = createManager();
|
|
362
|
+
const id = mgr.create({ content: '{"items": [10, 20, 30]}', format: 'json' });
|
|
363
|
+
const result = mgr.jsonGet(id, '$.items[1]');
|
|
364
|
+
if ('value' in result) {
|
|
365
|
+
expect(result.value).toBe(20);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
it('returns error for non-json format', () => {
|
|
369
|
+
const mgr = createManager();
|
|
370
|
+
const id = mgr.create({ content: 'hello', format: 'text' });
|
|
371
|
+
const result = mgr.jsonGet(id, '$.foo');
|
|
372
|
+
expect('error' in result).toBe(true);
|
|
373
|
+
if ('error' in result) {
|
|
374
|
+
expect(result.error).toContain('json');
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
it('returns error for invalid JSON in buffer', () => {
|
|
378
|
+
const mgr = createManager();
|
|
379
|
+
const id = mgr.create({ content: '{broken', format: 'json' });
|
|
380
|
+
const result = mgr.jsonGet(id, '$.foo');
|
|
381
|
+
expect('error' in result).toBe(true);
|
|
382
|
+
if ('error' in result) {
|
|
383
|
+
expect(result.error).toContain('not valid JSON');
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
it('returns error for bad path traversal', () => {
|
|
387
|
+
const mgr = createManager();
|
|
388
|
+
const id = mgr.create({ content: '{"a": 1}', format: 'json' });
|
|
389
|
+
const result = mgr.jsonGet(id, '$.a.b.c');
|
|
390
|
+
expect('error' in result).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
it('returns null for unknown scratchpad', () => {
|
|
393
|
+
const mgr = createManager();
|
|
394
|
+
expect(mgr.jsonGet('sp-nope', '$.a')).toBeNull();
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
describe('jsonSet()', () => {
|
|
398
|
+
it('sets a value at a path', () => {
|
|
399
|
+
const mgr = createManager();
|
|
400
|
+
const id = mgr.create({ content: '{"a": 1}', format: 'json' });
|
|
401
|
+
const result = mgr.jsonSet(id, '$.a', 99);
|
|
402
|
+
expect(result.message).toContain('Set $.a');
|
|
403
|
+
const content = mgr.getContent(id);
|
|
404
|
+
expect(JSON.parse(content).a).toBe(99);
|
|
405
|
+
});
|
|
406
|
+
it('sets a nested value', () => {
|
|
407
|
+
const mgr = createManager();
|
|
408
|
+
const id = mgr.create({ content: '{"a": {"b": 1}}', format: 'json' });
|
|
409
|
+
mgr.jsonSet(id, '$.a.b', 'new');
|
|
410
|
+
expect(JSON.parse(mgr.getContent(id)).a.b).toBe('new');
|
|
411
|
+
});
|
|
412
|
+
it('returns error for non-json format', () => {
|
|
413
|
+
const mgr = createManager();
|
|
414
|
+
const id = mgr.create({ content: 'hello', format: 'text' });
|
|
415
|
+
const result = mgr.jsonSet(id, '$.a', 1);
|
|
416
|
+
expect(result.message).toContain('Error');
|
|
417
|
+
});
|
|
418
|
+
it('returns error for invalid JSON in buffer', () => {
|
|
419
|
+
const mgr = createManager();
|
|
420
|
+
const id = mgr.create({ content: '{bad', format: 'json' });
|
|
421
|
+
const result = mgr.jsonSet(id, '$.a', 1);
|
|
422
|
+
expect(result.message).toContain('Error');
|
|
423
|
+
});
|
|
424
|
+
it('returns null for unknown scratchpad', () => {
|
|
425
|
+
const mgr = createManager();
|
|
426
|
+
expect(mgr.jsonSet('sp-nope', '$.a', 1)).toBeNull();
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
describe('jsonDelete()', () => {
|
|
430
|
+
it('deletes a key from an object', () => {
|
|
431
|
+
const mgr = createManager();
|
|
432
|
+
const id = mgr.create({ content: '{"a": 1, "b": 2}', format: 'json' });
|
|
433
|
+
const result = mgr.jsonDelete(id, '$.a');
|
|
434
|
+
expect(result.message).toContain('Deleted $.a');
|
|
435
|
+
const parsed = JSON.parse(mgr.getContent(id));
|
|
436
|
+
expect(parsed.a).toBeUndefined();
|
|
437
|
+
expect(parsed.b).toBe(2);
|
|
438
|
+
});
|
|
439
|
+
it('deletes an array element by index', () => {
|
|
440
|
+
const mgr = createManager();
|
|
441
|
+
const id = mgr.create({ content: '{"items": [1, 2, 3]}', format: 'json' });
|
|
442
|
+
mgr.jsonDelete(id, '$.items[1]');
|
|
443
|
+
expect(JSON.parse(mgr.getContent(id)).items).toEqual([1, 3]);
|
|
444
|
+
});
|
|
445
|
+
it('returns error for non-json format', () => {
|
|
446
|
+
const mgr = createManager();
|
|
447
|
+
const id = mgr.create({ content: 'x', format: 'text' });
|
|
448
|
+
const result = mgr.jsonDelete(id, '$.a');
|
|
449
|
+
expect(result.message).toContain('Error');
|
|
450
|
+
});
|
|
451
|
+
it('returns error for invalid JSON in buffer', () => {
|
|
452
|
+
const mgr = createManager();
|
|
453
|
+
const id = mgr.create({ content: '{bad', format: 'json' });
|
|
454
|
+
const result = mgr.jsonDelete(id, '$.a');
|
|
455
|
+
expect(result.message).toContain('Error');
|
|
456
|
+
});
|
|
457
|
+
it('returns null for unknown scratchpad', () => {
|
|
458
|
+
const mgr = createManager();
|
|
459
|
+
expect(mgr.jsonDelete('sp-nope', '$.a')).toBeNull();
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
describe('jsonInsert()', () => {
|
|
463
|
+
it('pushes a value into an array', () => {
|
|
464
|
+
const mgr = createManager();
|
|
465
|
+
const id = mgr.create({ content: '{"items": [1, 2]}', format: 'json' });
|
|
466
|
+
const result = mgr.jsonInsert(id, '$.items', 3);
|
|
467
|
+
expect(result.message).toContain('Inserted into $.items');
|
|
468
|
+
expect(JSON.parse(mgr.getContent(id)).items).toEqual([1, 2, 3]);
|
|
469
|
+
});
|
|
470
|
+
it('returns error when target is not an array', () => {
|
|
471
|
+
const mgr = createManager();
|
|
472
|
+
const id = mgr.create({ content: '{"a": "string"}', format: 'json' });
|
|
473
|
+
const result = mgr.jsonInsert(id, '$.a', 'val');
|
|
474
|
+
expect(result.message).toContain('not an array');
|
|
475
|
+
});
|
|
476
|
+
it('returns error for non-json format', () => {
|
|
477
|
+
const mgr = createManager();
|
|
478
|
+
const id = mgr.create({ content: 'x', format: 'text' });
|
|
479
|
+
const result = mgr.jsonInsert(id, '$.a', 1);
|
|
480
|
+
expect(result.message).toContain('Error');
|
|
481
|
+
});
|
|
482
|
+
it('returns error for invalid JSON in buffer', () => {
|
|
483
|
+
const mgr = createManager();
|
|
484
|
+
const id = mgr.create({ content: '{bad', format: 'json' });
|
|
485
|
+
const result = mgr.jsonInsert(id, '$.a', 1);
|
|
486
|
+
expect(result.message).toContain('Error');
|
|
487
|
+
});
|
|
488
|
+
it('returns error for bad path', () => {
|
|
489
|
+
const mgr = createManager();
|
|
490
|
+
const id = mgr.create({ content: '{"a": 1}', format: 'json' });
|
|
491
|
+
const result = mgr.jsonInsert(id, '$.a.b.c', 1);
|
|
492
|
+
expect(result.message).toContain('Error');
|
|
493
|
+
});
|
|
494
|
+
it('returns null for unknown scratchpad', () => {
|
|
495
|
+
const mgr = createManager();
|
|
496
|
+
expect(mgr.jsonInsert('sp-nope', '$.a', 1)).toBeNull();
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
// ── 6. Attachments ─────────────────────────────────────────
|
|
500
|
+
describe('attach()', () => {
|
|
501
|
+
const sampleRef = {
|
|
502
|
+
source: 'drive',
|
|
503
|
+
filename: 'report.pdf',
|
|
504
|
+
mimeType: 'application/pdf',
|
|
505
|
+
size: 2048,
|
|
506
|
+
location: 'drive://abc123',
|
|
507
|
+
};
|
|
508
|
+
it('attaches a file and inserts a marker line at the end', () => {
|
|
509
|
+
const mgr = createManager();
|
|
510
|
+
const id = mgr.create({ content: 'hello' });
|
|
511
|
+
const result = mgr.attach(id, sampleRef);
|
|
512
|
+
expect(result.refId).toBe('att-1');
|
|
513
|
+
expect(result.message).toContain('report.pdf');
|
|
514
|
+
expect(result.message).toContain('att-1');
|
|
515
|
+
// Marker inserted at end (line 2)
|
|
516
|
+
const sp = mgr.get(id);
|
|
517
|
+
expect(sp.lines.length).toBe(2);
|
|
518
|
+
expect(sp.lines[1]).toContain('att:att-1');
|
|
519
|
+
});
|
|
520
|
+
it('attaches a file after a specific line', () => {
|
|
521
|
+
const mgr = createManager();
|
|
522
|
+
const id = mgr.create({ content: 'a\nb' });
|
|
523
|
+
mgr.attach(id, sampleRef, 1);
|
|
524
|
+
const sp = mgr.get(id);
|
|
525
|
+
expect(sp.lines.length).toBe(3);
|
|
526
|
+
// Marker inserted between a and b
|
|
527
|
+
expect(sp.lines[0]).toBe('a');
|
|
528
|
+
expect(sp.lines[1]).toContain('report.pdf');
|
|
529
|
+
expect(sp.lines[2]).toBe('b');
|
|
530
|
+
});
|
|
531
|
+
it('formats size in the marker (KB)', () => {
|
|
532
|
+
const mgr = createManager();
|
|
533
|
+
const id = mgr.create();
|
|
534
|
+
mgr.attach(id, { ...sampleRef, size: 2048 });
|
|
535
|
+
const sp = mgr.get(id);
|
|
536
|
+
expect(sp.lines[0]).toContain('2.0 KB');
|
|
537
|
+
});
|
|
538
|
+
it('formats size in the marker (MB)', () => {
|
|
539
|
+
const mgr = createManager();
|
|
540
|
+
const id = mgr.create();
|
|
541
|
+
mgr.attach(id, { ...sampleRef, size: 5 * 1024 * 1024 });
|
|
542
|
+
const sp = mgr.get(id);
|
|
543
|
+
expect(sp.lines[0]).toContain('5.0 MB');
|
|
544
|
+
});
|
|
545
|
+
it('formats size in bytes for small files', () => {
|
|
546
|
+
const mgr = createManager();
|
|
547
|
+
const id = mgr.create();
|
|
548
|
+
mgr.attach(id, { ...sampleRef, size: 500 });
|
|
549
|
+
const sp = mgr.get(id);
|
|
550
|
+
expect(sp.lines[0]).toContain('500 B');
|
|
551
|
+
});
|
|
552
|
+
it('increments refId for multiple attachments', () => {
|
|
553
|
+
const mgr = createManager();
|
|
554
|
+
const id = mgr.create();
|
|
555
|
+
const r1 = mgr.attach(id, sampleRef);
|
|
556
|
+
const r2 = mgr.attach(id, { ...sampleRef, filename: 'second.pdf' });
|
|
557
|
+
expect(r1.refId).toBe('att-1');
|
|
558
|
+
expect(r2.refId).toBe('att-2');
|
|
559
|
+
});
|
|
560
|
+
it('returns null for unknown scratchpad', () => {
|
|
561
|
+
const mgr = createManager();
|
|
562
|
+
expect(mgr.attach('sp-nope', sampleRef)).toBeNull();
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
describe('detach()', () => {
|
|
566
|
+
it('removes attachment from side-table but leaves marker', () => {
|
|
567
|
+
const mgr = createManager();
|
|
568
|
+
const id = mgr.create({ content: 'hello' });
|
|
569
|
+
mgr.attach(id, {
|
|
570
|
+
source: 'drive',
|
|
571
|
+
filename: 'file.pdf',
|
|
572
|
+
mimeType: 'application/pdf',
|
|
573
|
+
size: 100,
|
|
574
|
+
location: 'x',
|
|
575
|
+
});
|
|
576
|
+
const result = mgr.detach(id, 'att-1');
|
|
577
|
+
expect(result).toContain('Detached');
|
|
578
|
+
expect(result).toContain('file.pdf');
|
|
579
|
+
// Marker line still in buffer
|
|
580
|
+
expect(mgr.get(id).lines.length).toBe(2);
|
|
581
|
+
// But attachment removed from map
|
|
582
|
+
expect(mgr.getAttachments(id).size).toBe(0);
|
|
583
|
+
});
|
|
584
|
+
it('returns error when refId not found', () => {
|
|
585
|
+
const mgr = createManager();
|
|
586
|
+
const id = mgr.create();
|
|
587
|
+
const result = mgr.detach(id, 'att-999');
|
|
588
|
+
expect(result).toContain('Error');
|
|
589
|
+
});
|
|
590
|
+
it('returns null for unknown scratchpad', () => {
|
|
591
|
+
const mgr = createManager();
|
|
592
|
+
expect(mgr.detach('sp-nope', 'att-1')).toBeNull();
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
describe('getAttachments()', () => {
|
|
596
|
+
it('returns attachment map', () => {
|
|
597
|
+
const mgr = createManager();
|
|
598
|
+
const id = mgr.create();
|
|
599
|
+
mgr.attach(id, {
|
|
600
|
+
source: 'workspace',
|
|
601
|
+
filename: 'a.txt',
|
|
602
|
+
mimeType: 'text/plain',
|
|
603
|
+
size: 10,
|
|
604
|
+
location: '/a.txt',
|
|
605
|
+
});
|
|
606
|
+
const atts = mgr.getAttachments(id);
|
|
607
|
+
expect(atts.size).toBe(1);
|
|
608
|
+
expect(atts.get('att-1').filename).toBe('a.txt');
|
|
609
|
+
});
|
|
610
|
+
it('returns null for unknown scratchpad', () => {
|
|
611
|
+
const mgr = createManager();
|
|
612
|
+
expect(mgr.getAttachments('sp-nope')).toBeNull();
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
// ── 7. Live binding ────────────────────────────────────────
|
|
616
|
+
describe('setBinding() / getBinding()', () => {
|
|
617
|
+
const binding = {
|
|
618
|
+
service: 'docs',
|
|
619
|
+
resourceId: 'doc-abc',
|
|
620
|
+
account: 'user@example.com',
|
|
621
|
+
};
|
|
622
|
+
it('sets and retrieves a binding', () => {
|
|
623
|
+
const mgr = createManager();
|
|
624
|
+
const id = mgr.create();
|
|
625
|
+
expect(mgr.setBinding(id, binding)).toBe(true);
|
|
626
|
+
expect(mgr.getBinding(id)).toEqual(binding);
|
|
627
|
+
});
|
|
628
|
+
it('returns false for setBinding on unknown scratchpad', () => {
|
|
629
|
+
const mgr = createManager();
|
|
630
|
+
expect(mgr.setBinding('sp-nope', binding)).toBe(false);
|
|
631
|
+
});
|
|
632
|
+
it('returns undefined for getBinding on unknown scratchpad', () => {
|
|
633
|
+
const mgr = createManager();
|
|
634
|
+
expect(mgr.getBinding('sp-nope')).toBeUndefined();
|
|
635
|
+
});
|
|
636
|
+
it('returns undefined when no binding is set', () => {
|
|
637
|
+
const mgr = createManager();
|
|
638
|
+
const id = mgr.create();
|
|
639
|
+
expect(mgr.getBinding(id)).toBeUndefined();
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
// ── 8. Format validation ──────────────────────────────────
|
|
643
|
+
describe('format validation', () => {
|
|
644
|
+
it('text format is always valid', () => {
|
|
645
|
+
const mgr = createManager();
|
|
646
|
+
const id = mgr.create({ content: 'anything goes {{{{', format: 'text' });
|
|
647
|
+
const view = mgr.view(id);
|
|
648
|
+
expect(view).toContain('Status: valid');
|
|
649
|
+
});
|
|
650
|
+
it('empty buffer shows status empty', () => {
|
|
651
|
+
const mgr = createManager();
|
|
652
|
+
const id = mgr.create({ format: 'json' });
|
|
653
|
+
const view = mgr.view(id);
|
|
654
|
+
expect(view).toContain('Status: empty');
|
|
655
|
+
});
|
|
656
|
+
it('markdown detects unclosed code fence', () => {
|
|
657
|
+
const mgr = createManager();
|
|
658
|
+
const id = mgr.create({ content: '# Title\n```js\ncode', format: 'markdown' });
|
|
659
|
+
const view = mgr.view(id);
|
|
660
|
+
expect(view).toContain('Status: invalid');
|
|
661
|
+
expect(view).toContain('unclosed code fence');
|
|
662
|
+
});
|
|
663
|
+
it('markdown is valid with matched fences', () => {
|
|
664
|
+
const mgr = createManager();
|
|
665
|
+
const id = mgr.create({ content: '```\ncode\n```', format: 'markdown' });
|
|
666
|
+
const view = mgr.view(id);
|
|
667
|
+
expect(view).toContain('Status: valid');
|
|
668
|
+
});
|
|
669
|
+
it('json shows parse error with line info', () => {
|
|
670
|
+
const mgr = createManager();
|
|
671
|
+
// Missing closing brace — error at some position
|
|
672
|
+
const id = mgr.create({ content: '{\n "a": 1,\n "b":\n}', format: 'json' });
|
|
673
|
+
const view = mgr.view(id);
|
|
674
|
+
expect(view).toContain('Status: invalid');
|
|
675
|
+
});
|
|
676
|
+
it('json is valid for correct JSON', () => {
|
|
677
|
+
const mgr = createManager();
|
|
678
|
+
const id = mgr.create({ content: '{"a": 1}', format: 'json' });
|
|
679
|
+
const view = mgr.view(id);
|
|
680
|
+
expect(view).toContain('Status: valid');
|
|
681
|
+
});
|
|
682
|
+
it('csv detects column inconsistency', () => {
|
|
683
|
+
const mgr = createManager();
|
|
684
|
+
const id = mgr.create({ content: 'a,b,c\n1,2\n3,4,5', format: 'csv' });
|
|
685
|
+
const view = mgr.view(id);
|
|
686
|
+
expect(view).toContain('Status: invalid');
|
|
687
|
+
expect(view).toContain('expected 3 columns, got 2');
|
|
688
|
+
});
|
|
689
|
+
it('csv is valid with consistent columns', () => {
|
|
690
|
+
const mgr = createManager();
|
|
691
|
+
const id = mgr.create({ content: 'a,b,c\n1,2,3\n4,5,6', format: 'csv' });
|
|
692
|
+
const view = mgr.view(id);
|
|
693
|
+
expect(view).toContain('Status: valid');
|
|
694
|
+
expect(view).toContain('3 columns');
|
|
695
|
+
});
|
|
696
|
+
it('csv handles quoted fields with commas', () => {
|
|
697
|
+
const mgr = createManager();
|
|
698
|
+
const id = mgr.create({ content: 'name,value\n"Smith, John",42', format: 'csv' });
|
|
699
|
+
const view = mgr.view(id);
|
|
700
|
+
expect(view).toContain('Status: valid');
|
|
701
|
+
expect(view).toContain('2 columns');
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
// ── 9. Epoch-based GC ─────────────────────────────────────
|
|
705
|
+
describe('epoch-based garbage collection', () => {
|
|
706
|
+
it('scratchpad is accessible when epoch is within range', () => {
|
|
707
|
+
const mgr = createManager();
|
|
708
|
+
mockEpoch = 10;
|
|
709
|
+
const id = mgr.create();
|
|
710
|
+
mockEpoch = 110; // 100 epochs later — exactly at boundary
|
|
711
|
+
expect(mgr.get(id)).not.toBeNull();
|
|
712
|
+
});
|
|
713
|
+
it('scratchpad expires after 100 epochs without touch', () => {
|
|
714
|
+
const mgr = createManager();
|
|
715
|
+
mockEpoch = 0;
|
|
716
|
+
const id = mgr.create();
|
|
717
|
+
mockEpoch = 101; // 101 > 100, expired
|
|
718
|
+
expect(mgr.get(id)).toBeNull();
|
|
719
|
+
});
|
|
720
|
+
it('touching a scratchpad resets its epoch', () => {
|
|
721
|
+
const mgr = createManager();
|
|
722
|
+
mockEpoch = 0;
|
|
723
|
+
const id = mgr.create({ content: 'data' });
|
|
724
|
+
mockEpoch = 50;
|
|
725
|
+
mgr.view(id); // touches it, resetting to epoch 50
|
|
726
|
+
mockEpoch = 150; // 100 from creation, but only 100 from touch — at boundary
|
|
727
|
+
expect(mgr.get(id)).not.toBeNull();
|
|
728
|
+
mockEpoch = 151; // now expired
|
|
729
|
+
expect(mgr.get(id)).toBeNull();
|
|
730
|
+
});
|
|
731
|
+
it('gc runs during create() and removes expired pads', () => {
|
|
732
|
+
const mgr = createManager();
|
|
733
|
+
mockEpoch = 0;
|
|
734
|
+
const old = mgr.create({ label: 'old' });
|
|
735
|
+
mockEpoch = 200;
|
|
736
|
+
mgr.create({ label: 'new' }); // triggers gc
|
|
737
|
+
expect(mgr.get(old)).toBeNull();
|
|
738
|
+
});
|
|
739
|
+
it('gc runs during list() and removes expired pads', () => {
|
|
740
|
+
const mgr = createManager();
|
|
741
|
+
mockEpoch = 0;
|
|
742
|
+
mgr.create({ label: 'old' });
|
|
743
|
+
mockEpoch = 200;
|
|
744
|
+
const items = mgr.list();
|
|
745
|
+
expect(items.length).toBe(0);
|
|
746
|
+
});
|
|
747
|
+
it('mutation operations touch the scratchpad', () => {
|
|
748
|
+
const mgr = createManager();
|
|
749
|
+
mockEpoch = 0;
|
|
750
|
+
const id = mgr.create({ content: 'a' });
|
|
751
|
+
mockEpoch = 50;
|
|
752
|
+
mgr.appendLines(id, 'b'); // touch at 50
|
|
753
|
+
mockEpoch = 150;
|
|
754
|
+
expect(mgr.get(id)).not.toBeNull(); // 150 - 50 = 100, at boundary
|
|
755
|
+
mockEpoch = 151;
|
|
756
|
+
expect(mgr.get(id)).toBeNull();
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
// ── 10. Mutation response format ──────────────────────────
|
|
760
|
+
describe('mutation response format', () => {
|
|
761
|
+
it('includes context markers showing affected lines', () => {
|
|
762
|
+
const mgr = createManager();
|
|
763
|
+
const id = mgr.create({ content: 'a\nb\nc\nd\ne' });
|
|
764
|
+
const result = mgr.insertLines(id, 2, 'x');
|
|
765
|
+
// Context should show surrounding lines
|
|
766
|
+
expect(result.context).toBeTruthy();
|
|
767
|
+
expect(result.context).toContain('x');
|
|
768
|
+
});
|
|
769
|
+
it('includes validation status in every mutation result', () => {
|
|
770
|
+
const mgr = createManager();
|
|
771
|
+
const id = mgr.create({ content: '{"a": 1}', format: 'json' });
|
|
772
|
+
const result = mgr.appendLines(id, '"b": 2}');
|
|
773
|
+
expect(result.validation).toBeTruthy();
|
|
774
|
+
// After appending broken JSON, validation should flag it
|
|
775
|
+
expect(result.validation).toContain('Status:');
|
|
776
|
+
});
|
|
777
|
+
it('context elides middle for large insertions', () => {
|
|
778
|
+
const mgr = createManager();
|
|
779
|
+
const id = mgr.create({ content: 'before\nafter' });
|
|
780
|
+
const result = mgr.insertLines(id, 1, 'x\ny\nz');
|
|
781
|
+
// 3 affected lines — the middle should be elided (> 2 affected)
|
|
782
|
+
expect(result.context).toContain('...');
|
|
783
|
+
});
|
|
784
|
+
it('context shows one line before and after', () => {
|
|
785
|
+
const mgr = createManager();
|
|
786
|
+
const id = mgr.create({ content: 'before\nold\nafter' });
|
|
787
|
+
const result = mgr.replaceLines(id, 2, 2, 'new');
|
|
788
|
+
expect(result.context).toContain('before');
|
|
789
|
+
expect(result.context).toContain('new');
|
|
790
|
+
expect(result.context).toContain('after');
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
// ── 11. CRLF normalization ─────────────────────────────────
|
|
794
|
+
describe('CRLF normalization', () => {
|
|
795
|
+
it('normalizes CRLF to LF on create', () => {
|
|
796
|
+
const mgr = createManager();
|
|
797
|
+
const id = mgr.create({ content: 'line1\r\nline2\r\nline3' });
|
|
798
|
+
expect(mgr.get(id).lines).toEqual(['line1', 'line2', 'line3']);
|
|
799
|
+
});
|
|
800
|
+
it('normalizes CR to LF on create', () => {
|
|
801
|
+
const mgr = createManager();
|
|
802
|
+
const id = mgr.create({ content: 'line1\rline2' });
|
|
803
|
+
expect(mgr.get(id).lines).toEqual(['line1', 'line2']);
|
|
804
|
+
});
|
|
805
|
+
it('normalizes CRLF on insertLines', () => {
|
|
806
|
+
const mgr = createManager();
|
|
807
|
+
const id = mgr.create({ content: 'a' });
|
|
808
|
+
mgr.insertLines(id, 1, 'b\r\nc');
|
|
809
|
+
expect(mgr.getContent(id)).toBe('a\nb\nc');
|
|
810
|
+
});
|
|
811
|
+
it('normalizes CRLF on appendLines', () => {
|
|
812
|
+
const mgr = createManager();
|
|
813
|
+
const id = mgr.create();
|
|
814
|
+
mgr.appendLines(id, 'x\r\ny');
|
|
815
|
+
expect(mgr.getContent(id)).toBe('x\ny');
|
|
816
|
+
});
|
|
817
|
+
it('normalizes CRLF on replaceLines', () => {
|
|
818
|
+
const mgr = createManager();
|
|
819
|
+
const id = mgr.create({ content: 'a\nb' });
|
|
820
|
+
mgr.replaceLines(id, 1, 1, 'x\r\ny');
|
|
821
|
+
expect(mgr.getContent(id)).toBe('x\ny\nb');
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
// ── 12. discard() and list() lifecycle ─────────────────────
|
|
825
|
+
describe('discard() and list()', () => {
|
|
826
|
+
it('discard removes a scratchpad', () => {
|
|
827
|
+
const mgr = createManager();
|
|
828
|
+
const id = mgr.create();
|
|
829
|
+
expect(mgr.discard(id)).toBe(true);
|
|
830
|
+
expect(mgr.get(id)).toBeNull();
|
|
831
|
+
});
|
|
832
|
+
it('discard returns false for unknown id', () => {
|
|
833
|
+
const mgr = createManager();
|
|
834
|
+
expect(mgr.discard('sp-nonexistent')).toBe(false);
|
|
835
|
+
});
|
|
836
|
+
it('list returns summaries of all active scratchpads', () => {
|
|
837
|
+
const mgr = createManager();
|
|
838
|
+
mgr.create({ label: 'first', content: 'a\nb', format: 'text' });
|
|
839
|
+
mgr.create({ label: 'second', format: 'json' });
|
|
840
|
+
const items = mgr.list();
|
|
841
|
+
expect(items.length).toBe(2);
|
|
842
|
+
expect(items[0].label).toBe('first');
|
|
843
|
+
expect(items[0].lineCount).toBe(2);
|
|
844
|
+
expect(items[0].format).toBe('text');
|
|
845
|
+
expect(items[0].bound).toBe(false);
|
|
846
|
+
expect(items[1].label).toBe('second');
|
|
847
|
+
expect(items[1].lineCount).toBe(0);
|
|
848
|
+
});
|
|
849
|
+
it('list excludes discarded scratchpads', () => {
|
|
850
|
+
const mgr = createManager();
|
|
851
|
+
const id1 = mgr.create({ label: 'keep' });
|
|
852
|
+
const id2 = mgr.create({ label: 'drop' });
|
|
853
|
+
mgr.discard(id2);
|
|
854
|
+
const items = mgr.list();
|
|
855
|
+
expect(items.length).toBe(1);
|
|
856
|
+
expect(items[0].label).toBe('keep');
|
|
857
|
+
});
|
|
858
|
+
it('list includes validation status', () => {
|
|
859
|
+
const mgr = createManager();
|
|
860
|
+
mgr.create({ content: '{"a": 1}', format: 'json' });
|
|
861
|
+
const items = mgr.list();
|
|
862
|
+
expect(items[0].validation).toContain('Status: valid');
|
|
863
|
+
});
|
|
864
|
+
it('list includes attachment count', () => {
|
|
865
|
+
const mgr = createManager();
|
|
866
|
+
const id = mgr.create();
|
|
867
|
+
mgr.attach(id, {
|
|
868
|
+
source: 'drive',
|
|
869
|
+
filename: 'f.txt',
|
|
870
|
+
mimeType: 'text/plain',
|
|
871
|
+
size: 10,
|
|
872
|
+
location: 'x',
|
|
873
|
+
});
|
|
874
|
+
const items = mgr.list();
|
|
875
|
+
expect(items[0].attachmentCount).toBe(1);
|
|
876
|
+
});
|
|
877
|
+
it('list shows bound status', () => {
|
|
878
|
+
const mgr = createManager();
|
|
879
|
+
const id = mgr.create();
|
|
880
|
+
mgr.setBinding(id, { service: 'sheets', resourceId: 's1', account: 'a@b.com' });
|
|
881
|
+
const items = mgr.list();
|
|
882
|
+
expect(items[0].bound).toBe(true);
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
//# sourceMappingURL=manager.test.js.map
|