Mercurial
diff mrjunejune/test/conversation_api_test.js @ 260:1f9877b637e9
Add Copilot-powered cyberpunk JRPG chat
Integrate the production JRPG chat with Seobeo streaming, Deita persistence, and a Bazel-managed Copilot SDK and LiteLLM inference stack.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <mrjunejune@users.noreply.github.com> |
|---|---|
| date | Wed, 05 Aug 2026 09:19:41 -0700 |
| parents | |
| children | b401627fc49e |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/test/conversation_api_test.js Wed Aug 05 09:19:41 2026 -0700 @@ -0,0 +1,181 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const net = require('node:net'); +const os = require('node:os'); +const path = require('node:path'); +const { spawn } = require('node:child_process'); + +const RUNFILES = process.env.JS_BINARY__RUNFILES; +const WORKSPACE = process.env.JS_BINARY__WORKSPACE; +const runfilesWorkspace = path.join(RUNFILES, WORKSPACE); + +function findFreePort() { + return new Promise((resolve, reject) => { + const socket = net.createServer(); + socket.once('error', reject); + socket.listen(0, '127.0.0.1', () => { + const address = socket.address(); + socket.close(error => { + if (error) reject(error); + else resolve(String(address.port)); + }); + }); + }); +} + +async function waitForServer(child, baseUrl, logs) { + const deadline = Date.now() + 15000; + while (Date.now() < deadline) { + if (child.exitCode !== null) { + throw new Error(`Server exited with ${child.exitCode}\n${logs.join('')}`); + } + try { + const response = await fetch(baseUrl); + if (response.ok) return; + } catch { + // Keep waiting for startup. + } + await new Promise(resolve => setTimeout(resolve, 100)); + } + throw new Error(`Server startup timed out\n${logs.join('')}`); +} + +async function stopProcess(child) { + if (!child || child.exitCode !== null) return; + child.kill('SIGTERM'); + await new Promise(resolve => { + const timer = setTimeout(() => { + if (child.exitCode === null) child.kill('SIGKILL'); + }, 3000); + child.once('exit', () => { + clearTimeout(timer); + resolve(); + }); + }); +} + +(async () => { + assert.ok(RUNFILES); + assert.ok(WORKSPACE); + const serverBinary = path.join( + runfilesWorkspace, + 'mrjunejune/mrjunejune_server', + ); + const fakeSidecar = path.join( + runfilesWorkspace, + 'mrjunejune/test/inference_bridge_fake_sidecar', + ); + const tempDirectory = fs.mkdtempSync( + path.join(os.tmpdir(), 'mrjunejune-conversation-api-'), + ); + const databasePath = path.join(tempDirectory, 'conversations.db'); + const port = await findFreePort(); + const baseUrl = `http://127.0.0.1:${port}`; + const logs = []; + let server; + + try { + server = spawn(serverBinary, [], { + cwd: runfilesWorkspace, + env: { + ...process.env, + MRJUNEJUNE_PORT: port, + MRJUNEJUNE_DB_PATH: databasePath, + MRJUNEJUNE_INFERENCE_SIDECAR_PATH: fakeSidecar, + MRJUNEJUNE_COPILOT_CLI_PATH: fakeSidecar, + MRJUNEJUNE_ALLOW_ANONYMOUS_INFERENCE: '1', + }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + server.stdout.on('data', chunk => logs.push(chunk.toString())); + server.stderr.on('data', chunk => logs.push(chunk.toString())); + await waitForServer(server, baseUrl, logs); + let response = await fetch(`${baseUrl}/api/inference/health`); + assert.equal(response.status, 200); + + response = await fetch(`${baseUrl}/api/conversations`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Origin: 'https://attacker.invalid', + }, + body: JSON.stringify({ title: 'Blocked' }), + }); + assert.equal(response.status, 403); + + response = await fetch(`${baseUrl}/api/conversations`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Origin: baseUrl, + }, + body: JSON.stringify({ title: 'First quest' }), + }); + assert.equal(response.status, 201); + const created = await response.json(); + assert.match(created.id, /^[0-9a-f-]{36}$/); + + response = await fetch(`${baseUrl}/api/conversations/${created.id}`); + assert.equal(response.status, 200); + let conversation = await response.json(); + assert.equal(conversation.title, 'First quest'); + assert.deepEqual(conversation.turns, []); + + response = await fetch(`${baseUrl}/api/conversations/${created.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Origin: baseUrl, + }, + body: JSON.stringify({ title: 'Renamed quest' }), + }); + assert.equal(response.status, 200); + + response = await fetch( + `${baseUrl}/api/conversations/${created.id}/turns`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Origin: baseUrl, + }, + body: JSON.stringify({ prompt: 'Hello Epi' }), + }, + ); + assert.equal(response.status, 200); + assert.match( + response.headers.get('content-type') || '', + /^text\/event-stream/, + ); + const stream = await response.text(); + assert.match(stream, /event: turn\.accepted/); + assert.match(stream, /event: assistant\.delta/); + assert.match(stream, /hello \\"traveler\\"\\\\path/); + assert.match(stream, /event: assistant\.completed/); + assert.match(stream, /event: assistant\.usage/); + assert.match(stream, /event: turn\.done/); + + response = await fetch(`${baseUrl}/api/conversations/${created.id}`); + conversation = await response.json(); + assert.equal(conversation.title, 'Renamed quest'); + assert.equal(conversation.turns.length, 2); + assert.equal(conversation.turns[0].content, 'Hello Epi'); + assert.equal(conversation.turns[1].content, 'hello traveler'); + assert.equal(conversation.turns[1].input_tokens, 7); + assert.equal(conversation.turns[1].output_tokens, 3); + + response = await fetch(`${baseUrl}/api/conversations/${created.id}`, { + method: 'DELETE', + headers: { Origin: baseUrl }, + }); + assert.equal(response.status, 204); + response = await fetch(`${baseUrl}/api/conversations/${created.id}`); + assert.equal(response.status, 404); + } finally { + await stopProcess(server); + fs.rmSync(tempDirectory, { recursive: true, force: true }); + } +})().catch(error => { + console.error(error.stack || error); + process.exitCode = 1; +});