flow-capture-unit.test.mjs
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d
docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge
Human
10 hours ago
| 1 | /** |
| 2 | * Tier 1 — UNIT: capture thresholds, validation, confidence, gating envelopes. |
| 3 | * |
| 4 | * @see lib/flow/flow-capture.mjs |
| 5 | * @see docs/FLOW-CAPTURE-FLYWHEEL-CONTRACT-7A-L4.md §4, §10 |
| 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 | FLOW_CAPTURE_MIN_REPETITIONS, |
| 14 | FLOW_CAPTURE_MIN_CONFIDENCE, |
| 15 | FLOW_CAPTURE_PER_SESSION_CAP, |
| 16 | FLOW_CAPTURE_DEDUP_OVERLAP, |
| 17 | MAX_SESSION_SIGNAL_REFS, |
| 18 | MAX_CANDIDATE_SUMMARIES, |
| 19 | MAX_DRAFT_STEPS, |
| 20 | FLOW_CANDIDATE_SCHEMA, |
| 21 | FLOW_CAPTURE_PROPOSAL_SCHEMA, |
| 22 | validateSessionMeta, |
| 23 | deriveConfidence, |
| 24 | validateCandidate, |
| 25 | runDetectors, |
| 26 | getFlowCaptureDetectionEnabled, |
| 27 | getFlowCaptureWritesEnabled, |
| 28 | handleFlowCaptureObserveRequest, |
| 29 | handleFlowCaptureProposeRequest, |
| 30 | handleFlowCaptureDismissRequest, |
| 31 | } from '../lib/flow/flow-capture.mjs'; |
| 32 | import { upsertCandidate } from '../lib/flow/flow-store.mjs'; |
| 33 | import { createProposal } from '../hub/proposals-store.mjs'; |
| 34 | import { validSessionMeta, payloadBearingSessionMeta, makeCandidateRecord } from './fixtures/flow/capture-helpers.mjs'; |
| 35 | |
| 36 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 37 | const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-flow-capture-unit'); |
| 38 | |
| 39 | describe('pinned threshold constants', () => { |
| 40 | it('match contract §4.3', () => { |
| 41 | assert.equal(FLOW_CAPTURE_MIN_REPETITIONS, 3); |
| 42 | assert.equal(FLOW_CAPTURE_MIN_CONFIDENCE, 'medium'); |
| 43 | assert.equal(FLOW_CAPTURE_PER_SESSION_CAP, 2); |
| 44 | assert.equal(FLOW_CAPTURE_DEDUP_OVERLAP, 0.8); |
| 45 | assert.equal(MAX_SESSION_SIGNAL_REFS, 64); |
| 46 | assert.equal(MAX_CANDIDATE_SUMMARIES, 50); |
| 47 | assert.equal(MAX_DRAFT_STEPS, 32); |
| 48 | }); |
| 49 | }); |
| 50 | |
| 51 | describe('validateSessionMeta — rejects raw content', () => { |
| 52 | it('accepts valid structural meta', () => { |
| 53 | const r = validateSessionMeta(validSessionMeta()); |
| 54 | assert.equal(r.ok, true); |
| 55 | }); |
| 56 | |
| 57 | it('rejects payload-bearing forbidden keys', () => { |
| 58 | const r = validateSessionMeta(payloadBearingSessionMeta()); |
| 59 | assert.equal(r.ok, false); |
| 60 | }); |
| 61 | |
| 62 | it('rejects unbounded step_sequence_refs', () => { |
| 63 | const refs = Array.from({ length: MAX_SESSION_SIGNAL_REFS + 1 }, (_, i) => `flow_x#${i + 1}`); |
| 64 | const r = validateSessionMeta(validSessionMeta({ step_sequence_refs: refs })); |
| 65 | assert.equal(r.ok, false); |
| 66 | }); |
| 67 | }); |
| 68 | |
| 69 | describe('deriveConfidence — bounded enum', () => { |
| 70 | it('low at threshold edge with single signal', () => { |
| 71 | assert.equal(deriveConfidence('repetition', 2), 'low'); |
| 72 | }); |
| 73 | |
| 74 | it('medium at threshold', () => { |
| 75 | assert.equal(deriveConfidence('repetition', FLOW_CAPTURE_MIN_REPETITIONS), 'medium'); |
| 76 | }); |
| 77 | |
| 78 | it('high at 2× threshold or multi-signal', () => { |
| 79 | assert.equal(deriveConfidence('repetition', FLOW_CAPTURE_MIN_REPETITIONS * 2), 'high'); |
| 80 | assert.equal(deriveConfidence('repetition', 3, 2), 'high'); |
| 81 | }); |
| 82 | }); |
| 83 | |
| 84 | describe('validateCandidate — stamps knowtation.flow_candidate/v0', () => { |
| 85 | it('accepts canonical candidate', () => { |
| 86 | const r = validateCandidate(makeCandidateRecord()); |
| 87 | assert.equal(r.ok, true); |
| 88 | assert.equal(r.candidate.schema, FLOW_CANDIDATE_SCHEMA); |
| 89 | }); |
| 90 | |
| 91 | it('rejects malformed candidate_id', () => { |
| 92 | const r = validateCandidate(makeCandidateRecord({ candidate_id: 'bad' })); |
| 93 | assert.equal(r.ok, false); |
| 94 | }); |
| 95 | }); |
| 96 | |
| 97 | describe('runDetectors — server-side only', () => { |
| 98 | it('emits repetition when count meets threshold', () => { |
| 99 | const hits = runDetectors(validSessionMeta(), { session_extraction_opt_in: false }); |
| 100 | assert.ok(hits.some((h) => h.signal === 'repetition')); |
| 101 | }); |
| 102 | }); |
| 103 | |
| 104 | describe('gating — sub-gates default OFF', () => { |
| 105 | const dataDir = path.join(tmpRoot, 'gate'); |
| 106 | |
| 107 | beforeEach(() => { |
| 108 | fs.rmSync(tmpRoot, { recursive: true, force: true }); |
| 109 | fs.mkdirSync(dataDir, { recursive: true }); |
| 110 | delete process.env.FLOW_CAPTURE_DETECTION_ENABLED; |
| 111 | delete process.env.FLOW_CAPTURE_WRITES_ENABLED; |
| 112 | }); |
| 113 | afterEach(() => { |
| 114 | fs.rmSync(tmpRoot, { recursive: true, force: true }); |
| 115 | delete process.env.FLOW_CAPTURE_DETECTION_ENABLED; |
| 116 | delete process.env.FLOW_CAPTURE_WRITES_ENABLED; |
| 117 | }); |
| 118 | |
| 119 | it('detection defaults off', () => { |
| 120 | assert.equal(getFlowCaptureDetectionEnabled(dataDir), false); |
| 121 | assert.equal(getFlowCaptureWritesEnabled(dataDir), false); |
| 122 | }); |
| 123 | |
| 124 | it('observe off ⇒ detection_authorized false, no candidates', () => { |
| 125 | const r = handleFlowCaptureObserveRequest({ |
| 126 | dataDir, |
| 127 | vaultId: 'default', |
| 128 | sessionMeta: validSessionMeta(), |
| 129 | }); |
| 130 | assert.equal(r.ok, true); |
| 131 | assert.equal(r.payload.detection_authorized, false); |
| 132 | assert.equal(r.payload.returned_count, 0); |
| 133 | }); |
| 134 | |
| 135 | it('propose off ⇒ FLOW_CAPTURE_WRITES_DISABLED', () => { |
| 136 | const r = handleFlowCaptureProposeRequest({ |
| 137 | dataDir, |
| 138 | vaultId: 'default', |
| 139 | candidateId: 'cand_a1b2c3d4', |
| 140 | confirmedScope: 'personal', |
| 141 | intent: 'promote', |
| 142 | createProposal, |
| 143 | }); |
| 144 | assert.equal(r.ok, false); |
| 145 | assert.equal(r.code, 'FLOW_CAPTURE_WRITES_DISABLED'); |
| 146 | }); |
| 147 | |
| 148 | it('dismiss off ⇒ FLOW_CAPTURE_WRITES_DISABLED', () => { |
| 149 | const r = handleFlowCaptureDismissRequest({ |
| 150 | dataDir, |
| 151 | vaultId: 'default', |
| 152 | candidateId: 'cand_a1b2c3d4', |
| 153 | intent: 'dismiss', |
| 154 | createProposal, |
| 155 | }); |
| 156 | assert.equal(r.ok, false); |
| 157 | assert.equal(r.code, 'FLOW_CAPTURE_WRITES_DISABLED'); |
| 158 | }); |
| 159 | }); |
| 160 | |
| 161 | describe('proposal envelopes when writes forced on', () => { |
| 162 | const dataDir = path.join(tmpRoot, 'writes'); |
| 163 | const visible = new Set(['personal', 'project', 'org']); |
| 164 | |
| 165 | beforeEach(() => { |
| 166 | fs.rmSync(tmpRoot, { recursive: true, force: true }); |
| 167 | fs.mkdirSync(dataDir, { recursive: true }); |
| 168 | process.env.FLOW_CAPTURE_WRITES_ENABLED = '1'; |
| 169 | process.env.FLOW_CAPTURE_DETECTION_ENABLED = '1'; |
| 170 | upsertCandidate(dataDir, 'default', makeCandidateRecord()); |
| 171 | }); |
| 172 | afterEach(() => { |
| 173 | fs.rmSync(tmpRoot, { recursive: true, force: true }); |
| 174 | delete process.env.FLOW_CAPTURE_WRITES_ENABLED; |
| 175 | delete process.env.FLOW_CAPTURE_DETECTION_ENABLED; |
| 176 | }); |
| 177 | |
| 178 | it('propose stamps flow_candidate_promote envelope', () => { |
| 179 | const r = handleFlowCaptureProposeRequest({ |
| 180 | dataDir, |
| 181 | vaultId: 'default', |
| 182 | visibleScopes: visible, |
| 183 | candidateId: 'cand_a1b2c3d4', |
| 184 | confirmedScope: 'personal', |
| 185 | intent: 'Promote weekly verify', |
| 186 | createProposal, |
| 187 | }); |
| 188 | assert.equal(r.ok, true); |
| 189 | assert.equal(r.payload.schema, FLOW_CAPTURE_PROPOSAL_SCHEMA); |
| 190 | assert.equal(r.payload.proposal_kind, 'flow_candidate_promote'); |
| 191 | }); |
| 192 | |
| 193 | it('dismiss stamps flow_candidate_dismiss envelope', () => { |
| 194 | const r = handleFlowCaptureDismissRequest({ |
| 195 | dataDir, |
| 196 | vaultId: 'default', |
| 197 | visibleScopes: visible, |
| 198 | candidateId: 'cand_a1b2c3d4', |
| 199 | intent: 'Not recurring', |
| 200 | createProposal, |
| 201 | }); |
| 202 | assert.equal(r.ok, true); |
| 203 | assert.equal(r.payload.proposal_kind, 'flow_candidate_dismiss'); |
| 204 | }); |
| 205 | }); |
File History
1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d
docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge
Human
10 hours ago