attachment-list-get-parity-integration.test.mjs
124 lines 4.3 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 10 days ago
1 /**
2 * Tier 2 — INTEGRATION: CLI = MCP = Hub handler parity (deep-equality gate).
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 handleAttachmentListRequest,
13 handleAttachmentGetRequest,
14 serializeAttachmentPayload,
15 } from '../lib/attachments/attachment-handlers.mjs';
16 import { buildAttachmentFixtureVault } from './fixtures/attachment-fixture.mjs';
17 import { deriveAttachmentId, normalizeUrlForId } from '../lib/attachments/attachment-store.mjs';
18 import { getRepoRoot } from '../lib/repo-root.mjs';
19
20 const __dirname = path.dirname(fileURLToPath(import.meta.url));
21 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-attachment-parity');
22
23 function hubList(input) {
24 return handleAttachmentListRequest({ ...input, role: 'admin' });
25 }
26
27 function cliList(input) {
28 return handleAttachmentListRequest({
29 ...input,
30 cliScopes: ['personal', 'project', 'org'],
31 });
32 }
33
34 function mcpList(input) {
35 return handleAttachmentListRequest({
36 ...input,
37 cliScopes: ['personal', 'project', 'org'],
38 });
39 }
40
41 describe('Attachment list/get — triple-surface parity', () => {
42 let fx;
43
44 beforeEach(() => {
45 fs.rmSync(tmpRoot, { recursive: true, force: true });
46 fx = buildAttachmentFixtureVault(tmpRoot);
47 });
48
49 afterEach(() => {
50 fs.rmSync(tmpRoot, { recursive: true, force: true });
51 });
52
53 it('list: Hub, CLI, and MCP payloads are deep-equal', () => {
54 const base = { dataDir: fx.dataDir, vaultPath: fx.vaultPath, vaultId: fx.vaultId };
55 const hub = hubList(base);
56 const cli = cliList(base);
57 const mcp = mcpList(base);
58 assert.equal(hub.ok, true);
59 assert.equal(cli.ok, true);
60 assert.equal(mcp.ok, true);
61 assert.deepEqual(hub.payload, cli.payload);
62 assert.deepEqual(cli.payload, mcp.payload);
63 assert.equal(serializeAttachmentPayload(hub.payload), serializeAttachmentPayload(mcp.payload));
64 });
65
66 it('get: Hub, CLI, and MCP payloads are deep-equal', () => {
67 const base = {
68 dataDir: fx.dataDir,
69 vaultPath: fx.vaultPath,
70 vaultId: fx.vaultId,
71 attachmentId: fx.fileId,
72 };
73 const hub = handleAttachmentGetRequest({ ...base, role: 'admin' });
74 const cli = handleAttachmentGetRequest({ ...base, cliScopes: ['personal', 'project', 'org'] });
75 assert.equal(hub.ok, true);
76 assert.deepEqual(hub.payload, cli.payload);
77 });
78
79 it('scope filter is identical across surfaces for list and get', () => {
80 const listPersonal = hubList({
81 dataDir: fx.dataDir,
82 vaultPath: fx.vaultPath,
83 vaultId: fx.vaultId,
84 scope: 'personal',
85 });
86 assert.equal(listPersonal.ok, true);
87 assert.ok(listPersonal.payload.attachments.every((a) => a.scope === 'personal'));
88
89 const getProjectDenied = handleAttachmentGetRequest({
90 dataDir: fx.dataDir,
91 vaultPath: fx.vaultPath,
92 vaultId: fx.vaultId,
93 attachmentId: deriveProjectUrlId(),
94 visibleScopes: new Set(['personal']),
95 });
96 assert.equal(getProjectDenied.ok, false);
97 assert.equal(getProjectDenied.code, 'unknown_attachment');
98 });
99
100 it('derivation is idempotent across repeated listAttachments', () => {
101 const base = { dataDir: fx.dataDir, vaultPath: fx.vaultPath, vaultId: fx.vaultId };
102 const first = hubList(base);
103 const second = hubList(base);
104 assert.deepEqual(first.payload.attachments, second.payload.attachments);
105 });
106 });
107
108 function deriveProjectUrlId() {
109 return deriveAttachmentId(
110 'url',
111 `url:projects/demo/project-photo.md|${normalizeUrlForId('https://project.example.org/img.png')}`,
112 );
113 }
114
115 describe('Attachment routes — hub wiring contract', () => {
116 it('registers GET /api/v1/attachments list and get with auth middleware', () => {
117 const src = fs.readFileSync(path.join(getRepoRoot(), 'hub/server.mjs'), 'utf8');
118 assert.match(src, /app\.use\('\/api\/v1\/attachments', jwtAuth, apiLimiter, requireVaultAccess\)/);
119 assert.match(src, /app\.get\('\/api\/v1\/attachments', requireRole\('viewer'/);
120 assert.match(src, /app\.get\('\/api\/v1\/attachments\/:id', requireRole\('viewer'/);
121 assert.match(src, /handleAttachmentListRequest/);
122 assert.match(src, /handleAttachmentGetRequest/);
123 });
124 });
File History 1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago