attachment-store-data-integrity.test.mjs
113 lines 3.7 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 10 days ago
1 /**
2 * Tier 5 — DATA INTEGRITY: deletion propagation and overlay orphan GC.
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 { handleAttachmentGetRequest, handleAttachmentListRequest } from '../lib/attachments/attachment-handlers.mjs';
12 import { attachmentForClient } from '../lib/attachments/attachment-store.mjs';
13 import { saveAttachmentStore } from '../lib/attachments/attachment-store-file.mjs';
14 import { buildAttachmentFixtureVault } from './fixtures/attachment-fixture.mjs';
15
16 const __dirname = path.dirname(fileURLToPath(import.meta.url));
17 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-attachment-integrity');
18
19 describe('Attachment store — data integrity', () => {
20 let fx;
21
22 beforeEach(() => {
23 fs.rmSync(tmpRoot, { recursive: true, force: true });
24 fx = buildAttachmentFixtureVault(tmpRoot);
25 });
26
27 afterEach(() => {
28 fs.rmSync(tmpRoot, { recursive: true, force: true });
29 });
30
31 it('I-A-DEL-01: deleting owning note removes mist/embedded attachments', () => {
32 const before = handleAttachmentListRequest({
33 dataDir: fx.dataDir,
34 vaultPath: fx.vaultPath,
35 vaultId: fx.vaultId,
36 role: 'admin',
37 });
38 assert.equal(before.ok, true);
39 assert.ok(before.payload.attachments.some((a) => a.attachment_id === fx.mistId));
40
41 fs.unlinkSync(path.join(fx.vaultPath, 'notes', 'mist-note.md'));
42
43 const afterList = handleAttachmentListRequest({
44 dataDir: fx.dataDir,
45 vaultPath: fx.vaultPath,
46 vaultId: fx.vaultId,
47 role: 'admin',
48 });
49 assert.ok(!afterList.payload.attachments.some((a) => a.attachment_id === fx.mistId));
50
51 const afterGet = handleAttachmentGetRequest({
52 dataDir: fx.dataDir,
53 vaultPath: fx.vaultPath,
54 vaultId: fx.vaultId,
55 attachmentId: fx.mistId,
56 role: 'admin',
57 });
58 assert.equal(afterGet.ok, false);
59 assert.equal(afterGet.code, 'unknown_attachment');
60 });
61
62 it('I-A-DEL-01: deleting media file removes vault_file attachment', () => {
63 fs.unlinkSync(path.join(fx.vaultPath, 'media', 'sample.png'));
64 const got = handleAttachmentGetRequest({
65 dataDir: fx.dataDir,
66 vaultPath: fx.vaultPath,
67 vaultId: fx.vaultId,
68 attachmentId: fx.fileId,
69 role: 'admin',
70 });
71 assert.equal(got.ok, false);
72 assert.equal(got.code, 'unknown_attachment');
73 });
74
75 it('I-A-ORPH-01: overlay entry for underivable id never resurrects attachment', () => {
76 saveAttachmentStore(fx.dataDir, {
77 vaults: {
78 [fx.vaultId]: {
79 policies: {
80 att_file_deadbeefdeadbeefdeadbeefdeadbe: {
81 agent_visible: true,
82 retention: 'vault_lifetime',
83 updated: '2026-07-02T00:00:00Z',
84 },
85 },
86 },
87 },
88 });
89 const list = handleAttachmentListRequest({
90 dataDir: fx.dataDir,
91 vaultPath: fx.vaultPath,
92 vaultId: fx.vaultId,
93 role: 'admin',
94 });
95 assert.ok(!list.payload.attachments.some((a) => a.attachment_id.startsWith('att_file_dead')));
96 });
97
98 it('attachmentForClient adds no path/url/byte leak fields', () => {
99 const got = handleAttachmentGetRequest({
100 dataDir: fx.dataDir,
101 vaultPath: fx.vaultPath,
102 vaultId: fx.vaultId,
103 attachmentId: fx.urlId,
104 role: 'admin',
105 });
106 assert.equal(got.ok, true);
107 const client = attachmentForClient(got.payload.attachment);
108 const json = JSON.stringify(client);
109 assert.ok(!json.includes('media/'));
110 assert.ok(!json.includes('https://'));
111 assert.equal('bytes' in client, false);
112 });
113 });
File History 1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago