attachment-store-unit.test.mjs
122 lines 4.5 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 10 days ago
1 /**
2 * Tier 1 — UNIT: Attachment store helpers, id derivation, and projections.
3 *
4 * @see docs/ATTACHMENT-STORE-CONTRACT-2F-b.md §9
5 */
6 import { describe, it, beforeEach, afterEach } from 'node:test';
7 import assert from 'node:assert/strict';
8 import fs from 'node:fs';
9 import path from 'node:path';
10 import { fileURLToPath } from 'node:url';
11 import {
12 ATTACHMENT_ID_RE,
13 NOTE_REF_RE,
14 deriveAttachmentId,
15 normalizeUrlForId,
16 noteRefFromPath,
17 attachmentSummaryForClient,
18 attachmentForClient,
19 urlHostOnly,
20 MAX_LINKED_NOTES,
21 } from '../lib/attachments/attachment-store.mjs';
22 import { getVaultAttachmentPolicies } from '../lib/attachments/attachment-store-file.mjs';
23 import { buildAttachmentFixtureVault, FIXTURE_MIST_ID } from './fixtures/attachment-fixture.mjs';
24 import { listAttachments, getAttachment } from '../lib/attachments/attachment-store.mjs';
25
26 const __dirname = path.dirname(fileURLToPath(import.meta.url));
27 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-attachment-unit');
28
29 describe('Attachment store — id regexes', () => {
30 it('ATTACHMENT_ID_RE accepts canonical ids and rejects malformed', () => {
31 const id = deriveAttachmentId('file', 'file:media/x.png');
32 assert.ok(ATTACHMENT_ID_RE.test(id));
33 assert.ok(!ATTACHMENT_ID_RE.test('att_file_SHORT'));
34 assert.ok(ATTACHMENT_ID_RE.test('att_link_' + 'a'.repeat(32)));
35 assert.ok(!ATTACHMENT_ID_RE.test('att_LINK_' + 'a'.repeat(32)));
36 assert.ok(!ATTACHMENT_ID_RE.test('media_file_' + 'a'.repeat(32)));
37 });
38
39 it('NOTE_REF_RE accepts canonical refs and rejects traversal', () => {
40 assert.ok(NOTE_REF_RE.test('note:inbox/foo.md'));
41 assert.equal(noteRefFromPath('../escape.md'), '');
42 assert.equal(noteRefFromPath('/absolute.md'), 'note:absolute.md');
43 });
44
45 it('id derivation is deterministic and source-distinct', () => {
46 const a = deriveAttachmentId('file', 'file:media/a.png');
47 const b = deriveAttachmentId('file', 'file:media/b.png');
48 const c = deriveAttachmentId('mist', `mist:${FIXTURE_MIST_ID}`);
49 assert.equal(a, deriveAttachmentId('file', 'file:media/a.png'));
50 assert.notEqual(a, b);
51 assert.notEqual(a, c);
52 });
53 });
54
55 describe('Attachment store — client projections', () => {
56 const sample = {
57 schema: 'knowtation.attachment/v0',
58 attachment_id: deriveAttachmentId('file', 'file:media/x.png'),
59 source: 'vault_file',
60 storage_kind: 'vault_blob',
61 mime_class: 'image',
62 mime_type: 'image/png',
63 scope: 'personal',
64 display_label: 'x',
65 byte_size: 100,
66 linked_note_refs: ['note:inbox/x.md'],
67 agent_visible: false,
68 created: '2026-07-02T00:00:00Z',
69 updated: '2026-07-02T00:00:00Z',
70 truncated: false,
71 };
72
73 it('attachmentSummaryForClient drops sensitive list fields', () => {
74 const summary = attachmentSummaryForClient(sample);
75 assert.equal('byte_size' in summary, false);
76 assert.equal('linked_note_refs' in summary, false);
77 assert.equal('agent_visible' in summary, false);
78 assert.equal('mime_type' in summary, false);
79 assert.equal(summary.attachment_id, sample.attachment_id);
80 });
81
82 it('attachmentForClient caps linked_note_refs and sets truncated', () => {
83 const refs = Array.from({ length: MAX_LINKED_NOTES + 3 }, (_, i) => `note:ref${i}.md`);
84 const client = attachmentForClient({ ...sample, linked_note_refs: refs });
85 assert.equal(client.linked_note_refs.length, MAX_LINKED_NOTES);
86 assert.equal(client.truncated, true);
87 });
88
89 it('display_label for embedded_url uses host only', () => {
90 assert.equal(urlHostOnly('https://user:[email protected]/x.png?sig=abc'), 'cdn.example.com');
91 });
92 });
93
94 describe('Attachment store — overlay defaults', () => {
95 const dataDir = path.join(tmpRoot, 'overlay');
96
97 beforeEach(() => {
98 fs.rmSync(tmpRoot, { recursive: true, force: true });
99 fs.mkdirSync(dataDir, { recursive: true });
100 });
101
102 afterEach(() => {
103 fs.rmSync(tmpRoot, { recursive: true, force: true });
104 });
105
106 it('missing overlay ⇒ agent_visible:false default', () => {
107 const fx = buildAttachmentFixtureVault(tmpRoot);
108 const policies = getVaultAttachmentPolicies(fx.dataDir, fx.vaultId);
109 assert.deepEqual(policies, {});
110
111 const list = listAttachments(fx.dataDir, fx.vaultPath, fx.vaultId, {
112 filterScopes: new Set(['personal', 'project', 'org']),
113 effectiveScope: 'org',
114 });
115 const got = getAttachment(fx.dataDir, fx.vaultPath, fx.vaultId, fx.mistId, {
116 visibleScopes: new Set(['personal', 'project', 'org']),
117 });
118 assert.ok(got);
119 assert.equal(got.agent_visible, false);
120 assert.ok(list.attachments.length >= 1);
121 });
122 });
File History 1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago