finish-complete-apply-kn-b.test.mjs
514 lines 18.3 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago
1 /**
2 * FINISH-COMPLETE-APPLY-KN-b — seven-tier coverage (§FCA.7).
3 * Frozen: ~/scooling/docs/FINISH-COMPLETE-APPLY-CONTRACT.md
4 *
5 * Tiers: unit · integration · e2e · stress · data-integrity · performance · security
6 */
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 { performance } from 'node:perf_hooks';
13 import { fileURLToPath } from 'node:url';
14 import {
15 personalSelfApplyRefusalReason,
16 isPersonalSelfApplyClass,
17 matchesScoolingTaskFingerprint,
18 matchesScoolingMediaFingerprint,
19 isAdmittedSeamSelfApplyFingerprint,
20 applyPersonalSelfApplyEvaluationE1,
21 SCOOLING_TASK_EXTERNAL_REF_RE,
22 SCOOLING_MEDIA_EXTERNAL_REF_RE,
23 SCOOLING_REVIEW_TRAY_INTENT,
24 } from '../lib/hub-proposal-personal-self-apply.mjs';
25 import {
26 resolveOptionalScoolingExternalRef,
27 readProposeExternalRefRaw,
28 } from '../lib/scooling-external-ref.mjs';
29 import { TASK_PROPOSAL_SOURCE, handleTaskProposeRequest } from '../lib/task/task-write.mjs';
30 import { MEDIA_PROPOSAL_SOURCE, handleMediaLinkProposeRequest } from '../lib/attachments/attachment-write.mjs';
31 import { DELEGATION_PROPOSAL_SOURCE } from '../lib/agent/delegation.mjs';
32 import { FLOW_PROPOSAL_SOURCE } from '../lib/flow/flow-authoring.mjs';
33 import { createProposal, getProposal } from '../hub/proposals-store.mjs';
34 import { stripClientEvaluationFields } from '../lib/hub-proposal-create-augment.mjs';
35 import { sampleTaskCreatePayload } from './fixtures/task/write-helpers.mjs';
36 import {
37 buildMediaWriteFixture,
38 grantActiveConsent,
39 sampleLinkProposeBody,
40 } from './fixtures/media/write-helpers.mjs';
41 import { FM_PROPOSAL_SOURCE, FM_TASK_PROPOSAL_KIND } from '../lib/task/task-hosted-proposal.mjs';
42
43 const __dirname = path.dirname(fileURLToPath(import.meta.url));
44 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-finish-complete-apply-kn-b');
45
46 const ACTOR = 'google:learner-a';
47 const OTHER = 'google:learner-b';
48
49 /**
50 * Enable media external-link propose for FCA fixtures (§FCA.7 media rows).
51 * @param {string} dataDir
52 */
53 function enableMediaExternalLinkWrites(dataDir) {
54 fs.mkdirSync(dataDir, { recursive: true });
55 fs.writeFileSync(
56 path.join(dataDir, 'hub_media_write_policy.json'),
57 JSON.stringify({ media_external_link_enabled: true, media_attach_enabled: true }),
58 'utf8',
59 );
60 }
61
62 function taskProposal(overrides = {}) {
63 const proposalId = overrides.proposal_id || 'prop-task-1';
64 const bodyObj = overrides.bodyObj || {
65 proposal_kind: 'task_create',
66 task: {
67 schema: 'knowtation.task/v0',
68 task_id: 'task_fca_1',
69 kind: 'personal',
70 scope: 'personal',
71 status: 'pending',
72 title: 'FCA',
73 workspace_id: 'ws',
74 },
75 };
76 return {
77 proposal_id: proposalId,
78 status: 'proposed',
79 source: TASK_PROPOSAL_SOURCE,
80 path: `meta/tasks/proposals/${proposalId}.json`,
81 external_ref: 'scooling.task:fixture-001',
82 body: JSON.stringify(bodyObj),
83 frontmatter: {
84 [FM_PROPOSAL_SOURCE]: TASK_PROPOSAL_SOURCE,
85 [FM_TASK_PROPOSAL_KIND]: bodyObj.proposal_kind,
86 },
87 task_meta: {
88 record_kind: 'task',
89 proposal_kind: bodyObj.proposal_kind,
90 task_id: bodyObj.task?.task_id ?? null,
91 },
92 ...overrides,
93 };
94 }
95
96 function mediaProposal(overrides = {}) {
97 const proposalId = overrides.proposal_id || 'prop-media-1';
98 const bodyObj = overrides.bodyObj || {
99 proposal_kind: 'media_external_link',
100 scope: 'personal',
101 connector_id: 'ext_drive',
102 opaque_ref: 'file:abc',
103 consent_id: 'consent_1',
104 attachment_id: 'att_1',
105 };
106 return {
107 proposal_id: proposalId,
108 status: 'proposed',
109 source: MEDIA_PROPOSAL_SOURCE,
110 path: `meta/media/proposals/${proposalId}.json`,
111 external_ref: 'scooling.media:fixture-001',
112 body: JSON.stringify(bodyObj),
113 media_meta: { proposal_kind: bodyObj.proposal_kind, record_kind: 'media_external_link' },
114 ...overrides,
115 };
116 }
117
118 function eligible(proposal, extra = {}) {
119 return {
120 proposal,
121 hasVaultWrite: true,
122 partitionOwned: true,
123 role: 'member',
124 humanActor: true,
125 tokenType: null,
126 actorKind: 'human',
127 sessionBound: true,
128 authorActorId: ACTOR,
129 approverActorId: ACTOR,
130 ...extra,
131 };
132 }
133
134 describe('FINISH-COMPLETE-APPLY-KN-b unit — admission predicate §FCA.4', () => {
135 it('admits personal task_create + media_external_link fingerprints', () => {
136 assert.equal(matchesScoolingTaskFingerprint(taskProposal()), true);
137 assert.equal(matchesScoolingMediaFingerprint(mediaProposal()), true);
138 assert.equal(personalSelfApplyRefusalReason(eligible(taskProposal())), null);
139 assert.equal(personalSelfApplyRefusalReason(eligible(mediaProposal())), null);
140 });
141
142 it('refuses Delegation unconditionally; Flow not admitted', () => {
143 assert.equal(
144 personalSelfApplyRefusalReason(
145 eligible({ status: 'proposed', source: DELEGATION_PROPOSAL_SOURCE }),
146 ),
147 'SELF_APPLY_DELEGATION_REFUSED',
148 );
149 assert.equal(
150 personalSelfApplyRefusalReason(
151 eligible({
152 status: 'proposed',
153 source: FLOW_PROPOSAL_SOURCE,
154 path: 'meta/flows/x.json',
155 external_ref: 'scooling.task:x',
156 }),
157 ),
158 'SELF_APPLY_NOT_ADMITTED',
159 );
160 });
161
162 it('empty author / mcp_access / project scope / other-assignee refuse', () => {
163 assert.equal(
164 personalSelfApplyRefusalReason(eligible(taskProposal(), { authorActorId: '' })),
165 'SELF_APPLY_AUTHOR_UNVERIFIED',
166 );
167 assert.equal(
168 personalSelfApplyRefusalReason(eligible(taskProposal(), { tokenType: 'mcp_access' })),
169 'ROLE_NOT_ELIGIBLE',
170 );
171 const project = taskProposal({
172 bodyObj: {
173 proposal_kind: 'task_create',
174 task: { task_id: 't', kind: 'personal', scope: 'project', status: 'pending', title: 'P', workspace_id: 'ws' },
175 },
176 });
177 assert.equal(personalSelfApplyRefusalReason(eligible(project)), 'SELF_APPLY_NOT_ADMITTED');
178 const assignOther = taskProposal({
179 bodyObj: {
180 proposal_kind: 'task_assign',
181 task_id: 't1',
182 scope: 'personal',
183 assignee_ref: OTHER,
184 },
185 });
186 assert.equal(isAdmittedSeamSelfApplyFingerprint(assignOther, ACTOR), false);
187 assert.equal(personalSelfApplyRefusalReason(eligible(assignOther)), 'SELF_APPLY_NOT_ADMITTED');
188 const assignSelf = taskProposal({
189 bodyObj: {
190 proposal_kind: 'task_assign',
191 task_id: 't1',
192 scope: 'personal',
193 assignee_ref: ACTOR,
194 },
195 });
196 assert.equal(personalSelfApplyRefusalReason(eligible(assignSelf)), null);
197 });
198
199 it('pending path slug is not admitted; rewritten path is', () => {
200 const pending = taskProposal({ path: 'meta/tasks/proposals/pending.json', proposal_id: 'prop-x' });
201 assert.equal(matchesScoolingTaskFingerprint(pending), false);
202 assert.equal(matchesScoolingTaskFingerprint(pending, { allowPendingPath: true }), true);
203 });
204
205 it('external_ref helpers validate regex; malformed refuse', () => {
206 assert.equal(SCOOLING_TASK_EXTERNAL_REF_RE.test('scooling.task:ok'), true);
207 assert.equal(SCOOLING_MEDIA_EXTERNAL_REF_RE.test('scooling.media:ok'), true);
208 const bad = resolveOptionalScoolingExternalRef('scooling.review:x', SCOOLING_TASK_EXTERNAL_REF_RE);
209 assert.equal(bad.ok, false);
210 assert.equal(bad.status, 400);
211 assert.equal(readProposeExternalRefRaw({ body: { external_ref: 'scooling.task:a' } }), 'scooling.task:a');
212 });
213 });
214
215 describe('FINISH-COMPLETE-APPLY-KN-b integration — propose persist + approve class', () => {
216 const dataDir = path.join(tmpRoot, 'integ');
217 beforeEach(() => {
218 fs.rmSync(tmpRoot, { recursive: true, force: true });
219 fs.mkdirSync(dataDir, { recursive: true });
220 process.env.TASK_WRITES_ENABLED = '1';
221 });
222 afterEach(() => {
223 delete process.env.TASK_WRITES_ENABLED;
224 });
225
226 it('persists external_ref on task propose; session-bound class holds after path rewrite', async () => {
227 const proposed = await handleTaskProposeRequest({
228 dataDir,
229 vaultId: 'default',
230 userId: ACTOR,
231 role: 'editor',
232 proposalKind: 'task_create',
233 intent: 'create personal task',
234 sessionBound: true,
235 body: {
236 ...sampleTaskCreatePayload(),
237 task: { ...sampleTaskCreatePayload().task, task_id: 'task_fca_persist' },
238 external_ref: 'scooling.task:persist-1',
239 },
240 createProposal,
241 });
242 assert.equal(proposed.ok, true);
243 const row = getProposal(dataDir, proposed.payload.proposal_id);
244 assert.equal(row.external_ref, 'scooling.task:persist-1');
245 assert.match(row.path, /^meta\/tasks\/proposals\/.+\.json$/);
246 assert.notEqual(row.path, 'meta/tasks/proposals/pending.json');
247 assert.equal(
248 personalSelfApplyRefusalReason(
249 eligible(
250 {
251 ...row,
252 task_meta: row.task_meta,
253 },
254 { role: 'editor' },
255 ),
256 ),
257 null,
258 );
259 });
260
261 it('absent external_ref proposes ok but not admitted; malformed → 400', async () => {
262 const noRef = await handleTaskProposeRequest({
263 dataDir,
264 vaultId: 'default',
265 userId: ACTOR,
266 role: 'editor',
267 proposalKind: 'task_create',
268 intent: 'create',
269 body: { ...sampleTaskCreatePayload(), task: { ...sampleTaskCreatePayload().task, task_id: 'task_fca_noref' } },
270 createProposal,
271 });
272 assert.equal(noRef.ok, true);
273 const row = getProposal(dataDir, noRef.payload.proposal_id);
274 assert.equal(
275 personalSelfApplyRefusalReason(eligible({ ...row, source: TASK_PROPOSAL_SOURCE })),
276 'SELF_APPLY_NOT_ADMITTED',
277 );
278
279 const bad = await handleTaskProposeRequest({
280 dataDir,
281 vaultId: 'default',
282 userId: ACTOR,
283 role: 'editor',
284 proposalKind: 'task_create',
285 intent: 'create',
286 body: { ...{ ...sampleTaskCreatePayload(), task: { ...sampleTaskCreatePayload().task, task_id: 'task_fca_badref' } }, external_ref: 'nope' },
287 createProposal,
288 });
289 assert.equal(bad.ok, false);
290 assert.equal(bad.status, 400);
291 assert.equal(bad.code, 'EXTERNAL_REF_INVALID');
292 });
293
294 it('elevated task fingerprint does not self-apply', () => {
295 const elevated = taskProposal({ review_severity: 'elevated' });
296 assert.equal(personalSelfApplyRefusalReason(eligible(elevated)), 'ELEVATED_OR_AUTO_FLAGGED');
297 });
298
299 it('persists external_ref on media_external_link propose; class holds after path rewrite', async () => {
300 const fx = buildMediaWriteFixture(path.join(tmpRoot, 'media-integ'));
301 enableMediaExternalLinkWrites(fx.dataDir);
302 const consentId = grantActiveConsent(fx.dataDir, fx.vaultId, 'gdrive');
303 const proposed = await handleMediaLinkProposeRequest({
304 dataDir: fx.dataDir,
305 vaultId: fx.vaultId,
306 userId: ACTOR,
307 cliScopes: ['personal', 'project', 'org'],
308 intent: 'link personal media',
309 sessionBound: true,
310 body: {
311 ...sampleLinkProposeBody({ consent_id: consentId }),
312 external_ref: 'scooling.media:persist-1',
313 },
314 createProposal,
315 });
316 assert.equal(proposed.ok, true);
317 const row = getProposal(fx.dataDir, proposed.payload.proposal_id);
318 assert.equal(row.external_ref, 'scooling.media:persist-1');
319 assert.match(row.path, /^meta\/media\/proposals\/.+\.json$/);
320 assert.notEqual(row.path, 'meta/media/proposals/pending.json');
321 assert.equal(
322 personalSelfApplyRefusalReason(
323 eligible(
324 {
325 ...row,
326 media_meta: row.media_meta,
327 },
328 { role: 'editor' },
329 ),
330 ),
331 null,
332 );
333 });
334 });
335
336 describe('FINISH-COMPLETE-APPLY-KN-b e2e — personal task_create + media_external_link without Hub eval hop', () => {
337 const dataDir = path.join(tmpRoot, 'e2e');
338 beforeEach(() => {
339 fs.rmSync(tmpRoot, { recursive: true, force: true });
340 fs.mkdirSync(dataDir, { recursive: true });
341 process.env.TASK_WRITES_ENABLED = '1';
342 });
343 afterEach(() => {
344 delete process.env.TASK_WRITES_ENABLED;
345 });
346
347 it('create+E1+class holds for session-bound personal task', async () => {
348 const proposed = await handleTaskProposeRequest({
349 dataDir,
350 vaultId: 'default',
351 userId: ACTOR,
352 role: 'editor',
353 proposalKind: 'task_create',
354 intent: 'e2e',
355 sessionBound: true,
356 body: {
357 ...{ ...sampleTaskCreatePayload(), task: { ...sampleTaskCreatePayload().task, task_id: 'task_fca_e2e' } },
358 external_ref: 'scooling.task:e2e-1',
359 },
360 createProposal,
361 });
362 assert.equal(proposed.ok, true);
363 const row = getProposal(dataDir, proposed.payload.proposal_id);
364 assert.equal(row.evaluation_status, 'passed');
365 assert.equal(row.evaluated_by, ACTOR);
366 assert.equal(isPersonalSelfApplyClass(eligible(row, { role: 'editor' })), true);
367 });
368
369 it('create+E1+class holds for session-bound personal media_external_link', async () => {
370 const fx = buildMediaWriteFixture(path.join(tmpRoot, 'e2e-media'));
371 enableMediaExternalLinkWrites(fx.dataDir);
372 const consentId = grantActiveConsent(fx.dataDir, fx.vaultId, 'gdrive');
373 const proposed = await handleMediaLinkProposeRequest({
374 dataDir: fx.dataDir,
375 vaultId: fx.vaultId,
376 userId: ACTOR,
377 cliScopes: ['personal', 'project', 'org'],
378 intent: 'e2e media',
379 sessionBound: true,
380 body: {
381 ...sampleLinkProposeBody({ consent_id: consentId }),
382 external_ref: 'scooling.media:e2e-1',
383 },
384 createProposal,
385 });
386 assert.equal(proposed.ok, true);
387 const row = getProposal(fx.dataDir, proposed.payload.proposal_id);
388 assert.equal(row.evaluation_status, 'passed');
389 assert.equal(row.evaluated_by, ACTOR);
390 assert.equal(isPersonalSelfApplyClass(eligible(row, { role: 'editor' })), true);
391 });
392 });
393
394 describe('FINISH-COMPLETE-APPLY-KN-b stress — N cycles no cross-user leakage', () => {
395 it('distinct users never share admission', () => {
396 for (let i = 0; i < 100; i++) {
397 const a = taskProposal({
398 proposal_id: `prop-a-${i}`,
399 external_ref: `scooling.task:a-${i}`,
400 });
401 const b = taskProposal({
402 proposal_id: `prop-b-${i}`,
403 external_ref: `scooling.task:b-${i}`,
404 });
405 assert.equal(personalSelfApplyRefusalReason(eligible(a, { authorActorId: ACTOR, approverActorId: ACTOR })), null);
406 assert.equal(
407 personalSelfApplyRefusalReason(eligible(b, { authorActorId: ACTOR, approverActorId: OTHER })),
408 'SELF_APPLY_AUTHOR_MISMATCH',
409 );
410 }
411 });
412 });
413
414 describe('FINISH-COMPLETE-APPLY-KN-b data-integrity — P2 strip + server evaluation', () => {
415 it('client evaluation_status stripped; E1 sets server audit', () => {
416 const stripped = stripClientEvaluationFields({
417 evaluation_status: 'passed',
418 evaluated_by: 'forged',
419 evaluated_at: '2099-01-01T00:00:00.000Z',
420 path: 'meta/tasks/proposals/pending.json',
421 source: TASK_PROPOSAL_SOURCE,
422 external_ref: 'scooling.task:di-1',
423 body: JSON.stringify({
424 proposal_kind: 'task_create',
425 task: { scope: 'personal', task_id: 't', kind: 'personal', status: 'pending', title: 'x', workspace_id: 'w' },
426 }),
427 frontmatter: { [FM_PROPOSAL_SOURCE]: 'task', [FM_TASK_PROPOSAL_KIND]: 'task_create' },
428 });
429 assert.equal(stripped.evaluation_status, undefined);
430 const e1 = applyPersonalSelfApplyEvaluationE1(stripped, {
431 evaluatedBy: ACTOR,
432 sessionBound: true,
433 authorActorId: ACTOR,
434 evaluatedAt: '2026-07-27T00:00:00.000Z',
435 });
436 assert.equal(e1.evaluation_status, 'passed');
437 assert.equal(e1.evaluated_by, ACTOR);
438 assert.equal(e1.evaluated_at, '2026-07-27T00:00:00.000Z');
439 });
440 });
441
442 describe('FINISH-COMPLETE-APPLY-KN-b performance — E1 is pure and bounded', () => {
443 it('1000 E1 stamps stay under budget', () => {
444 const body = {
445 path: 'meta/tasks/proposals/pending.json',
446 source: TASK_PROPOSAL_SOURCE,
447 external_ref: 'scooling.task:perf',
448 body: JSON.stringify({
449 proposal_kind: 'task_create',
450 task: { scope: 'personal', task_id: 't', kind: 'personal', status: 'pending', title: 'x', workspace_id: 'w' },
451 }),
452 frontmatter: { [FM_PROPOSAL_SOURCE]: 'task', [FM_TASK_PROPOSAL_KIND]: 'task_create' },
453 };
454 const t0 = performance.now();
455 for (let i = 0; i < 1000; i++) {
456 applyPersonalSelfApplyEvaluationE1(body, {
457 evaluatedBy: ACTOR,
458 sessionBound: true,
459 authorActorId: ACTOR,
460 });
461 }
462 const ms = performance.now() - t0;
463 assert.ok(ms < 500, `E1 1000 cycles took ${ms}ms`);
464 });
465 });
466
467 describe('FINISH-COMPLETE-APPLY-KN-b security — IDOR / credential / forge', () => {
468 it('IDOR: foreign partition never admits; shared/service/legacy_session cannot admit; forged evaluation ignored on E1 refuse', () => {
469 assert.equal(
470 personalSelfApplyRefusalReason(eligible(taskProposal(), { partitionOwned: false })),
471 'NOT_PARTITION_OWNED',
472 );
473 assert.equal(
474 personalSelfApplyRefusalReason(eligible(mediaProposal(), { partitionOwned: false })),
475 'NOT_PARTITION_OWNED',
476 );
477 assert.equal(
478 personalSelfApplyRefusalReason(eligible(taskProposal(), { sessionBound: false })),
479 'SELF_APPLY_SESSION_BINDING_REQUIRED',
480 );
481 assert.equal(
482 personalSelfApplyRefusalReason(eligible(taskProposal(), { authorActorId: ACTOR, approverActorId: OTHER })),
483 'SELF_APPLY_AUTHOR_MISMATCH',
484 );
485 assert.equal(
486 personalSelfApplyRefusalReason(
487 eligible({ status: 'proposed', source: DELEGATION_PROPOSAL_SOURCE }, { sessionBound: true }),
488 ),
489 'SELF_APPLY_DELEGATION_REFUSED',
490 );
491 const forged = applyPersonalSelfApplyEvaluationE1(
492 {
493 ...taskProposal({ path: 'meta/tasks/proposals/pending.json' }),
494 evaluation_status: 'passed',
495 evaluated_by: 'attacker',
496 },
497 { sessionBound: false, evaluatedBy: ACTOR },
498 );
499 // sessionBound false → E1 does not stamp; forged fields remain on copy unless elevated clear
500 assert.notEqual(forged.evaluated_by, ACTOR);
501 });
502
503 it('notes tray still eligible; wrong-prefix task ref not admitted', () => {
504 const notes = {
505 status: 'proposed',
506 intent: SCOOLING_REVIEW_TRAY_INTENT,
507 external_ref: 'scooling.review:ok',
508 path: 'reviewed/ok.md',
509 };
510 assert.equal(personalSelfApplyRefusalReason(eligible(notes)), null);
511 const wrongPrefix = taskProposal({ external_ref: 'scooling.review:nope' });
512 assert.equal(personalSelfApplyRefusalReason(eligible(wrongPrefix)), 'SELF_APPLY_NOT_ADMITTED');
513 });
514 });
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago