path-list-data-integrity.test.mjs
133 lines 4.8 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago
1 /**
2 * Tier 5 — DATA-INTEGRITY: blob merge by path_id, apply twice, archive keeps row (KN-WORK-PATH-LIST-b).
3 *
4 * @see docs/KN-WORK-PATH-LIST-FREEZE.md §7
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 { mergeFlowStoreJson } from '../hub/bridge/external-agent-blob-store.mjs';
12 import { loadFlowStore } from '../lib/flow/flow-store.mjs';
13 import { upsertLearningPath, getLearningPath, loadLearningPaths } from '../lib/path/path-store.mjs';
14 import { applyApprovedPathProposal } from '../lib/path/path-write.mjs';
15
16 const __dirname = path.dirname(fileURLToPath(import.meta.url));
17 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-path-list-integrity');
18
19 function sampleRecord(overrides = {}) {
20 return {
21 schema: 'knowtation.learning_path/v0',
22 path_id: 'path_aabbccddeeff0011',
23 scope: 'personal',
24 status: 'active',
25 title: 'Keep me',
26 summary: 'Summary',
27 goal: 'Goal',
28 steps: [{ title: 'S1', objective: 'O1', source_document_ids: [] }],
29 current_step_index: 0,
30 step_count: 1,
31 next_step_title: 'S1',
32 active_decisions: '',
33 workspace_id: 'ws-personal',
34 note_path: 'notes/keep.md',
35 created: '2026-08-18T00:00:00Z',
36 updated: '2026-08-18T00:00:00Z',
37 ...overrides,
38 };
39 }
40
41 beforeEach(() => {
42 fs.rmSync(tmpRoot, { recursive: true, force: true });
43 fs.mkdirSync(tmpRoot, { recursive: true });
44 process.env.PATH_WRITES_ENABLED = '1';
45 });
46
47 afterEach(() => {
48 fs.rmSync(tmpRoot, { recursive: true, force: true });
49 delete process.env.PATH_WRITES_ENABLED;
50 });
51
52 describe('path-list data-integrity', () => {
53 it('mergeFlowStoreJson unions by path_id (stale local must not mask blob)', () => {
54 const local = {
55 vaults: {
56 v: {
57 learning_paths: [sampleRecord({ path_id: 'path_localonly0000001', title: 'Local only' })],
58 },
59 },
60 };
61 const blob = {
62 vaults: {
63 v: {
64 learning_paths: [
65 sampleRecord({
66 path_id: 'path_blobonly00000001',
67 title: 'Blob only',
68 updated: '2026-08-19T00:00:00Z',
69 }),
70 sampleRecord({
71 path_id: 'path_localonly0000001',
72 title: 'Fresher blob title',
73 updated: '2026-08-20T00:00:00Z',
74 }),
75 ],
76 },
77 },
78 };
79 const merged = JSON.parse(mergeFlowStoreJson(JSON.stringify(local), JSON.stringify(blob)));
80 const ids = merged.vaults.v.learning_paths.map((p) => p.path_id).sort();
81 assert.deepEqual(ids, ['path_blobonly00000001', 'path_localonly0000001']);
82 const shared = merged.vaults.v.learning_paths.find((p) => p.path_id === 'path_localonly0000001');
83 assert.equal(shared.title, 'Fresher blob title');
84 });
85
86 it('apply twice is idempotent', () => {
87 const dataDir = path.join(tmpRoot, 'twice');
88 fs.mkdirSync(dataDir, { recursive: true });
89 const record = sampleRecord();
90 const proposal = {
91 proposal_id: 'prop_twice',
92 vault_id: 'v',
93 source: 'learning_path',
94 review_queue: 'learning-path',
95 body: JSON.stringify({ proposal_kind: 'path_create', path: record }),
96 frontmatter: { proposal_kind: 'path_create', path_id: record.path_id },
97 };
98 const first = applyApprovedPathProposal(dataDir, proposal);
99 const second = applyApprovedPathProposal(dataDir, proposal);
100 assert.equal(first.ok, true);
101 assert.equal(second.ok, true);
102 assert.equal(loadLearningPaths(dataDir, 'v').length, 1);
103 });
104
105 it('archive keeps the row and does not delete note_path', () => {
106 const dataDir = path.join(tmpRoot, 'arch');
107 fs.mkdirSync(dataDir, { recursive: true });
108 const record = sampleRecord();
109 upsertLearningPath(dataDir, 'v', record);
110 const proposal = {
111 proposal_id: 'prop_arch',
112 vault_id: 'v',
113 source: 'learning_path',
114 body: JSON.stringify({ proposal_kind: 'path_archive', path_id: record.path_id }),
115 frontmatter: { proposal_kind: 'path_archive' },
116 };
117 const applied = applyApprovedPathProposal(dataDir, proposal);
118 assert.equal(applied.ok, true);
119 const got = getLearningPath(dataDir, 'v', record.path_id, { visibleScopes: new Set(['personal']) });
120 assert.equal(got.status, 'archived');
121 assert.equal(got.note_path, 'notes/keep.md');
122 });
123
124 it('restart load reads the same store file', () => {
125 const dataDir = path.join(tmpRoot, 'restart');
126 fs.mkdirSync(dataDir, { recursive: true });
127 upsertLearningPath(dataDir, 'v', sampleRecord());
128 const first = loadFlowStore(dataDir);
129 const second = loadFlowStore(dataDir);
130 assert.equal(first.vaults.v.learning_paths[0].path_id, second.vaults.v.learning_paths[0].path_id);
131 assert.equal(second.vaults.v.learning_paths[0].title, 'Keep me');
132 });
133 });
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago