flow-capture-parity-integration.test.mjs
135 lines 6.2 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 17 hours ago
1 /**
2 * Tier 2 — INTEGRATION: MCP / Hub / CLI parity + sub-gates off.
3 *
4 * @see lib/flow/flow-capture.mjs
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 handleFlowCaptureObserveRequest,
13 handleFlowCaptureListRequest,
14 handleFlowCaptureProposeRequest,
15 handleFlowCaptureDismissRequest,
16 } from '../lib/flow/flow-capture.mjs';
17 import { upsertCandidate } from '../lib/flow/flow-store.mjs';
18 import { createProposal, listProposals } from '../hub/proposals-store.mjs';
19 import { getRepoRoot } from '../lib/repo-root.mjs';
20 import { validSessionMeta, makeCandidateRecord } from './fixtures/flow/capture-helpers.mjs';
21
22 const __dirname = path.dirname(fileURLToPath(import.meta.url));
23 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-flow-capture-parity');
24
25 function stripVolatile(payload) {
26 const copy = structuredClone(payload);
27 if (Array.isArray(copy.candidates)) {
28 for (const c of copy.candidates) {
29 delete c.candidate_id;
30 if (c.provenance) {
31 delete c.provenance.actor;
32 delete c.provenance.harness;
33 }
34 }
35 }
36 delete copy.proposal_id;
37 return copy;
38 }
39
40 describe('Flow capture — triple-surface parity', () => {
41 beforeEach(() => {
42 fs.rmSync(tmpRoot, { recursive: true, force: true });
43 fs.mkdirSync(tmpRoot, { recursive: true });
44 process.env.FLOW_CAPTURE_DETECTION_ENABLED = '1';
45 process.env.FLOW_CAPTURE_WRITES_ENABLED = '1';
46 });
47 afterEach(() => {
48 fs.rmSync(tmpRoot, { recursive: true, force: true });
49 delete process.env.FLOW_CAPTURE_DETECTION_ENABLED;
50 delete process.env.FLOW_CAPTURE_WRITES_ENABLED;
51 });
52
53 it('observe/list/propose/dismiss produce deep-equal envelopes across surfaces', () => {
54 const hubDir = path.join(tmpRoot, 'hub');
55 const cliDir = path.join(tmpRoot, 'cli');
56 const mcpDir = path.join(tmpRoot, 'mcp');
57 for (const d of [hubDir, cliDir, mcpDir]) fs.mkdirSync(d, { recursive: true });
58
59 const meta = validSessionMeta();
60 const hubObs = handleFlowCaptureObserveRequest({ dataDir: hubDir, vaultId: 'default', sessionMeta: meta, harness: 'hub' });
61 const cliObs = handleFlowCaptureObserveRequest({ dataDir: cliDir, vaultId: 'default', cliScopes: ['personal'], sessionMeta: meta, harness: 'cli' });
62 const mcpObs = handleFlowCaptureObserveRequest({ dataDir: mcpDir, vaultId: 'default', cliScopes: ['personal'], sessionMeta: meta, harness: 'mcp' });
63 assert.equal(hubObs.ok, true);
64 assert.equal(cliObs.ok, true);
65 assert.equal(mcpObs.ok, true);
66 assert.deepEqual(stripVolatile(hubObs.payload), stripVolatile(cliObs.payload));
67 assert.deepEqual(stripVolatile(cliObs.payload), stripVolatile(mcpObs.payload));
68
69 const cand = makeCandidateRecord({ candidate_id: 'cand_parity1234' });
70 for (const d of [hubDir, cliDir, mcpDir]) upsertCandidate(d, 'default', cand);
71
72 const hubList = handleFlowCaptureListRequest({ dataDir: hubDir, vaultId: 'default' });
73 const cliList = handleFlowCaptureListRequest({ dataDir: cliDir, vaultId: 'default', cliScopes: ['personal'] });
74 const hubRow = hubList.payload.candidates.find((c) => c.candidate_id === 'cand_parity1234');
75 const cliRow = cliList.payload.candidates.find((c) => c.candidate_id === 'cand_parity1234');
76 assert.deepEqual(hubRow, cliRow);
77
78 const hubProp = handleFlowCaptureProposeRequest({
79 dataDir: hubDir, vaultId: 'default', candidateId: 'cand_parity1234', confirmedScope: 'personal', intent: 'x', createProposal,
80 });
81 const cliProp = handleFlowCaptureProposeRequest({
82 dataDir: cliDir, vaultId: 'default', cliScopes: ['personal'], candidateId: 'cand_parity1234', confirmedScope: 'personal', intent: 'x', createProposal,
83 });
84 assert.deepEqual(stripVolatile(hubProp.payload), stripVolatile(cliProp.payload));
85
86 const dismissHubDir = path.join(tmpRoot, 'dismiss-hub');
87 const dismissCliDir = path.join(tmpRoot, 'dismiss-cli');
88 for (const d of [dismissHubDir, dismissCliDir]) {
89 fs.mkdirSync(d, { recursive: true });
90 upsertCandidate(d, 'default', makeCandidateRecord({ candidate_id: 'cand_dismiss12' }));
91 }
92 const hubDis = handleFlowCaptureDismissRequest({
93 dataDir: dismissHubDir, vaultId: 'default', candidateId: 'cand_dismiss12', intent: 'no', createProposal,
94 });
95 const cliDis = handleFlowCaptureDismissRequest({
96 dataDir: dismissCliDir, vaultId: 'default', cliScopes: ['personal'], candidateId: 'cand_dismiss12', intent: 'no', createProposal,
97 });
98 assert.equal(hubDis.ok, true);
99 assert.equal(cliDis.ok, true);
100 assert.deepEqual(stripVolatile(hubDis.payload), stripVolatile(cliDis.payload));
101 });
102
103 it('sub-gates off ⇒ all surfaces return disabled/refusal', () => {
104 delete process.env.FLOW_CAPTURE_DETECTION_ENABLED;
105 delete process.env.FLOW_CAPTURE_WRITES_ENABLED;
106 const dir = path.join(tmpRoot, 'off');
107 fs.mkdirSync(dir, { recursive: true });
108
109 const obs = handleFlowCaptureObserveRequest({ dataDir: dir, vaultId: 'default', sessionMeta: validSessionMeta() });
110 assert.equal(obs.payload.detection_authorized, false);
111
112 const prop = handleFlowCaptureProposeRequest({
113 dataDir: dir, vaultId: 'default', candidateId: 'cand_x1234', confirmedScope: 'personal', intent: 'x', createProposal,
114 });
115 assert.equal(prop.code, 'FLOW_CAPTURE_WRITES_DISABLED');
116
117 const dis = handleFlowCaptureDismissRequest({
118 dataDir: dir, vaultId: 'default', candidateId: 'cand_x1234', intent: 'x', createProposal,
119 });
120 assert.equal(dis.code, 'FLOW_CAPTURE_WRITES_DISABLED');
121 assert.equal(listProposals(dir, { source: 'flow_capture' }).total, 0);
122 });
123 });
124
125 describe('Flow capture — Hub route wiring contract', () => {
126 it('registers capture routes and approve reconcile', () => {
127 const src = fs.readFileSync(path.join(getRepoRoot(), 'hub/server.mjs'), 'utf8');
128 assert.match(src, /\/api\/v1\/flows\/capture\/observe/);
129 assert.match(src, /\/api\/v1\/flows\/candidates/);
130 assert.match(src, /\/api\/v1\/flows\/candidates\/:candidate_id\/propose/);
131 assert.match(src, /\/api\/v1\/flows\/candidates\/:candidate_id\/dismiss/);
132 assert.match(src, /precheckApprovedCaptureProposal/);
133 assert.match(src, /applyCaptureProposal/);
134 });
135 });
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 17 hours ago