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: '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); });