muse-thin-bridge-audit.test.mjs
162 lines 5.3 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Audit-level tests for Muse thin bridge: env edge cases, proxy limits, path hardening, JSON shapes.
3 */
4 import { describe, it } from 'node:test';
5 import assert from 'node:assert/strict';
6 import {
7 parseMuseConfigFromEnv,
8 normalizeExternalRef,
9 resolveExternalRefForApprove,
10 isAllowedMuseProxyPath,
11 fetchMuseProxiedGet,
12 } from '../lib/muse-thin-bridge.mjs';
13
14 describe('muse-thin-bridge audit — env numeric hardening', () => {
15 it('parseMuseConfigFromEnv uses defaults when MUSE_LINEAGE_TIMEOUT_MS is non-numeric', () => {
16 const c = parseMuseConfigFromEnv({
17 MUSE_URL: 'https://muse.example.com',
18 MUSE_LINEAGE_TIMEOUT_MS: 'not-a-number',
19 });
20 assert.ok(c);
21 assert.strictEqual(c.lineageTimeoutMs, 5000);
22 });
23
24 it('parseMuseConfigFromEnv uses defaults when MUSE_PROXY_MAX_BYTES is non-numeric', () => {
25 const c = parseMuseConfigFromEnv({
26 MUSE_URL: 'https://muse.example.com',
27 MUSE_PROXY_MAX_BYTES: 'xyz',
28 });
29 assert.ok(c);
30 assert.strictEqual(c.proxyMaxBytes, 1024 * 1024);
31 });
32
33 it('parseMuseConfigFromEnv honors valid numeric overrides', () => {
34 const c = parseMuseConfigFromEnv({
35 MUSE_URL: 'https://m.example',
36 MUSE_LINEAGE_TIMEOUT_MS: '8000',
37 MUSE_PROXY_MAX_BYTES: '2048',
38 });
39 assert.ok(c);
40 assert.strictEqual(c.lineageTimeoutMs, 8000);
41 assert.strictEqual(c.proxyMaxBytes, 2048);
42 });
43
44 it('parseMuseConfigFromEnv clamps lineage timeout to 60s max', () => {
45 const c = parseMuseConfigFromEnv({
46 MUSE_URL: 'https://m.example',
47 MUSE_LINEAGE_TIMEOUT_MS: '999999',
48 });
49 assert.ok(c);
50 assert.strictEqual(c.lineageTimeoutMs, 60_000);
51 });
52 });
53
54 describe('muse-thin-bridge audit — proxy path hardening', () => {
55 const prefixes = ['/knowtation/v1/'];
56
57 it('rejects encoded path segments that decode to parent traversal', () => {
58 assert.strictEqual(isAllowedMuseProxyPath('/%2e%2e%2fetc/passwd', prefixes), false);
59 assert.strictEqual(isAllowedMuseProxyPath('/knowtation/v1/../secret', prefixes), false);
60 });
61
62 it('allows normal path under prefix', () => {
63 assert.strictEqual(isAllowedMuseProxyPath('/knowtation/v1/commits/abc', prefixes), true);
64 });
65 });
66
67 describe('muse-thin-bridge audit — fetchMuseProxiedGet', () => {
68 it('returns BAD_GATEWAY when response exceeds proxyMaxBytes', async () => {
69 const cfg = parseMuseConfigFromEnv({
70 MUSE_URL: 'https://upstream.test',
71 MUSE_PROXY_MAX_BYTES: '50',
72 });
73 assert.ok(cfg);
74 const big = Buffer.alloc(200, 0x61);
75 const fetchFn = async () =>
76 /** @type {any} */ ({
77 ok: true,
78 status: 200,
79 headers: { get: () => 'application/octet-stream' },
80 arrayBuffer: async () => big.buffer.slice(big.byteOffset, big.byteOffset + big.byteLength),
81 });
82 const r = await fetchMuseProxiedGet({
83 config: { ...cfg, proxyMaxBytes: 50 },
84 relativePath: '/knowtation/v1/blob',
85 fetchFn,
86 logWarn: () => {},
87 });
88 assert.strictEqual(r.ok, false);
89 assert.strictEqual(r.code, 'BAD_GATEWAY');
90 });
91
92 it('returns UPSTREAM with body when Muse returns 404', async () => {
93 const cfg = parseMuseConfigFromEnv({ MUSE_URL: 'https://upstream.test' });
94 assert.ok(cfg);
95 const errBody = Buffer.from('not found');
96 const fetchFn = async () =>
97 /** @type {any} */ ({
98 ok: false,
99 status: 404,
100 headers: { get: () => 'text/plain' },
101 arrayBuffer: async () => errBody.buffer.slice(errBody.byteOffset, errBody.byteOffset + errBody.byteLength),
102 });
103 const r = await fetchMuseProxiedGet({
104 config: cfg,
105 relativePath: '/knowtation/v1/missing',
106 fetchFn,
107 logWarn: () => {},
108 });
109 assert.strictEqual(r.ok, false);
110 assert.strictEqual(r.code, 'UPSTREAM');
111 assert.strictEqual(r.status, 404);
112 assert.ok(r.body && Buffer.compare(r.body, errBody) === 0);
113 });
114 });
115
116 describe('muse-thin-bridge audit — resolveExternalRef JSON shapes', () => {
117 it('ignores non-string external_ref in JSON response', async () => {
118 const fetchFn = async () =>
119 /** @type {any} */ ({
120 ok: true,
121 text: async () => '{"external_ref":999}',
122 });
123 const r = await resolveExternalRefForApprove({
124 clientRef: '',
125 proposalId: 'p1',
126 vaultId: 'default',
127 config: parseMuseConfigFromEnv({ MUSE_URL: 'https://m.example' }),
128 fetchFn,
129 logWarn: () => {},
130 });
131 assert.strictEqual(r, '');
132 });
133
134 it('normalizes oversized external_ref from Muse JSON to empty', async () => {
135 const huge = 'x'.repeat(600);
136 const fetchFn = async () =>
137 /** @type {any} */ ({
138 ok: true,
139 text: async () => JSON.stringify({ external_ref: huge }),
140 });
141 const r = await resolveExternalRefForApprove({
142 clientRef: '',
143 proposalId: 'p1',
144 vaultId: 'default',
145 config: parseMuseConfigFromEnv({ MUSE_URL: 'https://m.example' }),
146 fetchFn,
147 logWarn: () => {},
148 });
149 assert.strictEqual(r, '');
150 });
151 });
152
153 describe('muse-thin-bridge audit — normalizeExternalRef edge cases', () => {
154 it('rejects tab and DEL', () => {
155 assert.strictEqual(normalizeExternalRef('a\tb'), '');
156 assert.strictEqual(normalizeExternalRef('a\u007fb'), '');
157 });
158
159 it('allows common ref-safe punctuation', () => {
160 assert.strictEqual(normalizeExternalRef('commit:abc-123_456'), 'commit:abc-123_456');
161 });
162 });
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago