flow-authoring-performance.test.mjs
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d
docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge
Human
20 hours ago
| 1 | /** |
| 2 | * Tier 6 — PERFORMANCE: propose validation + flowStateId within a p95 budget on |
| 3 | * a large fixture; approve→apply bounded; no quadratic version resolution. |
| 4 | * |
| 5 | * @see lib/flow/flow-authoring.mjs |
| 6 | * @see docs/FLOW-AUTHORING-WRITEBACK-CONTRACT-7A-L1.md §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 { flowDefinitionForClient, MAX_STEPS_PER_FLOW, buildFlowStepId } from '../lib/flow/flow-store.mjs'; |
| 20 | import { createProposal, getProposal } from '../hub/proposals-store.mjs'; |
| 21 | |
| 22 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 23 | const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-flow-authoring-perf'); |
| 24 | const visible = new Set(['personal', 'project', 'org']); |
| 25 | |
| 26 | function bigFlow(flowId, version, stepCount) { |
| 27 | const steps = []; |
| 28 | const refs = []; |
| 29 | for (let i = 1; i <= stepCount; i += 1) { |
| 30 | const stepId = buildFlowStepId(flowId, i); |
| 31 | refs.push(stepId); |
| 32 | steps.push({ |
| 33 | schema: 'knowtation.flow_step/v0', |
| 34 | step_id: stepId, |
| 35 | flow_id: flowId, |
| 36 | ordinal: i, |
| 37 | owned_job: `Job ${i}`, |
| 38 | instruction: `Do step ${i}.`, |
| 39 | trigger: `Run ${i}.`, |
| 40 | when_not_to_run: `Skip ${i}.`, |
| 41 | boundaries: ['Read only'], |
| 42 | output_shape: `Out ${i}.`, |
| 43 | verification: { kind: 'artifact_exists', evidence_required: true, description: `Artifact ${i}.` }, |
| 44 | automatable: 'manual', |
| 45 | }); |
| 46 | } |
| 47 | return { |
| 48 | flow: { |
| 49 | schema: 'knowtation.flow/v0', flow_id: flowId, title: 'Big', version, |
| 50 | scope: 'personal', summary: 'big', tags: [], steps: refs, inputs: [], |
| 51 | updated: '2026-06-20T00:00:00Z', truncated: false, |
| 52 | }, |
| 53 | steps, |
| 54 | }; |
| 55 | } |
| 56 | |
| 57 | function p95(samples) { |
| 58 | const sorted = [...samples].sort((a, b) => a - b); |
| 59 | return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))]; |
| 60 | } |
| 61 | |
| 62 | describe('Flow authoring — performance', () => { |
| 63 | const dataDir = path.join(tmpRoot, 'data'); |
| 64 | const vaultId = 'default'; |
| 65 | |
| 66 | beforeEach(() => { |
| 67 | fs.rmSync(tmpRoot, { recursive: true, force: true }); |
| 68 | fs.mkdirSync(dataDir, { recursive: true }); |
| 69 | process.env.FLOW_AUTHORING_WRITES = '1'; |
| 70 | }); |
| 71 | afterEach(() => { |
| 72 | fs.rmSync(tmpRoot, { recursive: true, force: true }); |
| 73 | delete process.env.FLOW_AUTHORING_WRITES; |
| 74 | }); |
| 75 | |
| 76 | it('flowStateId on a MAX_STEPS_PER_FLOW flow stays under a p95 budget', () => { |
| 77 | const big = bigFlow('flow_perf', '1.0.0', MAX_STEPS_PER_FLOW); |
| 78 | const def = flowDefinitionForClient(big.flow, big.steps); |
| 79 | const samples = []; |
| 80 | for (let i = 0; i < 200; i += 1) { |
| 81 | const t = process.hrtime.bigint(); |
| 82 | flowStateId(def.flow, def.steps); |
| 83 | samples.push(Number(process.hrtime.bigint() - t) / 1e6); |
| 84 | } |
| 85 | assert.ok(p95(samples) < 25, `flowStateId p95 ${p95(samples).toFixed(2)}ms`); |
| 86 | }); |
| 87 | |
| 88 | it('propose validation on the large fixture stays under a p95 budget', () => { |
| 89 | const samples = []; |
| 90 | for (let i = 0; i < 50; i += 1) { |
| 91 | const big = bigFlow(`flow_perf_${i}`, '1.0.0', MAX_STEPS_PER_FLOW); |
| 92 | const t = process.hrtime.bigint(); |
| 93 | const r = handleFlowProposeRequest({ |
| 94 | dataDir, vaultId, visibleScopes: visible, kind: 'new', |
| 95 | flow: big.flow, steps: big.steps, intent: 'perf', createProposal, |
| 96 | }); |
| 97 | samples.push(Number(process.hrtime.bigint() - t) / 1e6); |
| 98 | assert.equal(r.ok, true); |
| 99 | } |
| 100 | assert.ok(p95(samples) < 150, `propose p95 ${p95(samples).toFixed(2)}ms`); |
| 101 | }); |
| 102 | |
| 103 | it('approve→apply reconcile is bounded for a large flow', () => { |
| 104 | const big = bigFlow('flow_perf_apply', '1.0.0', MAX_STEPS_PER_FLOW); |
| 105 | const proposed = handleFlowProposeRequest({ |
| 106 | dataDir, vaultId, visibleScopes: visible, kind: 'new', |
| 107 | flow: big.flow, steps: big.steps, intent: 'perf', createProposal, |
| 108 | }); |
| 109 | const proposal = getProposal(dataDir, proposed.payload.proposal_id); |
| 110 | const t = process.hrtime.bigint(); |
| 111 | const pre = precheckApprovedFlowProposal(dataDir, proposal); |
| 112 | applyFlowProposalToIndex(dataDir, pre.vaultId, pre.flow, pre.steps); |
| 113 | const ms = Number(process.hrtime.bigint() - t) / 1e6; |
| 114 | assert.ok(pre.ok); |
| 115 | assert.ok(ms < 200, `reconcile ${ms.toFixed(2)}ms`); |
| 116 | }); |
| 117 | }); |
File History
1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d
docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge
Human
20 hours ago