task-list-get-parity-integration.test.mjs
133 lines 4.4 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 lib/task/task-handlers.mjs
5 * @see docs/TASK-STORE-CONTRACT-2G.md §9
6 */
7 import { describe, it, beforeEach, afterEach } from 'node:test';
8 import assert from 'node:assert/strict';
9 import fs from 'node:fs';
10 import path from 'node:path';
11 import { fileURLToPath } from 'node:url';
12 import {
13 handleTaskListRequest,
14 handleTaskGetRequest,
15 serializeTaskPayload,
16 } from '../lib/task/task-handlers.mjs';
17 import { seedStarterTasks } from '../lib/task/task-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-task-parity-integration');
22 const starterDir = path.join(getRepoRoot(), 'tasks/starter');
23
24 function hubList(input) {
25 return handleTaskListRequest({ ...input, role: 'admin' });
26 }
27
28 function cliList(input) {
29 return handleTaskListRequest({
30 ...input,
31 cliScopes: ['personal', 'project', 'org'],
32 });
33 }
34
35 function mcpList(input) {
36 return handleTaskListRequest({
37 ...input,
38 cliScopes: ['personal', 'project', 'org'],
39 });
40 }
41
42 function hubGet(input) {
43 return handleTaskGetRequest({ ...input, role: 'admin' });
44 }
45
46 function cliGet(input) {
47 return handleTaskGetRequest({
48 ...input,
49 cliScopes: ['personal', 'project', 'org'],
50 });
51 }
52
53 function mcpGet(input) {
54 return handleTaskGetRequest({
55 ...input,
56 cliScopes: ['personal', 'project', 'org'],
57 });
58 }
59
60 describe('Task list/get — triple-surface parity', () => {
61 const dataDir = path.join(tmpRoot, 'data');
62 const vaultId = 'default';
63
64 beforeEach(() => {
65 fs.rmSync(tmpRoot, { recursive: true, force: true });
66 fs.mkdirSync(dataDir, { recursive: true });
67 seedStarterTasks(dataDir, vaultId, { starterDir });
68 });
69
70 afterEach(() => {
71 fs.rmSync(tmpRoot, { recursive: true, force: true });
72 });
73
74 it('list: Hub, CLI, and MCP payloads are deep-equal for the same authorized request', () => {
75 const base = { dataDir, vaultId, starterDir };
76 const hub = hubList(base);
77 const cli = cliList(base);
78 const mcp = mcpList(base);
79 assert.equal(hub.ok, true);
80 assert.equal(cli.ok, true);
81 assert.equal(mcp.ok, true);
82 assert.deepEqual(hub.payload, cli.payload);
83 assert.deepEqual(cli.payload, mcp.payload);
84 assert.equal(serializeTaskPayload(hub.payload), serializeTaskPayload(mcp.payload));
85 });
86
87 it('get: Hub, CLI, and MCP payloads are deep-equal for the same authorized request', () => {
88 const base = { dataDir, vaultId, taskId: 'task_2g_handover_001', starterDir };
89 const hub = hubGet(base);
90 const cli = cliGet(base);
91 const mcp = mcpGet(base);
92 assert.equal(hub.ok, true);
93 assert.deepEqual(hub.payload, cli.payload);
94 assert.deepEqual(cli.payload, mcp.payload);
95 });
96
97 it('scope filter is identical across surfaces for list and get', () => {
98 const listPersonal = hubList({ dataDir, vaultId, scope: 'personal', starterDir });
99 assert.equal(listPersonal.ok, true);
100 assert.ok(listPersonal.payload.tasks.every((t) => t.scope === 'personal'));
101 assert.equal(listPersonal.payload.tasks.length, 1);
102 assert.equal(listPersonal.payload.tasks[0].task_id, 'task_personal_practice');
103
104 const getOrgDenied = handleTaskGetRequest({
105 dataDir,
106 vaultId,
107 taskId: 'task_org_compliance_q2',
108 visibleScopes: new Set(['personal']),
109 starterDir,
110 });
111 assert.equal(getOrgDenied.ok, false);
112 assert.equal(getOrgDenied.code, 'unknown_task');
113 });
114
115 it('seeding is idempotent across repeated listTasks calls', () => {
116 const first = hubList({ dataDir, vaultId, starterDir });
117 const second = hubList({ dataDir, vaultId, starterDir });
118 assert.deepEqual(first.payload.tasks, second.payload.tasks);
119 const ids = second.payload.tasks.map((t) => t.task_id);
120 assert.equal(new Set(ids).size, ids.length);
121 });
122 });
123
124 describe('Task routes — hub wiring contract', () => {
125 it('registers GET /api/v1/tasks list and get with auth middleware', () => {
126 const src = fs.readFileSync(path.join(getRepoRoot(), 'hub/server.mjs'), 'utf8');
127 assert.match(src, /app\.use\('\/api\/v1\/tasks', jwtAuth, apiLimiter, requireVaultAccess\)/);
128 assert.match(src, /app\.get\('\/api\/v1\/tasks', requireRole\('viewer'/);
129 assert.match(src, /app\.get\('\/api\/v1\/tasks\/:id', requireRole\('viewer'/);
130 assert.match(src, /handleTaskListRequest/);
131 assert.match(src, /handleTaskGetRequest/);
132 });
133 });
File History 1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago