flow-capture-e2e.test.mjs
146 lines 6.3 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 19 hours ago
1 /**
2 * Tier 3 — E2E: observe → list → propose → approve → flow get; dismiss + dedup paths.
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 precheckApprovedCaptureProposal,
17 applyCaptureProposal,
18 } from '../lib/flow/flow-capture.mjs';
19 import { applyFlowProposalToIndex } from '../lib/flow/flow-authoring.mjs';
20 import { getFlow, upsertCandidate, getCandidate } from '../lib/flow/flow-store.mjs';
21 import { createProposal, getProposal, updateProposalStatus } from '../hub/proposals-store.mjs';
22 import { makeFlowBundle, emptyStarterDir } from './fixtures/flow/authoring-helpers.mjs';
23 import { validSessionMeta, makeCandidateRecord } from './fixtures/flow/capture-helpers.mjs';
24
25 const __dirname = path.dirname(fileURLToPath(import.meta.url));
26 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-flow-capture-e2e');
27 const visible = new Set(['personal', 'project', 'org']);
28
29 function approveCapture(dataDir, proposalId) {
30 const proposal = getProposal(dataDir, proposalId);
31 const pre = precheckApprovedCaptureProposal(dataDir, proposal);
32 if (!pre.ok) return pre;
33 applyCaptureProposal(dataDir, pre);
34 updateProposalStatus(dataDir, proposalId, 'approved');
35 return { ok: true, pre };
36 }
37
38 describe('Flow capture — full lifecycle', () => {
39 const dataDir = path.join(tmpRoot, 'data');
40 const vaultId = 'default';
41 let starterDir;
42
43 beforeEach(() => {
44 fs.rmSync(tmpRoot, { recursive: true, force: true });
45 fs.mkdirSync(dataDir, { recursive: true });
46 starterDir = emptyStarterDir(dataDir);
47 process.env.FLOW_CAPTURE_DETECTION_ENABLED = '1';
48 process.env.FLOW_CAPTURE_WRITES_ENABLED = '1';
49 });
50 afterEach(() => {
51 fs.rmSync(tmpRoot, { recursive: true, force: true });
52 delete process.env.FLOW_CAPTURE_DETECTION_ENABLED;
53 delete process.env.FLOW_CAPTURE_WRITES_ENABLED;
54 });
55
56 it('observe → list → propose → approve → flow get shows new Flow', () => {
57 const observed = handleFlowCaptureObserveRequest({
58 dataDir, vaultId, visibleScopes: visible, sessionMeta: validSessionMeta(), harness: 'test',
59 });
60 assert.equal(observed.ok, true);
61 assert.ok(observed.payload.returned_count >= 1);
62 const candidateId = observed.payload.candidates[0].candidate_id;
63
64 const listed = handleFlowCaptureListRequest({ dataDir, vaultId, visibleScopes: visible });
65 assert.ok(listed.payload.candidates.some((c) => c.candidate_id === candidateId));
66
67 const proposed = handleFlowCaptureProposeRequest({
68 dataDir, vaultId, visibleScopes: visible, candidateId, confirmedScope: 'personal', intent: 'promote it', createProposal, starterDir,
69 });
70 assert.equal(proposed.ok, true);
71 const approve = approveCapture(dataDir, proposed.payload.proposal_id);
72 assert.equal(approve.ok, true);
73
74 const got = getFlow(dataDir, vaultId, approve.pre.flow.flow_id, { filterScopes: visible, starterDir });
75 assert.ok(got);
76 assert.equal(got.flow.scope, 'personal');
77 assert.equal(getCandidate(dataDir, vaultId, candidateId, visible)?.status, 'promoted');
78 });
79
80 it('dismiss path → rejected terminal', () => {
81 upsertCandidate(dataDir, vaultId, makeCandidateRecord({ candidate_id: 'cand_e2edismiss' }));
82 const proposed = handleFlowCaptureDismissRequest({
83 dataDir, vaultId, visibleScopes: visible, candidateId: 'cand_e2edismiss', intent: 'not recurring', createProposal,
84 });
85 assert.equal(proposed.ok, true);
86 approveCapture(dataDir, proposed.payload.proposal_id);
87 assert.equal(getCandidate(dataDir, vaultId, 'cand_e2edismiss', visible)?.status, 'rejected');
88 });
89
90 it('dedup overlap → merge proposal path', () => {
91 const existing = makeFlowBundle({
92 flowId: 'flow_existing_dedup',
93 steps: 2,
94 summary: 'Open target URL verify response status record result pointer',
95 });
96 existing.steps[0].owned_job = 'Open the target URL';
97 existing.steps[0].instruction = 'Open the target URL and verify response status record result pointer';
98 existing.steps[1].owned_job = 'Record result pointer';
99 existing.steps[1].instruction = 'Record result pointer for verify step';
100 applyFlowProposalToIndex(dataDir, vaultId, existing.flow, existing.steps);
101
102 upsertCandidate(
103 dataDir,
104 vaultId,
105 makeCandidateRecord({
106 candidate_id: 'cand_e2emerge01',
107 draft_steps: [
108 'Open the target URL',
109 'Verify response status',
110 'Record result pointer',
111 ],
112 }),
113 );
114
115 const blocked = handleFlowCaptureProposeRequest({
116 dataDir, vaultId, visibleScopes: visible, candidateId: 'cand_e2emerge01', confirmedScope: 'personal', intent: 'promote', createProposal, starterDir,
117 });
118 assert.equal(blocked.ok, false);
119 assert.equal(blocked.code, 'FLOW_CAPTURE_DEDUP_MERGE_REQUIRED');
120
121 const merge = handleFlowCaptureProposeRequest({
122 dataDir, vaultId, visibleScopes: visible, candidateId: 'cand_e2emerge01', confirmedScope: 'personal', intent: 'merge it', mergeIntoFlowId: 'flow_existing_dedup', createProposal, starterDir,
123 });
124 assert.equal(merge.ok, true);
125 assert.equal(merge.payload.proposal_kind, 'flow_candidate_merge');
126 approveCapture(dataDir, merge.payload.proposal_id);
127 assert.equal(getCandidate(dataDir, vaultId, 'cand_e2emerge01', visible)?.status, 'merged_into:flow_existing_dedup');
128 });
129
130 it('low confidence suppressed unless flag set', () => {
131 upsertCandidate(
132 dataDir,
133 vaultId,
134 makeCandidateRecord({ candidate_id: 'cand_lowconf01', confidence: 'low' }),
135 );
136 const blocked = handleFlowCaptureProposeRequest({
137 dataDir, vaultId, visibleScopes: visible, candidateId: 'cand_lowconf01', confirmedScope: 'personal', intent: 'x', createProposal, starterDir,
138 });
139 assert.equal(blocked.code, 'FLOW_CAPTURE_LOW_CONFIDENCE_SUPPRESSED');
140
141 const allowed = handleFlowCaptureProposeRequest({
142 dataDir, vaultId, visibleScopes: visible, candidateId: 'cand_lowconf01', confirmedScope: 'personal', intent: 'x', allowLowConfidence: true, createProposal, starterDir,
143 });
144 assert.equal(allowed.ok, true);
145 });
146 });
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 19 hours ago