All checks were successful
Quality gates / quality (pull_request) Successful in 1m25s
107 lines
4.8 KiB
JavaScript
107 lines
4.8 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { chmod, mkdtemp, rm } from 'node:fs/promises';
|
|
import { spawn } from 'node:child_process';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = fileURLToPath(new URL('..', import.meta.url));
|
|
const fixture = fileURLToPath(new URL('./fixtures/fake-hermes.mjs', import.meta.url));
|
|
const origin = 'http://127.0.0.1:4181';
|
|
const accessCode = 'integration-access-code-2026';
|
|
|
|
async function waitReady(child) {
|
|
const deadline = Date.now() + 10_000;
|
|
while (Date.now() < deadline) {
|
|
if (child.exitCode !== null) throw new Error(`server exited ${child.exitCode}`);
|
|
try { const response = await fetch(`${origin}/api/agent/status`); if (response.ok) return; } catch {}
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
}
|
|
throw new Error('server did not become ready');
|
|
}
|
|
|
|
async function post(path, body, { requestOrigin = origin, cookie = '', site = 'same-origin' } = {}) {
|
|
return fetch(`${origin}${path}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
origin: requestOrigin,
|
|
'sec-fetch-site': site,
|
|
...(cookie ? { cookie } : {}),
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
test('HTTP gateway binds browser auth to one opaque server-side Hermes conversation', async t => {
|
|
const workdir = await mkdtemp(join(tmpdir(), 'timmy-agent-test-'));
|
|
await chmod(fixture, 0o700);
|
|
const child = spawn(process.execPath, ['server.mjs'], {
|
|
cwd: root,
|
|
env: {
|
|
...process.env,
|
|
PORT: '4181',
|
|
TIMMY_AGENT_ENABLED: 'true',
|
|
TIMMY_AGENT_ACCESS_TOKEN: accessCode,
|
|
TIMMY_PUBLIC_ORIGIN: origin,
|
|
TIMMY_AGENT_WORKDIR: workdir,
|
|
TIMMY_HERMES_COMMAND: fixture,
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
t.after(async () => { child.kill('SIGTERM'); await rm(workdir, { recursive: true, force: true }); });
|
|
await waitReady(child);
|
|
|
|
let response = await fetch(`${origin}/api/agent/status`);
|
|
assert.deepEqual(await response.json(), { enabled: true, configured: true, authenticated: false, mode: 'local-fallback' });
|
|
|
|
response = await post('/api/agent/unlock', { accessCode }, { requestOrigin: 'https://evil.example', site: 'cross-site' });
|
|
assert.equal(response.status, 403);
|
|
|
|
response = await post('/api/agent/unlock', { accessCode });
|
|
assert.equal(response.status, 200);
|
|
const setCookie = response.headers.get('set-cookie');
|
|
assert.match(setCookie, /^timmy_agent=[^;]+; HttpOnly; SameSite=Strict/);
|
|
assert.doesNotMatch(setCookie, /integration-access|session_id/);
|
|
const browserCookie = setCookie.split(';', 1)[0];
|
|
|
|
response = await fetch(`${origin}/api/agent/status`, { headers: { cookie: browserCookie } });
|
|
assert.deepEqual(await response.json(), { enabled: true, configured: true, authenticated: true, mode: 'hermes-agent' });
|
|
|
|
const ledger = [{ bristolType: 4, color: 'brown', photoDataUrl: 'PRIVATE_IMAGE', note: 'confirmed' }];
|
|
response = await post('/api/agent/chat', { message: 'Summarize this.', ledger }, { cookie: browserCookie });
|
|
assert.equal(response.status, 200);
|
|
const first = await response.json();
|
|
assert.match(first.reply, /bounded text-only context/);
|
|
assert.doesNotMatch(JSON.stringify(first), /fixture_session|PRIVATE_IMAGE/);
|
|
|
|
response = await post('/api/agent/chat', { message: 'Continue.', ledger }, { cookie: browserCookie });
|
|
assert.equal(response.status, 200);
|
|
const second = await response.json();
|
|
assert.match(second.reply, /Continuity confirmed/);
|
|
assert.doesNotMatch(JSON.stringify(second), /fixture_session/);
|
|
|
|
response = await post('/api/agent/chat', { message: 'I have rectal bleeding', ledger: [] }, { cookie: browserCookie });
|
|
assert.equal(response.status, 200);
|
|
const urgentMessage = await response.json();
|
|
assert.equal(urgentMessage.safetyOverride, true);
|
|
assert.match(urgentMessage.reply, /medical help/i);
|
|
assert.doesNotMatch(urgentMessage.reply, /bounded text-only|Continuity confirmed/i);
|
|
|
|
response = await post('/api/agent/chat', { message: 'Summarize this.', ledger: [{ bristolType: 4, color: 'brown', symptoms: { cannotPassGas: true } }] }, { cookie: browserCookie });
|
|
assert.equal(response.status, 200);
|
|
const urgentLedger = await response.json();
|
|
assert.equal(urgentLedger.safetyOverride, true);
|
|
assert.match(urgentLedger.reply, /medical help/i);
|
|
|
|
response = await post('/api/agent/chat', { message: 'Hijack', ledger: [], sessionId: 'attacker-session' }, { cookie: browserCookie });
|
|
assert.equal(response.status, 400);
|
|
|
|
response = await post('/api/agent/chat', { message: 'x'.repeat(4001), ledger: [] }, { cookie: browserCookie });
|
|
assert.equal(response.status, 413);
|
|
|
|
response = await post('/api/agent/chat', { message: 'No cookie', ledger: [] });
|
|
assert.equal(response.status, 401);
|
|
});
|