automation-ingest-unit.test.mjs
135 lines 5.0 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago
1 /**
2 * AIP-b unit: router, match, scopes, D3/D9/D10/D15.
3 */
4 import { describe, it } from 'node:test';
5 import assert from 'node:assert/strict';
6 import {
7 routeAutomationIngest,
8 isIngestContractBody,
9 normalizeIngestBody,
10 idempotencyKeyFromRequest,
11 normalizeRuleForSave,
12 listPackTemplates,
13 } from '../lib/automation-ingest-policy.mjs';
14 import {
15 agentScopesPermitMethod,
16 applyScopeCeiling,
17 DEFAULT_AGENT_SCOPES,
18 ALLOWED_AGENT_SCOPES,
19 } from '../hub/lib/agent-credential-core.mjs';
20
21 function rule(partial) {
22 return normalizeRuleForSave({
23 label: partial.label || 'r',
24 priority: partial.priority ?? 100,
25 disposition: partial.disposition || 'review_queue',
26 content_class: partial.content_class || 'research',
27 enabled: partial.enabled !== false,
28 match: partial.match || { path_prefix: 'inbox/trends/' },
29 rule_id: partial.rule_id,
30 });
31 }
32
33 describe('automation ingest unit', () => {
34 it('normalize + contract body', () => {
35 const n = normalizeIngestBody({
36 path: '/inbox/trends/a.md',
37 body: 'hello',
38 source_fingerprint: 'sha256:abcdef12',
39 content_class: 'research',
40 });
41 assert.equal(n.path, 'inbox/trends/a.md');
42 assert.equal(n.source, 'automation_ingest');
43 assert.equal(isIngestContractBody({ source_fingerprint: 'sha256:abcdef12', ingest: true }), true);
44 assert.equal(isIngestContractBody({ source_fingerprint: 'sha256:abcdef12', content_class: 'research' }), true);
45 assert.equal(isIngestContractBody({ source_fingerprint: 'sha256:abcdef12' }), false);
46 assert.equal(isIngestContractBody({ ingest: true }), false);
47 });
48
49 it('first-match by priority then rule_id', () => {
50 const a = rule({ rule_id: 'ingr_aaaaaaaaaaaaaaaa', priority: 50, label: 'a', disposition: 'direct_note' });
51 const b = rule({ rule_id: 'ingr_bbbbbbbbbbbbbbbb', priority: 10, label: 'b', disposition: 'review_queue' });
52 const routed = routeAutomationIngest(
53 { path: 'inbox/trends/x.md', body: 'x', content_class: 'research', credential_name: 'bot' },
54 [a, b]
55 );
56 assert.equal(routed.rule_id, b.rule_id);
57 assert.equal(routed.disposition, 'review_queue');
58 });
59
60 it('D3 empty match rejected on save', () => {
61 assert.throws(
62 () => normalizeRuleForSave({ label: 'empty', disposition: 'review_queue', match: {} }),
63 (e) => e.code === 'INGEST_RULE_MATCH_EMPTY'
64 );
65 });
66
67 it('D9 elevated override forces review_queue', () => {
68 const r = rule({
69 rule_id: 'ingr_cccccccccccccccc',
70 disposition: 'direct_note',
71 match: { path_prefix: 'legal/' },
72 });
73 const routed = routeAutomationIngest(
74 {
75 path: 'legal/secret.md',
76 body: 'reset password for admin',
77 content_class: 'ops',
78 triggers: {
79 literal_phrases: [{ match: 'reset password', review_severity: 'elevated' }],
80 path_prefixes: [],
81 label_any: [],
82 },
83 },
84 [r]
85 );
86 assert.equal(routed.disposition, 'review_queue');
87 assert.equal(routed.elevated_override, true);
88 });
89
90 it('D10 evaluation block rewrites auto-apply for agents', () => {
91 const r = rule({
92 rule_id: 'ingr_dddddddddddddddd',
93 disposition: 'proposal_auto_apply',
94 match: { path_prefix: 'inbox/trends/' },
95 });
96 const routed = routeAutomationIngest(
97 {
98 path: 'inbox/trends/x.md',
99 body: 'x',
100 content_class: 'research',
101 evaluationRequired: true,
102 sessionBound: false,
103 },
104 [r]
105 );
106 assert.equal(routed.disposition, 'review_queue');
107 assert.equal(routed.evaluation_block, true);
108 });
109
110 it('D15 idempotency key prefers header', () => {
111 assert.equal(idempotencyKeyFromRequest('headerkey12', 'fingerprint12'), 'headerkey12');
112 assert.equal(idempotencyKeyFromRequest(' ', 'fingerprint12'), 'fingerprint12');
113 });
114
115 it('scope allowlist: ingest path needs ingest:automation; propose cannot ingest; ingest cannot approve', () => {
116 assert.equal(ALLOWED_AGENT_SCOPES.includes('ingest:automation'), true);
117 assert.deepEqual([...DEFAULT_AGENT_SCOPES], ['propose', 'vault:read']);
118 const ingest = ['ingest:automation', 'vault:read'];
119 const propose = ['propose', 'vault:read'];
120 assert.equal(agentScopesPermitMethod(ingest, 'POST', 'api/v1/automation/ingest'), true);
121 assert.equal(agentScopesPermitMethod(propose, 'POST', 'api/v1/automation/ingest'), false);
122 assert.equal(agentScopesPermitMethod(ingest, 'POST', 'api/v1/proposals/abc/approve'), false);
123 assert.equal(agentScopesPermitMethod(ingest, 'GET', 'api/v1/automation/ingest-rules'), false);
124 assert.equal(agentScopesPermitMethod(['vault:write'], 'POST', 'api/v1/automation/ingest'), true);
125 const ceiling = applyScopeCeiling(['ingest:automation', 'vault:read'], ['vault:read']);
126 assert.ok(ceiling.includes('ingest:automation'));
127 assert.ok(!ceiling.includes('vault:write'));
128 });
129
130 it('pack templates exist and are disabled', () => {
131 const t = listPackTemplates();
132 assert.equal(t.length, 3);
133 assert.ok(t.every((x) => x.enabled === false));
134 });
135 });
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago