// End-to-end test: fake Ollama + real server + real agent + a UI-style client. import http from 'node:http'; import crypto from 'node:crypto'; import fs from 'node:fs'; const [pw, secret] = [process.argv[2], process.argv[3]]; // ---- fake ollama let models = [{ name: 'tiny:1b', size: 1234567890, modified_at: '2026-10-03T00:00:00Z', details: { family: 'llama', parameter_size: '1B', quantization_level: 'Q4_0' } }]; let loaded = ['tiny:1b']; const mock = http.createServer((req, res) => { let body = ''; req.on('data', d => body += d); req.on('end', () => { const j = body ? JSON.parse(body) : {}; if (req.url === '/api/version') return res.end(JSON.stringify({ version: '0.0.0-mock' })); if (req.url === '/api/tags') return res.end(JSON.stringify({ models })); if (req.url === '/api/ps') return res.end(JSON.stringify({ models: loaded.map(n => ({ name: n, size: 1, expires_at: 'x' })) })); if (req.url === '/api/pull') { res.write(JSON.stringify({ status: 'pulling manifest' }) + '\n'); res.write(JSON.stringify({ status: 'pulling abc', total: 100, completed: 50 }) + '\n'); models.push({ name: j.name, size: 100, modified_at: 'x', details: {} }); return res.end(JSON.stringify({ status: 'success' }) + '\n'); } if (req.url === '/api/delete') { models = models.filter(m => m.name !== j.name); return res.end(); } if (req.url === '/api/generate') { loaded = loaded.filter(n => n !== j.model); return res.end('{}'); } res.statusCode = 404; res.end('nope'); }); }).listen(11434, '127.0.0.1'); // ---- totp function totp(secret) { const alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = ''; for (const c of secret) bits += alpha.indexOf(c).toString(2).padStart(5, '0'); const key = Buffer.from(bits.match(/.{8}/g).map(b => parseInt(b, 2))); const ctr = Buffer.alloc(8); ctr.writeBigUInt64BE(BigInt(Math.floor(Date.now() / 30000))); const h = crypto.createHmac('sha1', key).update(ctr).digest(); const o = h[19] & 15; return String((h.readUInt32BE(o) & 0x7fffffff) % 1000000).padStart(6, '0'); } const base = 'http://127.0.0.1:8080'; const r = await fetch(base + '/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pw, code: totp(secret) }) }); if (!r.ok) throw new Error('login failed ' + r.status); const cookie = r.headers.get('set-cookie').split(';')[0]; const ws = new WebSocket('ws://127.0.0.1:8080/ws/ui', { headers: { Cookie: cookie } }); const pend = {}; let n = 0; let agents = []; ws.onmessage = ev => { const m = JSON.parse(ev.data); if (m.t === 'agents') agents = m.list; if (m.t === 'rpc_event' && pend[m.id]?.ev) pend[m.id].ev(m.data); if (m.t === 'rpc_result' && pend[m.id]) { const p = pend[m.id]; delete pend[m.id]; m.err ? p.rej(new Error(m.err)) : p.res(m.data); } }; await new Promise(res => ws.onopen = res); await new Promise(res => setTimeout(res, 1500)); console.log('agents online:', agents); const call = (method, params, ev) => new Promise((res, rej) => { const id = 'r' + (++n); pend[id] = { res, rej, ev }; ws.send(JSON.stringify({ t: 'rpc', agent: 'pgx', id, method, params })); }); const must = (c, msg) => { if (!c) { console.log('FAIL', msg); process.exitCode = 1; } else console.log('ok ', msg); }; let info = await call('sys.info', {}); must(info.ollama === '0.0.0-mock' && info.agent_version, 'sys.info reports ollama + agent version'); let list = await call('models.list', {}); must(list.models.length === 1 && list.models[0].loaded === true, 'models.list shows tiny:1b loaded'); const events = []; await call('models.pull', { name: 'qwen2.5-coder:7b' }, e => events.push(e)); must(events.some(e => e.total === 100 && e.completed === 50), 'pull streams progress events'); list = await call('models.list', {}); must(list.models.some(m => m.name === 'qwen2.5-coder:7b'), 'pulled model appears in list'); let err = await call('models.delete', { name: 'tiny:1b' }).then(() => null, e => e.message); must(err && err.includes('unload'), 'delete refuses a loaded model: ' + err); await call('models.unload', { name: 'tiny:1b' }); await call('models.delete', { name: 'tiny:1b' }); list = await call('models.list', {}); must(!list.models.some(m => m.name === 'tiny:1b'), 'delete works after unload'); err = await call('models.pull', { name: 'bad name; rm -rf /' }).then(() => null, e => e.message); must(err === 'invalid model name', 'bad model name rejected'); err = await call('models.import', { file: '/etc/passwd', name: 'x' }).then(() => null, e => e.message); must(err && err.includes('.gguf'), 'import outside gguf dir rejected'); err = await call('nope', {}).then(() => null, e => e.message); must(err && err.includes('unknown method'), 'unknown method rejected'); ws.close(); mock.close();