flow-authoring-e2e.test.mjs
136 lines 6.0 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 11 hours ago
1 /**
2 * Tier 3 — E2E: propose → approve → reconcile → read, end to end (lib path).
3 *
4 * Mirrors the Hub approve→apply reconcile (precheck + index upsert) that the
5 * server route performs for `source: "flow"` proposals. Synthetic bundles +
6 * an empty starter dir keep the lazy seed inert so only the reconcile mutates
7 * the index.
8 *
9 * @see lib/flow/flow-authoring.mjs
10 * @see docs/FLOW-AUTHORING-WRITEBACK-CONTRACT-7A-L1.md §3, §4
11 */
12 import { describe, it, beforeEach, afterEach } from 'node:test';
13 import assert from 'node:assert/strict';
14 import fs from 'node:fs';
15 import path from 'node:path';
16 import { fileURLToPath } from 'node:url';
17 import {
18 handleFlowProposeRequest,
19 precheckApprovedFlowProposal,
20 applyFlowProposalToIndex,
21 flowStateId,
22 } from '../lib/flow/flow-authoring.mjs';
23 import { getFlow, flowDefinitionForClient, latestStoredFlow, loadFlowStore } from '../lib/flow/flow-store.mjs';
24 import { createProposal, getProposal, updateProposalStatus } from '../hub/proposals-store.mjs';
25 import { makeFlowBundle, emptyStarterDir } from './fixtures/flow/authoring-helpers.mjs';
26
27 const __dirname = path.dirname(fileURLToPath(import.meta.url));
28 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-flow-authoring-e2e');
29 const visible = new Set(['personal', 'project', 'org']);
30
31 /** Simulate the Hub approve→apply reconcile for a flow proposal. */
32 function approveFlowProposal(dataDir, proposalId) {
33 const proposal = getProposal(dataDir, proposalId);
34 const pre = precheckApprovedFlowProposal(dataDir, proposal);
35 if (!pre.ok) return pre;
36 applyFlowProposalToIndex(dataDir, pre.vaultId, pre.flow, pre.steps);
37 updateProposalStatus(dataDir, proposalId, 'approved');
38 return { ok: true };
39 }
40
41 describe('Flow authoring — propose/approve lifecycle', () => {
42 const dataDir = path.join(tmpRoot, 'data');
43 const vaultId = 'default';
44 let starterDir;
45
46 beforeEach(() => {
47 fs.rmSync(tmpRoot, { recursive: true, force: true });
48 fs.mkdirSync(dataDir, { recursive: true });
49 starterDir = emptyStarterDir(dataDir);
50 process.env.FLOW_AUTHORING_WRITES = '1';
51 });
52 afterEach(() => {
53 fs.rmSync(tmpRoot, { recursive: true, force: true });
54 delete process.env.FLOW_AUTHORING_WRITES;
55 });
56
57 it('propose-new → approve → flow get shows the new flow at its version', () => {
58 const bundle = makeFlowBundle({ flowId: 'flow_e2e_new', version: '1.0.0', steps: 3 });
59 const proposed = handleFlowProposeRequest({
60 dataDir, vaultId, visibleScopes: visible, kind: 'new',
61 flow: bundle.flow, steps: bundle.steps, intent: 'add new flow', createProposal,
62 });
63 assert.equal(proposed.ok, true);
64 assert.equal(getFlow(dataDir, vaultId, 'flow_e2e_new', { filterScopes: visible, starterDir }), null);
65
66 assert.equal(approveFlowProposal(dataDir, proposed.payload.proposal_id).ok, true);
67
68 const got = getFlow(dataDir, vaultId, 'flow_e2e_new', { filterScopes: visible, starterDir });
69 assert.ok(got);
70 assert.equal(got.flow.version, '1.0.0');
71 assert.equal(got.steps.length, 3);
72 });
73
74 it('propose-edit with correct base → approve → version bumped, old still pinnable', () => {
75 const bundle = makeFlowBundle({ flowId: 'flow_e2e_edit', version: '1.0.0', steps: 2 });
76 approveFlowProposal(
77 dataDir,
78 handleFlowProposeRequest({
79 dataDir, vaultId, visibleScopes: visible, kind: 'new',
80 flow: bundle.flow, steps: bundle.steps, intent: 'add', createProposal,
81 }).payload.proposal_id,
82 );
83
84 const store = loadFlowStore(dataDir);
85 const cur = latestStoredFlow(store.vaults[vaultId], 'flow_e2e_edit');
86 const canonical = flowDefinitionForClient(cur.flow, cur.steps);
87 const baseStateId = flowStateId(canonical.flow, canonical.steps);
88
89 const edited = structuredClone(canonical);
90 edited.flow.version = '1.1.0';
91 edited.flow.summary = 'edited summary';
92
93 const editProposed = handleFlowProposeRequest({
94 dataDir, vaultId, visibleScopes: visible, kind: 'edit',
95 flow: edited.flow, steps: edited.steps, intent: 'edit it', flowId: 'flow_e2e_edit',
96 baseVersion: '1.0.0', baseStateId, createProposal,
97 });
98 assert.equal(editProposed.ok, true);
99 assert.equal(editProposed.payload.base_version, '1.0.0');
100
101 approveFlowProposal(dataDir, editProposed.payload.proposal_id);
102
103 const latest = getFlow(dataDir, vaultId, 'flow_e2e_edit', { filterScopes: visible, starterDir });
104 assert.equal(latest.flow.version, '1.1.0');
105 const old = getFlow(dataDir, vaultId, 'flow_e2e_edit', { filterScopes: visible, version: '1.0.0', starterDir });
106 assert.ok(old, 'old version row still pinnable');
107 assert.equal(old.flow.version, '1.0.0');
108 });
109
110 it('discard leaves the index unchanged', () => {
111 const bundle = makeFlowBundle({ flowId: 'flow_e2e_discard', steps: 2 });
112 const proposed = handleFlowProposeRequest({
113 dataDir, vaultId, visibleScopes: visible, kind: 'new',
114 flow: bundle.flow, steps: bundle.steps, intent: 'add', createProposal,
115 });
116 assert.equal(proposed.ok, true);
117 updateProposalStatus(dataDir, proposed.payload.proposal_id, 'discarded');
118 assert.equal(getFlow(dataDir, vaultId, 'flow_e2e_discard', { filterScopes: visible, starterDir }), null);
119 });
120
121 it('import bundle routes through the same propose path with lineage preserved', () => {
122 const bundle = makeFlowBundle({ flowId: 'flow_e2e_import', steps: 2 });
123 const imported = handleFlowProposeRequest({
124 dataDir, vaultId, visibleScopes: visible, kind: 'import',
125 bundle: { flow: bundle.flow, steps: bundle.steps }, intent: 'import it',
126 externalRef: 'muse:ref-123', sourceVaultHint: 'partner-vault', createProposal,
127 });
128 assert.equal(imported.ok, true);
129 const stored = getProposal(dataDir, imported.payload.proposal_id);
130 assert.equal(stored.source, 'flow');
131 assert.equal(stored.flow_meta.kind, 'import');
132 assert.match(stored.external_ref, /muse:ref-123/);
133 approveFlowProposal(dataDir, imported.payload.proposal_id);
134 assert.ok(getFlow(dataDir, vaultId, 'flow_e2e_import', { filterScopes: visible, starterDir }));
135 });
136 });
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 11 hours ago