flow-capture-parity-integration.test.mjs
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