88 lines
4.7 KiB
JavaScript
88 lines
4.7 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { request } from 'node:http';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { startServer } from '../src/serve.mjs';
|
|
import { startTesterSocket, TESTER_ORIGIN } from '../src/tester-socket.mjs';
|
|
import { close } from './fixture.mjs';
|
|
|
|
const path = '/api/chat/instances';
|
|
const preview = { host: 'chat.example', header: 'x-user', user: 'jason' };
|
|
const snapshot = {
|
|
revision: 'a'.repeat(64), plugins: { private: 'do-not-project' },
|
|
instances: {
|
|
assistant: { definition: 'assistant', vars: { model: 'private-model' } },
|
|
bean: { definition: 'companion', vars: { 'persona.name': 'Bean', 'persona.sha256': 'private-hash' } },
|
|
chet: { definition: 'continuity', vars: { 'persona.name': 'Bean', file: '/private/path' } },
|
|
pm: { definition: 'pm', vars: { 'persona.name': 'Hidden PM' } },
|
|
},
|
|
};
|
|
function get(server, { route = path, method = 'GET', headers = {} } = {}) {
|
|
const address = server.address(), unix = typeof address === 'string';
|
|
return new Promise((resolve, reject) => {
|
|
const req = request({ ...(unix ? { socketPath: address } : { host: '127.0.0.1', port: address.port }),
|
|
path: route, method, headers: { host: unix ? new URL(TESTER_ORIGIN).host : preview.host, ...headers } }, res => {
|
|
let text = '';
|
|
res.on('data', chunk => text += chunk);
|
|
res.on('end', () => resolve({ status: res.statusCode, text, value: JSON.parse(text) }));
|
|
});
|
|
req.on('error', reject); req.end();
|
|
});
|
|
}
|
|
test('C2-09a metadata uses the human listener bus, projects labels only and refuses unauthenticated reads', async t => {
|
|
let calls = 0;
|
|
const server = await startServer({ port: 0, preview, bus: { instances: async () => { calls++; return snapshot; } } });
|
|
t.after(() => close(server));
|
|
const refused = await get(server);
|
|
assert.equal(refused.status, 401);
|
|
assert.equal(calls, 0);
|
|
const answer = await get(server, { headers: { 'x-user': 'jason' } });
|
|
assert.equal(answer.status, 200);
|
|
assert.deepEqual(answer.value.rows, [{ instance: 'assistant', name: 'assistant' }, { instance: 'bean', name: 'Bean' }, { instance: 'chet', name: 'Bean' }]);
|
|
for (const row of answer.value.rows) assert.deepEqual(Object.keys(row).sort(), ['instance', 'name']);
|
|
assert.doesNotMatch(answer.text, /private|Hidden PM/);
|
|
assert.equal(calls, 1);
|
|
for (const options of [{ route: path + '?extra=1' }, { method: 'POST' }]) {
|
|
const result = await get(server, { ...options, headers: { 'x-user': 'jason' } });
|
|
assert.ok([400, 405].includes(result.status));
|
|
}
|
|
assert.equal(calls, 1, 'invalid metadata requests never call a bus');
|
|
});
|
|
test('C2-09a metadata uses the private tester listener own bus and no human transport', async t => {
|
|
const root = mkdtempSync(join(tmpdir(), 'chat219-')); t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
let reads = 0;
|
|
const server = await startTesterSocket({ dataRoot: root, board: 'http://127.0.0.1:9',
|
|
bus: { instances: async () => { reads++; return { instances: { quill: { definition: 'proofreader', vars: { 'persona.name': 'Quill' } } } }; } } });
|
|
t.after(() => close(server));
|
|
const answer = await get(server);
|
|
assert.equal(answer.status, 200);
|
|
assert.deepEqual(answer.value.rows, [{ instance: 'quill', name: 'Quill' }]);
|
|
assert.equal(reads, 1);
|
|
});
|
|
test('C2-09a metadata refuses missing or malformed reads and never echoes untrusted fields', async t => {
|
|
for (const bus of [null, { instances: async () => ({ instances: [] }) }, { instances: async () => { throw new Error('private-path'); } }]) {
|
|
const server = await startServer({ port: 0, preview, bus }); t.after(() => close(server));
|
|
const answer = await get(server, { headers: { 'x-user': 'jason' } });
|
|
assert.ok(answer.status >= 400);
|
|
assert.doesNotMatch(answer.text, /private-path/);
|
|
}
|
|
});
|
|
|
|
test('C2-09a metadata stays behind the SSO session check and a GET never refreshes session activity', async t => {
|
|
let session = null, reads = 0;
|
|
const touches = [];
|
|
const sso = { publicHost: 'sso-chat.example', publicOrigin: 'https://sso-chat.example', formTargets: () => [],
|
|
session: (_req, { touch }) => { touches.push(touch); return session; } };
|
|
const server = await startServer({ port: 0, sso, bus: { instances: async () => { reads++; return snapshot; } } });
|
|
t.after(() => close(server));
|
|
const headers = { host: sso.publicHost };
|
|
assert.equal((await get(server, { headers })).status, 401);
|
|
assert.equal(reads, 0);
|
|
session = { name: 'jason', rootOwner: true };
|
|
assert.equal((await get(server, { headers })).status, 200);
|
|
assert.equal(reads, 1);
|
|
assert.deepEqual(touches, [false, false]);
|
|
});
|