flow-projection-generator-unit.test.mjs
101 lines 4.3 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 1 day ago
1 /**
2 * Tier 1 — UNIT: projection generator pure functions.
3 *
4 * @see docs/FLOW-PROJECTION-GENERATOR-CONTRACT-7A-11.md §9
5 */
6 import { describe, it } 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 isHarnessActive,
13 projectFlow,
14 computeFidelity,
15 renderedContentHash,
16 isProjectionStale,
17 detectDrift,
18 flowProjectionForClient,
19 GENERATED_MARKER_PREFIX,
20 HARNESS_VALUES,
21 } from '../lib/flow/projection-generator.mjs';
22
23 const __dirname = path.dirname(fileURLToPath(import.meta.url));
24 const handoverBundle = JSON.parse(
25 fs.readFileSync(path.join(__dirname, '../flows/starter/flow_overseer_handover.json'), 'utf8'),
26 );
27
28 describe('projection generator — unit', () => {
29 it('isHarnessActive is true only for cursor_rule and cli_runbook', () => {
30 assert.equal(isHarnessActive('cursor_rule'), true);
31 assert.equal(isHarnessActive('cli_runbook'), true);
32 for (const h of HARNESS_VALUES) {
33 if (h !== 'cursor_rule' && h !== 'cli_runbook') {
34 assert.equal(isHarnessActive(h), false);
35 }
36 }
37 });
38
39 it('projectFlow returns §1.7 shape with marker-first rendered and ordered steps', () => {
40 const projection = projectFlow(handoverBundle.flow, handoverBundle.steps, {
41 harness: 'cli_runbook',
42 });
43 assert.equal(projection.schema, 'knowtation.flow_projection/v0');
44 assert.equal(projection.generated_from_canonical, true);
45 assert.equal(projection.editable, false);
46 assert.ok(projection.rendered.includes(GENERATED_MARKER_PREFIX));
47 assert.ok(projection.rendered.indexOf('## Step 1') < projection.rendered.indexOf('## Step 2'));
48 const client = flowProjectionForClient(projection);
49 assert.deepEqual(Object.keys(client).sort(), Object.keys(projection).sort());
50 assert.equal(Object.keys(client).length, Object.keys(projection).length);
51 });
52
53 it('projectFlow is deterministic for identical input', () => {
54 const a = projectFlow(handoverBundle.flow, handoverBundle.steps, { harness: 'cursor_rule' });
55 const b = projectFlow(handoverBundle.flow, handoverBundle.steps, { harness: 'cursor_rule' });
56 assert.equal(a.rendered, b.rendered);
57 assert.equal(renderedContentHash(a.rendered), renderedContentHash(b.rendered));
58 });
59
60 it('computeFidelity lists present-but-unexpressible fields for cursor_rule', () => {
61 const fidelity = computeFidelity('cursor_rule', handoverBundle.flow, handoverBundle.steps);
62 assert.ok(fidelity.dropped_fields.includes('when_not_to_run'));
63 assert.ok(fidelity.dropped_fields.includes('requires'));
64 assert.deepEqual(fidelity.dropped_fields, [...fidelity.dropped_fields].sort());
65 const runbook = computeFidelity('cli_runbook', handoverBundle.flow, handoverBundle.steps);
66 assert.ok(!runbook.dropped_fields.includes('when_not_to_run'));
67 });
68
69 it('isProjectionStale fails closed on unparseable and compares semver', () => {
70 assert.equal(isProjectionStale('0.1.0', '0.2.0'), true);
71 assert.equal(isProjectionStale('0.2.0', '0.2.0'), false);
72 assert.equal(isProjectionStale('0.2.0', '0.1.0'), false);
73 assert.equal(isProjectionStale('bad', '0.1.0'), true);
74 assert.equal(isProjectionStale('0.1.0', 'bad'), true);
75 });
76
77 it('renderedContentHash is stable and sensitive to one-byte changes', () => {
78 const projection = projectFlow(handoverBundle.flow, handoverBundle.steps, {
79 harness: 'cli_runbook',
80 });
81 const h1 = renderedContentHash(projection.rendered);
82 const h2 = renderedContentHash(projection.rendered);
83 assert.equal(h1, h2);
84 assert.match(h1, /^sha256:[a-f0-9]{64}$/);
85 const mutated = `${projection.rendered}x`;
86 assert.notEqual(renderedContentHash(mutated), h1);
87 });
88
89 it('detectDrift classifies clean, edited, missing_marker, and absent', () => {
90 const fresh = projectFlow(handoverBundle.flow, handoverBundle.steps, {
91 harness: 'cli_runbook',
92 }).rendered;
93 assert.deepEqual(detectDrift(fresh, fresh), { drift: false, reason: 'clean' });
94 assert.deepEqual(detectDrift('# hand edited\n', fresh), {
95 drift: true,
96 reason: 'missing_marker',
97 });
98 assert.deepEqual(detectDrift('', fresh), { drift: true, reason: 'absent' });
99 assert.deepEqual(detectDrift(`${fresh}\nextra`, fresh), { drift: true, reason: 'edited' });
100 });
101 });
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 1 day ago