flow-capture-data-integrity.test.mjs
113 lines 5.2 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 13 hours ago
1 /**
2 * Tier 5 — DATA-INTEGRITY: candidate round-trip, promote reconcile, merge terminal, scope.
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 handleFlowCaptureProposeRequest,
13 precheckApprovedCaptureProposal,
14 applyCaptureProposal,
15 candidateSummaryForClient,
16 } from '../lib/flow/flow-capture.mjs';
17 import { getFlow, upsertCandidate, getCandidate, loadFlowStore } from '../lib/flow/flow-store.mjs';
18 import { applyFlowProposalToIndex } from '../lib/flow/flow-authoring.mjs';
19 import { makeFlowBundle } from './fixtures/flow/authoring-helpers.mjs';
20 import { createProposal, getProposal } from '../hub/proposals-store.mjs';
21 import { emptyStarterDir } from './fixtures/flow/authoring-helpers.mjs';
22 import { makeCandidateRecord } from './fixtures/flow/capture-helpers.mjs';
23
24 const __dirname = path.dirname(fileURLToPath(import.meta.url));
25 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-flow-capture-integrity');
26 const visible = new Set(['personal', 'project', 'org']);
27
28 describe('Flow capture — data integrity', () => {
29 const dataDir = path.join(tmpRoot, 'data');
30 const vaultId = 'default';
31 let starterDir;
32
33 beforeEach(() => {
34 fs.rmSync(tmpRoot, { recursive: true, force: true });
35 fs.mkdirSync(dataDir, { recursive: true });
36 starterDir = emptyStarterDir(dataDir);
37 process.env.FLOW_CAPTURE_WRITES_ENABLED = '1';
38 });
39 afterEach(() => {
40 fs.rmSync(tmpRoot, { recursive: true, force: true });
41 delete process.env.FLOW_CAPTURE_WRITES_ENABLED;
42 });
43
44 it('candidate round-trip preserves fields via summary', () => {
45 const raw = makeCandidateRecord({ candidate_id: 'cand_integrity1' });
46 upsertCandidate(dataDir, vaultId, raw);
47 const stored = getCandidate(dataDir, vaultId, 'cand_integrity1', visible);
48 const summary = candidateSummaryForClient(stored);
49 assert.deepEqual(summary.evidence_refs, raw.evidence_refs);
50 assert.deepEqual(summary.draft_steps, raw.draft_steps);
51 assert.equal(summary.scope_hint, raw.scope_hint);
52 });
53
54 it('promote approve reconciles bundle byte-stable vs proposal', () => {
55 upsertCandidate(dataDir, vaultId, makeCandidateRecord({ candidate_id: 'cand_integrity2', scope_hint: 'project' }));
56 const proposed = handleFlowCaptureProposeRequest({
57 dataDir, vaultId, visibleScopes: visible, candidateId: 'cand_integrity2', confirmedScope: 'personal', intent: 'promote', createProposal, starterDir,
58 });
59 const proposal = getProposal(dataDir, proposed.payload.proposal_id);
60 const pre = precheckApprovedCaptureProposal(dataDir, proposal);
61 assert.equal(pre.ok, true);
62 const body = JSON.parse(proposal.body);
63 assert.deepEqual(pre.flow, body.bundle.flow);
64 applyCaptureProposal(dataDir, pre);
65 const got = getFlow(dataDir, vaultId, pre.flow.flow_id, { filterScopes: visible, starterDir });
66 assert.equal(got.flow.scope, 'personal');
67 assert.notEqual(got.flow.scope, 'project');
68 });
69
70 it('merge sets merged_into terminal on approve', () => {
71 const existing = makeFlowBundle({
72 flowId: 'flow_integrity_merge',
73 steps: 2,
74 summary: 'Open target URL verify response record pointer',
75 });
76 existing.steps[0].instruction = 'Open the target URL verify response record pointer';
77 existing.steps[1].instruction = 'Record result pointer verify step';
78 applyFlowProposalToIndex(dataDir, vaultId, existing.flow, existing.steps);
79
80 upsertCandidate(
81 dataDir,
82 vaultId,
83 makeCandidateRecord({
84 candidate_id: 'cand_integrity3',
85 draft_steps: ['Open the target URL', 'Verify response status', 'Record result pointer'],
86 }),
87 );
88 const proposed = handleFlowCaptureProposeRequest({
89 dataDir, vaultId, visibleScopes: visible, candidateId: 'cand_integrity3', confirmedScope: 'personal', intent: 'merge', mergeIntoFlowId: 'flow_integrity_merge', createProposal, starterDir,
90 });
91 assert.equal(proposed.ok, true);
92 assert.equal(proposed.payload.proposal_kind, 'flow_candidate_merge');
93 const pre = precheckApprovedCaptureProposal(dataDir, getProposal(dataDir, proposed.payload.proposal_id));
94 assert.equal(pre.ok, true);
95 applyCaptureProposal(dataDir, pre);
96 assert.equal(getCandidate(dataDir, vaultId, 'cand_integrity3', visible)?.status, 'merged_into:flow_integrity_merge');
97 });
98
99 it('evidence_refs remain pointers after lifecycle', () => {
100 const refs = ['hash:deadbeef', 'run:abc', 'proposal:xyz'];
101 upsertCandidate(dataDir, vaultId, makeCandidateRecord({ candidate_id: 'cand_integrity4', evidence_refs: refs }));
102 const proposed = handleFlowCaptureProposeRequest({
103 dataDir, vaultId, visibleScopes: visible, candidateId: 'cand_integrity4', confirmedScope: 'personal', intent: 'x', createProposal, starterDir,
104 });
105 const pre = precheckApprovedCaptureProposal(dataDir, getProposal(dataDir, proposed.payload.proposal_id));
106 applyCaptureProposal(dataDir, pre);
107 const after = getCandidate(dataDir, vaultId, 'cand_integrity4', visible);
108 assert.deepEqual(after.evidence_refs, refs);
109 for (const ref of after.evidence_refs) {
110 assert.match(ref, /^(hash:|run:|proposal:|skill:)/);
111 }
112 });
113 });
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 13 hours ago