flow-authoring-data-integrity.test.mjs
138 lines 5.9 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 16 hours ago
1 /**
2 * Tier 5 — DATA-INTEGRITY: reconcile preserves content; edit = new version row;
3 * a conflicting approve leaves zero partial state.
4 *
5 * @see lib/flow/flow-authoring.mjs
6 * @see docs/FLOW-AUTHORING-WRITEBACK-CONTRACT-7A-L1.md §3, §4, §7
7 */
8 import { describe, it, beforeEach, afterEach } from 'node:test';
9 import assert from 'node:assert/strict';
10 import fs from 'node:fs';
11 import path from 'node:path';
12 import { fileURLToPath } from 'node:url';
13 import {
14 handleFlowProposeRequest,
15 precheckApprovedFlowProposal,
16 applyFlowProposalToIndex,
17 flowStateId,
18 } from '../lib/flow/flow-authoring.mjs';
19 import {
20 getFlow,
21 flowDefinitionForClient,
22 latestStoredFlow,
23 loadFlowStore,
24 } from '../lib/flow/flow-store.mjs';
25 import { createProposal, getProposal } from '../hub/proposals-store.mjs';
26 import { makeFlowBundle, emptyStarterDir } from './fixtures/flow/authoring-helpers.mjs';
27
28 const __dirname = path.dirname(fileURLToPath(import.meta.url));
29 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-flow-authoring-integrity');
30 const visible = new Set(['personal', 'project', 'org']);
31
32 function approve(dataDir, id) {
33 const pre = precheckApprovedFlowProposal(dataDir, getProposal(dataDir, id));
34 if (pre.ok) applyFlowProposalToIndex(dataDir, pre.vaultId, pre.flow, pre.steps);
35 return pre;
36 }
37
38 describe('Flow authoring — data integrity', () => {
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_AUTHORING_WRITES = '1';
48 });
49 afterEach(() => {
50 fs.rmSync(tmpRoot, { recursive: true, force: true });
51 delete process.env.FLOW_AUTHORING_WRITES;
52 });
53
54 it('reconcile preserves steps/skill-refs/verification/scope/version/lineage byte-for-byte', () => {
55 const bundle = makeFlowBundle({ flowId: 'flow_di_preserve', version: '1.2.0', steps: 4 });
56 const proposed = handleFlowProposeRequest({
57 dataDir, vaultId, visibleScopes: visible, kind: 'new',
58 flow: bundle.flow, steps: bundle.steps, intent: 'add', createProposal,
59 });
60 approve(dataDir, proposed.payload.proposal_id);
61
62 const got = getFlow(dataDir, vaultId, 'flow_di_preserve', { filterScopes: visible, starterDir });
63 assert.equal(got.flow.scope, bundle.flow.scope);
64 assert.equal(got.flow.version, bundle.flow.version);
65 assert.deepEqual(got.flow.steps, bundle.flow.steps);
66 for (let i = 0; i < bundle.steps.length; i += 1) {
67 assert.deepEqual(got.steps[i].skill_refs, bundle.steps[i].skill_refs ?? []);
68 assert.deepEqual(got.steps[i].verification, bundle.steps[i].verification);
69 assert.equal(got.steps[i].instruction, bundle.steps[i].instruction);
70 }
71 });
72
73 it('an edit creates a NEW (flow_id, version) row (carry-forward; never mutates the version row in place)', () => {
74 const bundle = makeFlowBundle({ flowId: 'flow_di_carry', version: '1.0.0', steps: 2 });
75 approve(
76 dataDir,
77 handleFlowProposeRequest({
78 dataDir, vaultId, visibleScopes: visible, kind: 'new',
79 flow: bundle.flow, steps: bundle.steps, intent: 'add', createProposal,
80 }).payload.proposal_id,
81 );
82
83 const store = loadFlowStore(dataDir);
84 const cur = latestStoredFlow(store.vaults[vaultId], 'flow_di_carry');
85 const canonical = flowDefinitionForClient(cur.flow, cur.steps);
86 const baseStateId = flowStateId(canonical.flow, canonical.steps);
87 const edited = structuredClone(canonical);
88 edited.flow.version = '2.0.0';
89
90 const editProposed = handleFlowProposeRequest({
91 dataDir, vaultId, visibleScopes: visible, kind: 'edit',
92 flow: edited.flow, steps: edited.steps, intent: 'edit', flowId: 'flow_di_carry',
93 baseVersion: '1.0.0', baseStateId, createProposal,
94 });
95 approve(dataDir, editProposed.payload.proposal_id);
96
97 const finalStore = loadFlowStore(dataDir);
98 const rows = finalStore.vaults[vaultId].flows.filter((f) => f.flow_id === 'flow_di_carry');
99 assert.equal(rows.length, 2, 'both version rows present');
100 assert.ok(rows.find((r) => r.version === '1.0.0'), '1.0.0 row preserved (not version-mutated in place)');
101 assert.ok(rows.find((r) => r.version === '2.0.0'), '2.0.0 row added');
102 });
103
104 it('a conflicting approve leaves zero partial index state', () => {
105 const bundle = makeFlowBundle({ flowId: 'flow_di_conflict', version: '1.0.0', steps: 2 });
106 approve(
107 dataDir,
108 handleFlowProposeRequest({
109 dataDir, vaultId, visibleScopes: visible, kind: 'new',
110 flow: bundle.flow, steps: bundle.steps, intent: 'add', createProposal,
111 }).payload.proposal_id,
112 );
113
114 const store = loadFlowStore(dataDir);
115 const cur = latestStoredFlow(store.vaults[vaultId], 'flow_di_conflict');
116 const canonical = flowDefinitionForClient(cur.flow, cur.steps);
117 const baseStateId = flowStateId(canonical.flow, canonical.steps);
118
119 const a = handleFlowProposeRequest({
120 dataDir, vaultId, visibleScopes: visible, kind: 'edit',
121 flow: { ...canonical.flow, version: '2.0.0', summary: 'A' }, steps: canonical.steps,
122 intent: 'A', flowId: 'flow_di_conflict', baseVersion: '1.0.0', baseStateId, createProposal,
123 });
124 const b = handleFlowProposeRequest({
125 dataDir, vaultId, visibleScopes: visible, kind: 'edit',
126 flow: { ...canonical.flow, version: '2.0.0', summary: 'B' }, steps: canonical.steps,
127 intent: 'B', flowId: 'flow_di_conflict', baseVersion: '1.0.0', baseStateId, createProposal,
128 });
129 assert.equal(approve(dataDir, a.payload.proposal_id).ok, true);
130
131 const before = JSON.stringify(loadFlowStore(dataDir));
132 const second = approve(dataDir, b.payload.proposal_id);
133 assert.equal(second.ok, false);
134 assert.equal(second.code, 'FLOW_LINEAGE_CONFLICT');
135 const after = JSON.stringify(loadFlowStore(dataDir));
136 assert.equal(before, after, 'store unchanged by the conflicting approve');
137 });
138 });
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 16 hours ago