path-list-e2e.test.mjs
166 lines 5.7 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 9 days ago
1 /**
2 * Tier 3 — E2E: Hub/bridge/gateway walkthrough with fakes (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 { handlePathListRequest, handlePathGetRequest } from '../lib/path/path-handlers.mjs';
13 import { handlePathProposeRequest, applyApprovedPathProposal } from '../lib/path/path-write.mjs';
14 import { maybeApplyHostedPathAfterApprove } from '../hub/gateway/path-approve-hosted.mjs';
15 import { isPathProposalForHostedApply } from '../lib/path/path-hosted-proposal.mjs';
16 import { getRepoRoot } from '../lib/repo-root.mjs';
17
18 const __dirname = path.dirname(fileURLToPath(import.meta.url));
19 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-path-list-e2e');
20
21 function sampleSteps() {
22 return [{ title: 'One', objective: 'Do one', source_document_ids: [] }];
23 }
24
25 function fakeCreateProposal(dataDir, input) {
26 const proposal_id = `prop_e2e_${Date.now()}`;
27 const row = { proposal_id, status: 'proposed', ...input };
28 const fp = path.join(dataDir, 'hub_proposals.json');
29 const all = fs.existsSync(fp) ? JSON.parse(fs.readFileSync(fp, 'utf8')) : [];
30 all.push(row);
31 fs.writeFileSync(fp, JSON.stringify(all, null, 2), 'utf8');
32 return row;
33 }
34
35 beforeEach(() => {
36 fs.rmSync(tmpRoot, { recursive: true, force: true });
37 fs.mkdirSync(tmpRoot, { recursive: true });
38 delete process.env.PATH_WRITES_ENABLED;
39 });
40
41 afterEach(() => {
42 fs.rmSync(tmpRoot, { recursive: true, force: true });
43 delete process.env.PATH_WRITES_ENABLED;
44 });
45
46 describe('path-list e2e', () => {
47 it('GET list/get, POST propose, approve apply writes one path; blob hydrate then get', async () => {
48 process.env.PATH_WRITES_ENABLED = 'true';
49 const dataDir = path.join(tmpRoot, 'hub');
50 fs.mkdirSync(dataDir, { recursive: true });
51
52 const emptyList = handlePathListRequest({ dataDir, vaultId: 'v', cliScopes: ['personal'] });
53 assert.deepEqual(emptyList.payload.paths, []);
54
55 const proposed = await handlePathProposeRequest({
56 dataDir,
57 vaultId: 'v',
58 cliScopes: ['personal'],
59 body: { title: 'E2E path', summary: 'Walkthrough', goal: 'Prove list/get', steps: sampleSteps() },
60 createProposal: (dir, input) => fakeCreateProposal(dir, input),
61 });
62 assert.equal(proposed.ok, true);
63
64 const proposals = JSON.parse(fs.readFileSync(path.join(dataDir, 'hub_proposals.json'), 'utf8'));
65 const row = proposals.find((p) => p.proposal_id === proposed.payload.proposal_id);
66 row.status = 'approved';
67 const applied = applyApprovedPathProposal(dataDir, row);
68 assert.equal(applied.ok, true);
69
70 const got = handlePathGetRequest({
71 dataDir,
72 vaultId: 'v',
73 pathId: proposed.payload.path_id,
74 cliScopes: ['personal'],
75 });
76 assert.equal(got.ok, true);
77 assert.equal(got.payload.path.title, 'E2E path');
78
79 const storePath = path.join(dataDir, 'hub_flow_store.json');
80 const localRaw = fs.readFileSync(storePath, 'utf8');
81 const blob = {
82 vaults: {
83 v: {
84 learning_paths: [
85 JSON.parse(localRaw).vaults.v.learning_paths[0],
86 {
87 ...JSON.parse(localRaw).vaults.v.learning_paths[0],
88 path_id: 'path_fromblob00000001',
89 title: 'From blob',
90 updated: '2026-08-19T00:00:00Z',
91 },
92 ],
93 },
94 },
95 };
96 const merged = mergeFlowStoreJson(localRaw, JSON.stringify(blob));
97 fs.writeFileSync(storePath, merged, 'utf8');
98 const afterHydrate = handlePathGetRequest({
99 dataDir,
100 vaultId: 'v',
101 pathId: 'path_fromblob00000001',
102 cliScopes: ['personal'],
103 });
104 assert.equal(afterHydrate.ok, true);
105 assert.equal(afterHydrate.payload.path.title, 'From blob');
106 });
107
108 it('hosted hook returns null for task/capture/media proposals', async () => {
109 const nullOutcome = await maybeApplyHostedPathAfterApprove({
110 method: 'GET',
111 pathOnly: '/api/v1/proposals/x/approve',
112 upstreamStatus: 200,
113 canisterUrl: 'http://canister.test',
114 bridgeUrl: 'http://bridge.test',
115 authorization: undefined,
116 vaultId: 'v',
117 effectiveUserId: 'u',
118 actorUserId: 'u',
119 canisterAuthHeaders: () => ({}),
120 });
121 assert.equal(nullOutcome, null);
122
123 assert.equal(
124 isPathProposalForHostedApply({
125 source: 'task',
126 review_queue: 'task-writes',
127 body: JSON.stringify({ proposal_kind: 'task_create' }),
128 }),
129 false,
130 );
131 assert.equal(
132 isPathProposalForHostedApply({
133 source: 'flow_capture',
134 review_queue: 'flow-capture',
135 body: JSON.stringify({ proposal_kind: 'promote' }),
136 }),
137 false,
138 );
139 assert.equal(
140 isPathProposalForHostedApply({
141 source: 'media',
142 review_queue: 'media',
143 body: JSON.stringify({ proposal_kind: 'media_attach' }),
144 }),
145 false,
146 );
147 assert.equal(
148 isPathProposalForHostedApply({
149 source: 'learning_path',
150 review_queue: 'learning-path',
151 body: JSON.stringify({ proposal_kind: 'path_create' }),
152 }),
153 true,
154 );
155 });
156
157 it('no Scooling file import from path modules', () => {
158 const root = getRepoRoot();
159 const libPath = path.join(root, 'lib/path');
160 for (const name of fs.readdirSync(libPath)) {
161 const src = fs.readFileSync(path.join(libPath, name), 'utf8');
162 assert.equal(src.includes('scooling/'), false, `${name} must not import Scooling`);
163 assert.equal(src.includes('writeNote'), false, `${name} must not call writeNote`);
164 }
165 });
166 });
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 9 days ago