// amerc platform test suite. Spins up the API on a temp DB and exercises every surface. // Run: node --experimental-sqlite server/platform-tests.mjs import { spawn } from 'node:child_process'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const PORT = 5310, BASE = `http://127.0.0.1:${PORT}/api`; const dir = mkdtempSync(join(tmpdir(), 'amerc-test-')); const srv = spawn(process.execPath, ['--experimental-sqlite', new URL('./amerc-api.mjs', import.meta.url).pathname], { env: { ...process.env, PORT: String(PORT), AMERC_DB: join(dir, 't.db'), AMERC_FILES: join(dir, 'disk'), AMERC_SECRET: 'test-secret' }, stdio: 'ignore', }); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); let pass = 0, fail = 0; const fails = []; function ok(cond, name) { if (cond) { pass++; } else { fail++; fails.push(name); console.log(' ✗ ' + name); } } function eq(a, b, name) { ok(JSON.stringify(a) === JSON.stringify(b), `${name} (got ${JSON.stringify(a)}, want ${JSON.stringify(b)})`); } let cookies = {}; function setCookie(res) { const sc = res.headers.get('set-cookie'); if (sc) { const [kv] = sc.split(';'); const [k, v] = kv.split('='); cookies[k] = v; } } const cookieHdr = () => Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; '); async function req(path, { method = 'GET', body, cookie = true, bearer, raw } = {}) { const headers = {}; if (body) headers['Content-Type'] = 'application/json'; if (cookie && cookieHdr()) headers.Cookie = cookieHdr(); if (bearer) headers.Authorization = `Bearer ${bearer}`; const res = await fetch(BASE + path, { method, headers, body: body ? JSON.stringify(body) : undefined }); setCookie(res); if (raw) return res; let j = {}; try { j = await res.json(); } catch {} return { status: res.status, ...j }; } async function main() { // wait for server for (let i = 0; i < 30; i++) { try { const r = await fetch(BASE + '/health'); if (r.ok) break; } catch {} await sleep(150); } console.log('\n== auth + SSO =='); let r = await req('/health'); ok(r.ok && r.version === 2, 'health v2'); r = await req('/auth/signup', { method: 'POST', body: { email: 'Admin@x.io', password: 'secret123', handle: 'admin' } }); ok(r.ok && r.user.role === 'admin', 'first signup -> admin'); eq(r.user.email, 'admin@x.io', 'email lowercased'); r = await req('/auth/me'); ok(r.user && r.user.handle === 'admin', 'me via cookie'); r = await req('/auth/verify', { raw: true }); ok(r.status === 200 && r.headers.get('x-amerc-user') === 'admin', 'SSO verify header'); r = await req('/auth/signup', { method: 'POST', body: { email: 'm@x.io', password: 'short' } }); ok(r.status === 400, 'reject short password'); const adminCookies = { ...cookies }; // member cookies = {}; r = await req('/auth/signup', { method: 'POST', body: { email: 'mercA@x.io', password: 'hunter22', handle: 'mercA' } }); ok(r.user.role === 'member', 'second signup -> member'); r = await req('/admin/users'); ok(r.status === 403, 'member blocked from admin'); r = await req('/auth/password', { method: 'POST', body: { currentPassword: 'WRONG', newPassword: 'newpass1' } }); ok(r.status === 401, 'pw change wrong current'); r = await req('/auth/password', { method: 'POST', body: { currentPassword: 'hunter22', newPassword: 'newpass1' } }); ok(r.ok, 'pw change ok'); r = await req('/auth/login', { method: 'POST', body: { email: 'mercA@x.io', password: 'newpass1' } }); ok(r.ok, 'login with new pw'); const memberCookies = { ...cookies }; console.log('== admin + agent keys =='); cookies = adminCookies; r = await req('/admin/users'); ok(r.ok && r.users.length === 2, 'admin lists users'); r = await req('/admin/keys', { method: 'POST', body: { name: 'fleet' } }); const agentKey = r.key; ok(r.ok && agentKey?.startsWith('amk_'), 'mint agent key'); cookies = {}; r = await req('/auth/me', { bearer: agentKey }); ok(r.agent === 'fleet', 'agent identity via Bearer'); console.log('== docs / netdisk / boards (agent + user) =='); cookies = {}; r = await req('/docs', { method: 'POST', bearer: agentKey, body: { title: 'Agent Doc', body: '# hi' } }); const docId = r.id; ok(r.ok, 'agent writes doc'); cookies = adminCookies; r = await req(`/docs/${docId}`); ok(r.doc.updated_by === 'agent:fleet', 'doc authored by agent'); r = await req('/files', { method: 'POST', body: { name: 'n.md', kind: 'text', text: 'hello', mime: 'text/markdown' } }); const fileId = r.id; ok(r.ok, 'create text file'); r = await req(`/files/${fileId}/raw`, { raw: true }); ok((await r.text()) === 'hello', 'download text file'); r = await req('/boards', { method: 'POST', body: { name: 'B', data: '{}', links: [{ fileId }] } }); ok(r.ok, 'create board'); cookies = {}; r = await req('/docs'); ok(r.status === 401, 'docs need auth'); console.log('== agent platform: classes =='); cookies = adminCookies; r = await req('/agents/classes', { method: 'POST', body: { name: 'Backend Coder', tags: ['backend', 'model'], description: 'api', visibility: 'public' } }); const classId = r.id; ok(r.ok && r.slug === 'backend-coder', 'publish class + slug'); await req('/agents/classes', { method: 'POST', body: { name: 'Frontend Pixie', tags: ['frontend'] } }); cookies = memberCookies; await req('/agents/classes', { method: 'POST', body: { name: 'Secret Bot', tags: ['x'], visibility: 'private' } }); cookies = {}; r = await req('/agents/classes'); ok(r.classes.length === 2, 'browse: only public classes anon'); r = await req('/agents/classes?tag=backend'); eq(r.classes.map((c) => c.name), ['Backend Coder'], 'filter by tag'); cookies = memberCookies; r = await req('/agents/classes?mine=1'); ok(r.classes.length === 1 && r.classes[0].name === 'Secret Bot', 'mine shows own private'); cookies = adminCookies; r = await req('/agents/classes'); ok(r.classes.length === 3, 'admin sees private too'); r = await req(`/agents/classes/${classId}/subscribe`, { method: 'POST' }); ok(r.token?.startsWith('sub_'), 'subscription token'); console.log('== agent platform: instances + lifecycle =='); cookies = adminCookies; r = await req('/agents/instances', { method: 'POST', body: { classId, visibility: 'public' } }); const instId = r.id, accesskey = r.accesskey; ok(r.ok && accesskey?.startsWith('ik_'), 'create instance + accesskey'); ok(r.relayWs.includes(accesskey), 'instance returns relay WS'); r = await req(`/agents/instances/${instId}`); eq(r.instance.status, 'pending', 'instance starts pending'); r = await req(`/agents/instances/${instId}/heartbeat`, { method: 'POST', cookie: false, body: { accesskey, broker: 'kebab', tui: 'booting' } }); eq(r.status, 'online', 'heartbeat -> online'); r = await req(`/agents/instances/${instId}`); eq(r.instance.status, 'online', 'instance online'); r = await req(`/agents/instances/${instId}/heartbeat`, { method: 'POST', cookie: false, body: { accesskey: 'WRONG' } }); ok(r.status === 401, 'bad accesskey rejected'); // stats reflect it r = await req('/agents/stats'); ok(r.classes === 2 && r.instances === 1 && r.online === 1, 'stats classes/instances/online'); // session r = await req(`/agents/instances/${instId}/sessions`, { method: 'POST', body: { connector: 'chatbox' } }); ok(r.ok && r.relayToken === accesskey && r.chatboxJs.includes(accesskey), 'start session -> relay token + chatbox js'); r = await req('/agents/stats'); ok(r.sessions === 1, 'stats sessions'); // private instance needs accesskey for a non-owner cookies = adminCookies; r = await req('/agents/instances', { method: 'POST', body: { classId, visibility: 'private' } }); const privId = r.id, privKey = r.accesskey; cookies = memberCookies; r = await req(`/agents/instances/${privId}`); ok(r.status === 403, 'private instance hidden from non-owner'); r = await req(`/agents/instances/${privId}?accesskey=${privKey}`); ok(r.ok, 'private instance visible with accesskey'); // shutdown cookies = adminCookies; r = await req(`/agents/instances/${instId}`, { method: 'DELETE', body: { accesskey } }); ok(r.ok, 'shutdown instance'); r = await req(`/agents/instances/${instId}`); eq(r.instance.status, 'down', 'instance down after shutdown'); console.log('== webagent broker login =='); cookies = {}; r = await req('/webagent/login', { method: 'POST', body: { username: 'admin@x.io', password: 'secret123' } }); ok(r.ok && r.token?.startsWith('wa_'), 'webagent login (amerc user)'); r = await req('/webagent/login', { method: 'POST', body: { username: 'admin@x.io', password: 'nope' } }); ok(r.status === 401, 'webagent login bad pw'); console.log(`\n${fail ? '❌' : '✅'} ${pass} passed, ${fail} failed`); if (fail) console.log('FAILED: ' + fails.join(' | ')); srv.kill(); rmSync(dir, { recursive: true, force: true }); process.exit(fail ? 1 : 0); } main().catch((e) => { console.error(e); srv.kill(); process.exit(1); });