@cleocode/contracts 2026.4.68 → 2026.4.70
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/__tests__/acceptance-gate.test.d.ts +14 -0
- package/dist/__tests__/acceptance-gate.test.d.ts.map +1 -0
- package/dist/__tests__/acceptance-gate.test.js +642 -0
- package/dist/__tests__/acceptance-gate.test.js.map +1 -0
- package/dist/__tests__/attachment.test.d.ts +17 -0
- package/dist/__tests__/attachment.test.d.ts.map +1 -0
- package/dist/__tests__/attachment.test.js +505 -0
- package/dist/__tests__/attachment.test.js.map +1 -0
- package/dist/acceptance-gate-schema.d.ts +624 -0
- package/dist/acceptance-gate-schema.d.ts.map +1 -0
- package/dist/acceptance-gate-schema.js +244 -0
- package/dist/acceptance-gate-schema.js.map +1 -0
- package/dist/acceptance-gate.d.ts +253 -0
- package/dist/acceptance-gate.d.ts.map +1 -0
- package/dist/acceptance-gate.js +22 -0
- package/dist/acceptance-gate.js.map +1 -0
- package/dist/attachment-schema.d.ts +243 -0
- package/dist/attachment-schema.d.ts.map +1 -0
- package/dist/attachment-schema.js +200 -0
- package/dist/attachment-schema.js.map +1 -0
- package/dist/attachment.d.ts +224 -0
- package/dist/attachment.d.ts.map +1 -0
- package/dist/attachment.js +21 -0
- package/dist/attachment.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/task.d.ts +23 -4
- package/dist/task.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/acceptance-gate.test.ts +736 -0
- package/src/__tests__/attachment.test.ts +601 -0
- package/src/acceptance-gate-schema.ts +283 -0
- package/src/acceptance-gate.ts +264 -0
- package/src/attachment-schema.ts +223 -0
- package/src/attachment.ts +245 -0
- package/src/index.ts +49 -0
- package/src/task.ts +24 -4
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round-trip Zod parse tests for `Attachment` variants, `AttachmentMetadata`,
|
|
3
|
+
* and `AttachmentRef`.
|
|
4
|
+
*
|
|
5
|
+
* Each test constructs a valid input object for one schema variant, parses it
|
|
6
|
+
* through the corresponding Zod schema, and asserts the parsed output matches
|
|
7
|
+
* the input shape exactly. This guarantees schemas accept all valid shapes
|
|
8
|
+
* without transformation loss.
|
|
9
|
+
*
|
|
10
|
+
* Coverage: 5 variants × 2 cases (minimal + full) = 10 round-trip tests,
|
|
11
|
+
* plus invalid-input rejection tests for each variant, and metadata/ref tests.
|
|
12
|
+
*
|
|
13
|
+
* @epic T760
|
|
14
|
+
* @task T795
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, expect, it } from 'vitest';
|
|
18
|
+
import type { Attachment, AttachmentMetadata, AttachmentRef } from '../attachment.js';
|
|
19
|
+
import {
|
|
20
|
+
attachmentMetadataSchema,
|
|
21
|
+
attachmentRefSchema,
|
|
22
|
+
attachmentSchema,
|
|
23
|
+
blobAttachmentSchema,
|
|
24
|
+
llmsTxtAttachmentSchema,
|
|
25
|
+
llmtxtDocAttachmentSchema,
|
|
26
|
+
localFileAttachmentSchema,
|
|
27
|
+
urlAttachmentSchema,
|
|
28
|
+
} from '../attachment-schema.js';
|
|
29
|
+
|
|
30
|
+
/** Canonical 64-character SHA-256 hex string used across all tests. */
|
|
31
|
+
const SHA256 = 'a'.repeat(64);
|
|
32
|
+
|
|
33
|
+
// ─── LocalFileAttachment ──────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
describe('localFileAttachmentSchema', () => {
|
|
36
|
+
it('round-trips a minimal local-file attachment', () => {
|
|
37
|
+
const input = {
|
|
38
|
+
kind: 'local-file' as const,
|
|
39
|
+
path: 'docs/rfc-003.md',
|
|
40
|
+
sha256: SHA256,
|
|
41
|
+
mime: 'text/markdown',
|
|
42
|
+
size: 8192,
|
|
43
|
+
};
|
|
44
|
+
const result = localFileAttachmentSchema.parse(input);
|
|
45
|
+
expect(result.kind).toBe('local-file');
|
|
46
|
+
expect(result.path).toBe('docs/rfc-003.md');
|
|
47
|
+
expect(result.sha256).toBe(SHA256);
|
|
48
|
+
expect(result.mime).toBe('text/markdown');
|
|
49
|
+
expect(result.size).toBe(8192);
|
|
50
|
+
expect(result.description).toBeUndefined();
|
|
51
|
+
expect(result.labels).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('round-trips a full local-file attachment with optional fields', () => {
|
|
55
|
+
const input = {
|
|
56
|
+
kind: 'local-file' as const,
|
|
57
|
+
path: '/absolute/path/to/report.pdf',
|
|
58
|
+
sha256: SHA256,
|
|
59
|
+
mime: 'application/pdf',
|
|
60
|
+
size: 1_048_576,
|
|
61
|
+
description: 'Pen-test report Q1 2026',
|
|
62
|
+
labels: ['security', 'report'],
|
|
63
|
+
};
|
|
64
|
+
const result = localFileAttachmentSchema.parse(input);
|
|
65
|
+
expect(result.description).toBe('Pen-test report Q1 2026');
|
|
66
|
+
expect(result.labels).toEqual(['security', 'report']);
|
|
67
|
+
expect(result.size).toBe(1_048_576);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('rejects a sha256 that is not 64 characters', () => {
|
|
71
|
+
expect(() =>
|
|
72
|
+
localFileAttachmentSchema.parse({
|
|
73
|
+
kind: 'local-file',
|
|
74
|
+
path: 'docs/rfc.md',
|
|
75
|
+
sha256: 'tooshort',
|
|
76
|
+
mime: 'text/markdown',
|
|
77
|
+
size: 100,
|
|
78
|
+
}),
|
|
79
|
+
).toThrow();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rejects a negative size', () => {
|
|
83
|
+
expect(() =>
|
|
84
|
+
localFileAttachmentSchema.parse({
|
|
85
|
+
kind: 'local-file',
|
|
86
|
+
path: 'docs/rfc.md',
|
|
87
|
+
sha256: SHA256,
|
|
88
|
+
mime: 'text/markdown',
|
|
89
|
+
size: -1,
|
|
90
|
+
}),
|
|
91
|
+
).toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('rejects an empty path', () => {
|
|
95
|
+
expect(() =>
|
|
96
|
+
localFileAttachmentSchema.parse({
|
|
97
|
+
kind: 'local-file',
|
|
98
|
+
path: '',
|
|
99
|
+
sha256: SHA256,
|
|
100
|
+
mime: 'text/markdown',
|
|
101
|
+
size: 100,
|
|
102
|
+
}),
|
|
103
|
+
).toThrow();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ─── UrlAttachment ────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
describe('urlAttachmentSchema', () => {
|
|
110
|
+
it('round-trips a minimal url attachment', () => {
|
|
111
|
+
const input = {
|
|
112
|
+
kind: 'url' as const,
|
|
113
|
+
url: 'https://llmstxt.org',
|
|
114
|
+
};
|
|
115
|
+
const result = urlAttachmentSchema.parse(input);
|
|
116
|
+
expect(result.kind).toBe('url');
|
|
117
|
+
expect(result.url).toBe('https://llmstxt.org');
|
|
118
|
+
expect(result.cachedSha256).toBeUndefined();
|
|
119
|
+
expect(result.cachedAt).toBeUndefined();
|
|
120
|
+
expect(result.mime).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('round-trips a fully-cached url attachment', () => {
|
|
124
|
+
const input = {
|
|
125
|
+
kind: 'url' as const,
|
|
126
|
+
url: 'https://example.com/spec.html',
|
|
127
|
+
cachedSha256: SHA256,
|
|
128
|
+
cachedAt: '2026-04-16T12:00:00.000Z',
|
|
129
|
+
mime: 'text/html',
|
|
130
|
+
description: 'llms.txt spec upstream cache',
|
|
131
|
+
labels: ['spec', 'upstream'],
|
|
132
|
+
};
|
|
133
|
+
const result = urlAttachmentSchema.parse(input);
|
|
134
|
+
expect(result.cachedSha256).toBe(SHA256);
|
|
135
|
+
expect(result.cachedAt).toBe('2026-04-16T12:00:00.000Z');
|
|
136
|
+
expect(result.mime).toBe('text/html');
|
|
137
|
+
expect(result.labels).toEqual(['spec', 'upstream']);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('rejects an invalid URL', () => {
|
|
141
|
+
expect(() =>
|
|
142
|
+
urlAttachmentSchema.parse({
|
|
143
|
+
kind: 'url',
|
|
144
|
+
url: 'not-a-url',
|
|
145
|
+
}),
|
|
146
|
+
).toThrow();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('rejects a cachedSha256 that is not 64 characters', () => {
|
|
150
|
+
expect(() =>
|
|
151
|
+
urlAttachmentSchema.parse({
|
|
152
|
+
kind: 'url',
|
|
153
|
+
url: 'https://example.com/',
|
|
154
|
+
cachedSha256: 'bad',
|
|
155
|
+
}),
|
|
156
|
+
).toThrow();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('rejects an invalid cachedAt datetime', () => {
|
|
160
|
+
expect(() =>
|
|
161
|
+
urlAttachmentSchema.parse({
|
|
162
|
+
kind: 'url',
|
|
163
|
+
url: 'https://example.com/',
|
|
164
|
+
cachedAt: 'not-a-datetime',
|
|
165
|
+
}),
|
|
166
|
+
).toThrow();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ─── BlobAttachment ───────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
describe('blobAttachmentSchema', () => {
|
|
173
|
+
it('round-trips a minimal blob attachment', () => {
|
|
174
|
+
const input = {
|
|
175
|
+
kind: 'blob' as const,
|
|
176
|
+
sha256: SHA256,
|
|
177
|
+
storageKey: 'aa/aaa…abc.pdf',
|
|
178
|
+
mime: 'application/pdf',
|
|
179
|
+
size: 2_097_152,
|
|
180
|
+
};
|
|
181
|
+
const result = blobAttachmentSchema.parse(input);
|
|
182
|
+
expect(result.kind).toBe('blob');
|
|
183
|
+
expect(result.sha256).toBe(SHA256);
|
|
184
|
+
expect(result.storageKey).toBe('aa/aaa…abc.pdf');
|
|
185
|
+
expect(result.mime).toBe('application/pdf');
|
|
186
|
+
expect(result.size).toBe(2_097_152);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('round-trips a full blob attachment with optional fields', () => {
|
|
190
|
+
const input = {
|
|
191
|
+
kind: 'blob' as const,
|
|
192
|
+
sha256: SHA256,
|
|
193
|
+
storageKey: 'ab/abcdef…12.png',
|
|
194
|
+
mime: 'image/png',
|
|
195
|
+
size: 512_000,
|
|
196
|
+
description: 'Screenshot of the failing test run',
|
|
197
|
+
labels: ['screenshot', 'evidence'],
|
|
198
|
+
};
|
|
199
|
+
const result = blobAttachmentSchema.parse(input);
|
|
200
|
+
expect(result.description).toBe('Screenshot of the failing test run');
|
|
201
|
+
expect(result.labels).toEqual(['screenshot', 'evidence']);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('rejects an empty storageKey', () => {
|
|
205
|
+
expect(() =>
|
|
206
|
+
blobAttachmentSchema.parse({
|
|
207
|
+
kind: 'blob',
|
|
208
|
+
sha256: SHA256,
|
|
209
|
+
storageKey: '',
|
|
210
|
+
mime: 'application/pdf',
|
|
211
|
+
size: 100,
|
|
212
|
+
}),
|
|
213
|
+
).toThrow();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('rejects an empty mime', () => {
|
|
217
|
+
expect(() =>
|
|
218
|
+
blobAttachmentSchema.parse({
|
|
219
|
+
kind: 'blob',
|
|
220
|
+
sha256: SHA256,
|
|
221
|
+
storageKey: 'ab/abc.pdf',
|
|
222
|
+
mime: '',
|
|
223
|
+
size: 100,
|
|
224
|
+
}),
|
|
225
|
+
).toThrow();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ─── LlmsTxtAttachment ────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
describe('llmsTxtAttachmentSchema', () => {
|
|
232
|
+
it('round-trips a minimal llms-txt attachment (generated)', () => {
|
|
233
|
+
const content = '# My Project\n\n> A great project.\n\n## Docs\n\n- [README](./README.md)';
|
|
234
|
+
const input = {
|
|
235
|
+
kind: 'llms-txt' as const,
|
|
236
|
+
source: 'generated' as const,
|
|
237
|
+
content,
|
|
238
|
+
sha256: SHA256,
|
|
239
|
+
};
|
|
240
|
+
const result = llmsTxtAttachmentSchema.parse(input);
|
|
241
|
+
expect(result.kind).toBe('llms-txt');
|
|
242
|
+
expect(result.source).toBe('generated');
|
|
243
|
+
expect(result.content).toBe(content);
|
|
244
|
+
expect(result.sha256).toBe(SHA256);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('round-trips a full llms-txt attachment (url source) with optional fields', () => {
|
|
248
|
+
const content = '# llmstxt.org\n\n> The llms.txt spec.\n\n## Spec\n\n- [Spec](./spec.md)';
|
|
249
|
+
const input = {
|
|
250
|
+
kind: 'llms-txt' as const,
|
|
251
|
+
source: 'url' as const,
|
|
252
|
+
content,
|
|
253
|
+
sha256: SHA256,
|
|
254
|
+
description: 'llmstxt.org site index snapshot',
|
|
255
|
+
labels: ['spec', 'upstream'],
|
|
256
|
+
};
|
|
257
|
+
const result = llmsTxtAttachmentSchema.parse(input);
|
|
258
|
+
expect(result.source).toBe('url');
|
|
259
|
+
expect(result.description).toBe('llmstxt.org site index snapshot');
|
|
260
|
+
expect(result.labels).toEqual(['spec', 'upstream']);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('rejects an empty content', () => {
|
|
264
|
+
expect(() =>
|
|
265
|
+
llmsTxtAttachmentSchema.parse({
|
|
266
|
+
kind: 'llms-txt',
|
|
267
|
+
source: 'generated',
|
|
268
|
+
content: '',
|
|
269
|
+
sha256: SHA256,
|
|
270
|
+
}),
|
|
271
|
+
).toThrow();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('rejects an invalid source value', () => {
|
|
275
|
+
expect(() =>
|
|
276
|
+
llmsTxtAttachmentSchema.parse({
|
|
277
|
+
kind: 'llms-txt',
|
|
278
|
+
source: 'clipboard',
|
|
279
|
+
content: '# Foo',
|
|
280
|
+
sha256: SHA256,
|
|
281
|
+
}),
|
|
282
|
+
).toThrow();
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ─── LlmtxtDocAttachment ──────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
describe('llmtxtDocAttachmentSchema', () => {
|
|
289
|
+
it('round-trips a minimal llmtxt-doc attachment (local backend)', () => {
|
|
290
|
+
const input = {
|
|
291
|
+
kind: 'llmtxt-doc' as const,
|
|
292
|
+
slug: '9fZLOnf5',
|
|
293
|
+
backend: 'local' as const,
|
|
294
|
+
};
|
|
295
|
+
const result = llmtxtDocAttachmentSchema.parse(input);
|
|
296
|
+
expect(result.kind).toBe('llmtxt-doc');
|
|
297
|
+
expect(result.slug).toBe('9fZLOnf5');
|
|
298
|
+
expect(result.backend).toBe('local');
|
|
299
|
+
expect(result.pinnedVersion).toBeUndefined();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('round-trips a full llmtxt-doc attachment (remote backend) with optional fields', () => {
|
|
303
|
+
const input = {
|
|
304
|
+
kind: 'llmtxt-doc' as const,
|
|
305
|
+
slug: 'AbCdEfGh',
|
|
306
|
+
backend: 'remote' as const,
|
|
307
|
+
pinnedVersion: '42',
|
|
308
|
+
description: 'Architecture decision record v7 on llmtxt.my',
|
|
309
|
+
labels: ['adr', 'versioned'],
|
|
310
|
+
};
|
|
311
|
+
const result = llmtxtDocAttachmentSchema.parse(input);
|
|
312
|
+
expect(result.backend).toBe('remote');
|
|
313
|
+
expect(result.pinnedVersion).toBe('42');
|
|
314
|
+
expect(result.description).toBe('Architecture decision record v7 on llmtxt.my');
|
|
315
|
+
expect(result.labels).toEqual(['adr', 'versioned']);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('rejects an empty slug', () => {
|
|
319
|
+
expect(() =>
|
|
320
|
+
llmtxtDocAttachmentSchema.parse({
|
|
321
|
+
kind: 'llmtxt-doc',
|
|
322
|
+
slug: '',
|
|
323
|
+
backend: 'local',
|
|
324
|
+
}),
|
|
325
|
+
).toThrow();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('rejects an invalid backend value', () => {
|
|
329
|
+
expect(() =>
|
|
330
|
+
llmtxtDocAttachmentSchema.parse({
|
|
331
|
+
kind: 'llmtxt-doc',
|
|
332
|
+
slug: 'abc123',
|
|
333
|
+
backend: 'cloud',
|
|
334
|
+
}),
|
|
335
|
+
).toThrow();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ─── attachmentSchema (discriminated union) ───────────────────────────────────
|
|
340
|
+
|
|
341
|
+
describe('attachmentSchema (discriminated union)', () => {
|
|
342
|
+
it('routes local-file by kind', () => {
|
|
343
|
+
const result = attachmentSchema.parse({
|
|
344
|
+
kind: 'local-file',
|
|
345
|
+
path: 'src/index.ts',
|
|
346
|
+
sha256: SHA256,
|
|
347
|
+
mime: 'application/typescript',
|
|
348
|
+
size: 1024,
|
|
349
|
+
});
|
|
350
|
+
expect(result.kind).toBe('local-file');
|
|
351
|
+
if (result.kind === 'local-file') {
|
|
352
|
+
expect(result.path).toBe('src/index.ts');
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('routes url by kind', () => {
|
|
357
|
+
const result = attachmentSchema.parse({
|
|
358
|
+
kind: 'url',
|
|
359
|
+
url: 'https://example.com/',
|
|
360
|
+
});
|
|
361
|
+
expect(result.kind).toBe('url');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('routes blob by kind', () => {
|
|
365
|
+
const result = attachmentSchema.parse({
|
|
366
|
+
kind: 'blob',
|
|
367
|
+
sha256: SHA256,
|
|
368
|
+
storageKey: 'ab/abc.pdf',
|
|
369
|
+
mime: 'application/pdf',
|
|
370
|
+
size: 100,
|
|
371
|
+
});
|
|
372
|
+
expect(result.kind).toBe('blob');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('routes llms-txt by kind', () => {
|
|
376
|
+
const result = attachmentSchema.parse({
|
|
377
|
+
kind: 'llms-txt',
|
|
378
|
+
source: 'generated',
|
|
379
|
+
content: '# Project\n\n> Summary.',
|
|
380
|
+
sha256: SHA256,
|
|
381
|
+
});
|
|
382
|
+
expect(result.kind).toBe('llms-txt');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('routes llmtxt-doc by kind', () => {
|
|
386
|
+
const result = attachmentSchema.parse({
|
|
387
|
+
kind: 'llmtxt-doc',
|
|
388
|
+
slug: 'xY12abCD',
|
|
389
|
+
backend: 'local',
|
|
390
|
+
});
|
|
391
|
+
expect(result.kind).toBe('llmtxt-doc');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('rejects an unknown kind', () => {
|
|
395
|
+
expect(() =>
|
|
396
|
+
attachmentSchema.parse({
|
|
397
|
+
kind: 'unknown-kind',
|
|
398
|
+
path: 'docs/foo.md',
|
|
399
|
+
}),
|
|
400
|
+
).toThrow();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('type-checks inferred type is assignable to Attachment', () => {
|
|
404
|
+
const raw = {
|
|
405
|
+
kind: 'blob' as const,
|
|
406
|
+
sha256: SHA256,
|
|
407
|
+
storageKey: 'ab/abc.bin',
|
|
408
|
+
mime: 'application/octet-stream',
|
|
409
|
+
size: 256,
|
|
410
|
+
};
|
|
411
|
+
const parsed = attachmentSchema.parse(raw);
|
|
412
|
+
// Structural compatibility check with the canonical Attachment type.
|
|
413
|
+
const att: Attachment = parsed;
|
|
414
|
+
expect(att.kind).toBe('blob');
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// ─── attachmentMetadataSchema ─────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
describe('attachmentMetadataSchema', () => {
|
|
421
|
+
it('round-trips a metadata row for a local-file attachment', () => {
|
|
422
|
+
const input = {
|
|
423
|
+
id: 'att_abc123',
|
|
424
|
+
sha256: SHA256,
|
|
425
|
+
attachment: {
|
|
426
|
+
kind: 'local-file' as const,
|
|
427
|
+
path: 'docs/spec.md',
|
|
428
|
+
sha256: SHA256,
|
|
429
|
+
mime: 'text/markdown',
|
|
430
|
+
size: 4096,
|
|
431
|
+
},
|
|
432
|
+
createdAt: '2026-04-16T08:00:00.000Z',
|
|
433
|
+
refCount: 3,
|
|
434
|
+
};
|
|
435
|
+
const result = attachmentMetadataSchema.parse(input);
|
|
436
|
+
expect(result.id).toBe('att_abc123');
|
|
437
|
+
expect(result.refCount).toBe(3);
|
|
438
|
+
expect(result.attachment.kind).toBe('local-file');
|
|
439
|
+
// TypeScript narrowing confirms the nested parse
|
|
440
|
+
if (result.attachment.kind === 'local-file') {
|
|
441
|
+
expect(result.attachment.path).toBe('docs/spec.md');
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('round-trips a metadata row for a url attachment with refCount=0', () => {
|
|
446
|
+
const input = {
|
|
447
|
+
id: 'att_xyz789',
|
|
448
|
+
sha256: '',
|
|
449
|
+
attachment: {
|
|
450
|
+
kind: 'url' as const,
|
|
451
|
+
url: 'https://example.com/',
|
|
452
|
+
},
|
|
453
|
+
createdAt: '2026-04-16T09:00:00.000Z',
|
|
454
|
+
refCount: 0,
|
|
455
|
+
};
|
|
456
|
+
const result = attachmentMetadataSchema.parse(input);
|
|
457
|
+
expect(result.refCount).toBe(0);
|
|
458
|
+
expect(result.sha256).toBe('');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('type-checks inferred result is assignable to AttachmentMetadata', () => {
|
|
462
|
+
const raw = {
|
|
463
|
+
id: 'att_type_check',
|
|
464
|
+
sha256: SHA256,
|
|
465
|
+
attachment: {
|
|
466
|
+
kind: 'blob' as const,
|
|
467
|
+
sha256: SHA256,
|
|
468
|
+
storageKey: 'ab/abc.pdf',
|
|
469
|
+
mime: 'application/pdf',
|
|
470
|
+
size: 512,
|
|
471
|
+
},
|
|
472
|
+
createdAt: '2026-04-16T10:00:00.000Z',
|
|
473
|
+
refCount: 1,
|
|
474
|
+
};
|
|
475
|
+
const parsed = attachmentMetadataSchema.parse(raw);
|
|
476
|
+
const meta: AttachmentMetadata = parsed;
|
|
477
|
+
expect(meta.id).toBe('att_type_check');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('rejects a negative refCount', () => {
|
|
481
|
+
expect(() =>
|
|
482
|
+
attachmentMetadataSchema.parse({
|
|
483
|
+
id: 'att_bad',
|
|
484
|
+
sha256: SHA256,
|
|
485
|
+
attachment: {
|
|
486
|
+
kind: 'local-file',
|
|
487
|
+
path: 'x.ts',
|
|
488
|
+
sha256: SHA256,
|
|
489
|
+
mime: 'text/plain',
|
|
490
|
+
size: 10,
|
|
491
|
+
},
|
|
492
|
+
createdAt: '2026-04-16T00:00:00.000Z',
|
|
493
|
+
refCount: -1,
|
|
494
|
+
}),
|
|
495
|
+
).toThrow();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('rejects an invalid createdAt timestamp', () => {
|
|
499
|
+
expect(() =>
|
|
500
|
+
attachmentMetadataSchema.parse({
|
|
501
|
+
id: 'att_bad2',
|
|
502
|
+
sha256: SHA256,
|
|
503
|
+
attachment: {
|
|
504
|
+
kind: 'local-file',
|
|
505
|
+
path: 'x.ts',
|
|
506
|
+
sha256: SHA256,
|
|
507
|
+
mime: 'text/plain',
|
|
508
|
+
size: 10,
|
|
509
|
+
},
|
|
510
|
+
createdAt: 'not-a-date',
|
|
511
|
+
refCount: 0,
|
|
512
|
+
}),
|
|
513
|
+
).toThrow();
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// ─── attachmentRefSchema ──────────────────────────────────────────────────────
|
|
518
|
+
|
|
519
|
+
describe('attachmentRefSchema', () => {
|
|
520
|
+
it('round-trips a minimal ref row (task owner)', () => {
|
|
521
|
+
const input = {
|
|
522
|
+
attachmentId: 'att_abc123',
|
|
523
|
+
ownerType: 'task' as const,
|
|
524
|
+
ownerId: 'T766',
|
|
525
|
+
attachedAt: '2026-04-16T08:30:00.000Z',
|
|
526
|
+
};
|
|
527
|
+
const result = attachmentRefSchema.parse(input);
|
|
528
|
+
expect(result.attachmentId).toBe('att_abc123');
|
|
529
|
+
expect(result.ownerType).toBe('task');
|
|
530
|
+
expect(result.ownerId).toBe('T766');
|
|
531
|
+
expect(result.attachedBy).toBeUndefined();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('round-trips a ref row with all owner types and attachedBy', () => {
|
|
535
|
+
const ownerTypes = [
|
|
536
|
+
'task',
|
|
537
|
+
'observation',
|
|
538
|
+
'session',
|
|
539
|
+
'decision',
|
|
540
|
+
'learning',
|
|
541
|
+
'pattern',
|
|
542
|
+
] as const;
|
|
543
|
+
|
|
544
|
+
for (const ownerType of ownerTypes) {
|
|
545
|
+
const result = attachmentRefSchema.parse({
|
|
546
|
+
attachmentId: 'att_xyz',
|
|
547
|
+
ownerType,
|
|
548
|
+
ownerId: 'owner-001',
|
|
549
|
+
attachedAt: '2026-04-16T08:30:00.000Z',
|
|
550
|
+
attachedBy: 'cleo-prime',
|
|
551
|
+
});
|
|
552
|
+
expect(result.ownerType).toBe(ownerType);
|
|
553
|
+
expect(result.attachedBy).toBe('cleo-prime');
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('type-checks inferred result is assignable to AttachmentRef', () => {
|
|
558
|
+
const raw = {
|
|
559
|
+
attachmentId: 'att_type',
|
|
560
|
+
ownerType: 'observation' as const,
|
|
561
|
+
ownerId: 'O-abc123',
|
|
562
|
+
attachedAt: '2026-04-16T00:00:00.000Z',
|
|
563
|
+
};
|
|
564
|
+
const parsed = attachmentRefSchema.parse(raw);
|
|
565
|
+
const ref: AttachmentRef = parsed;
|
|
566
|
+
expect(ref.ownerId).toBe('O-abc123');
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('rejects an invalid ownerType', () => {
|
|
570
|
+
expect(() =>
|
|
571
|
+
attachmentRefSchema.parse({
|
|
572
|
+
attachmentId: 'att_xyz',
|
|
573
|
+
ownerType: 'message',
|
|
574
|
+
ownerId: 'msg_001',
|
|
575
|
+
attachedAt: '2026-04-16T08:30:00.000Z',
|
|
576
|
+
}),
|
|
577
|
+
).toThrow();
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('rejects an empty attachmentId', () => {
|
|
581
|
+
expect(() =>
|
|
582
|
+
attachmentRefSchema.parse({
|
|
583
|
+
attachmentId: '',
|
|
584
|
+
ownerType: 'task',
|
|
585
|
+
ownerId: 'T001',
|
|
586
|
+
attachedAt: '2026-04-16T08:30:00.000Z',
|
|
587
|
+
}),
|
|
588
|
+
).toThrow();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('rejects an invalid attachedAt datetime', () => {
|
|
592
|
+
expect(() =>
|
|
593
|
+
attachmentRefSchema.parse({
|
|
594
|
+
attachmentId: 'att_ok',
|
|
595
|
+
ownerType: 'task',
|
|
596
|
+
ownerId: 'T001',
|
|
597
|
+
attachedAt: 'yesterday',
|
|
598
|
+
}),
|
|
599
|
+
).toThrow();
|
|
600
|
+
});
|
|
601
|
+
});
|