flow-list-get-parity-integration.test.mjs
145 lines 4.9 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 13 hours ago
1 /**
2 * Tier 2 — INTEGRATION: CLI = MCP = Hub handler parity (deep-equality gate).
3 *
4 * @see lib/flow/flow-handlers.mjs
5 * @see docs/FLOW-STORE-CONTRACT-7A-10.md §7–§8
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 handleFlowListRequest,
14 handleFlowGetRequest,
15 serializeFlowPayload,
16 } from '../lib/flow/flow-handlers.mjs';
17 import { seedStarterFlows } from '../lib/flow/flow-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-flow-parity-integration');
22 const starterDir = path.join(getRepoRoot(), 'flows/starter');
23
24 function hubList(input) {
25 return handleFlowListRequest({ ...input, role: 'admin' });
26 }
27
28 function cliList(input) {
29 return handleFlowListRequest({
30 ...input,
31 cliScopes: ['personal', 'project'],
32 });
33 }
34
35 function mcpList(input) {
36 return handleFlowListRequest({
37 ...input,
38 cliScopes: ['personal', 'project'],
39 });
40 }
41
42 function hubGet(input) {
43 return handleFlowGetRequest({ ...input, role: 'admin' });
44 }
45
46 function cliGet(input) {
47 return handleFlowGetRequest({
48 ...input,
49 cliScopes: ['personal', 'project'],
50 });
51 }
52
53 function mcpGet(input) {
54 return handleFlowGetRequest({
55 ...input,
56 cliScopes: ['personal', 'project'],
57 });
58 }
59
60 describe('Flow 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 seedStarterFlows(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, scope: 'personal' };
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(serializeFlowPayload(hub.payload), serializeFlowPayload(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, flowId: 'flow_overseer_handover' };
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' });
99 assert.equal(listPersonal.ok, true);
100 assert.ok(listPersonal.payload.flows.every((f) => f.scope === 'personal'));
101 assert.equal(listPersonal.payload.flows.length, 4);
102
103 const getProjectDenied = handleFlowGetRequest({
104 dataDir,
105 vaultId,
106 flowId: 'flow_overseer_handover',
107 visibleScopes: new Set(['personal']),
108 });
109 assert.equal(getProjectDenied.ok, false);
110 assert.equal(getProjectDenied.code, 'unknown_flow');
111 });
112
113 it('getFlow step ids match flow.steps order and step_count equals steps.length', () => {
114 const got = hubGet({ dataDir, vaultId, flowId: 'flow_overseer_handover' });
115 assert.equal(got.ok, true);
116 const list = hubList({ dataDir, vaultId });
117 assert.equal(list.ok, true);
118 const summary = list.payload.flows.find((f) => f.flow_id === 'flow_overseer_handover');
119 assert.ok(summary);
120 assert.equal(summary.step_count, got.payload.steps.length);
121 assert.deepEqual(
122 got.payload.flow.steps,
123 got.payload.steps.map((s) => s.step_id),
124 );
125 });
126
127 it('seeding is idempotent across repeated listFlows calls', () => {
128 const first = hubList({ dataDir, vaultId });
129 const second = hubList({ dataDir, vaultId });
130 assert.deepEqual(first.payload.flows, second.payload.flows);
131 const ids = second.payload.flows.map((f) => `${f.flow_id}@${f.version}`);
132 assert.equal(new Set(ids).size, ids.length);
133 });
134 });
135
136 describe('Flow routes — hub wiring contract', () => {
137 it('registers GET /api/v1/flows list and get with auth middleware', () => {
138 const src = fs.readFileSync(path.join(getRepoRoot(), 'hub/server.mjs'), 'utf8');
139 assert.match(src, /app\.use\('\/api\/v1\/flows', jwtAuth, apiLimiter, requireVaultAccess\)/);
140 assert.match(src, /app\.get\('\/api\/v1\/flows', requireRole\('viewer'/);
141 assert.match(src, /app\.get\('\/api\/v1\/flows\/:id', requireRole\('viewer'/);
142 assert.match(src, /handleFlowListRequest/);
143 assert.match(src, /handleFlowGetRequest/);
144 });
145 });
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 13 hours ago