comparison 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
comparison
equal deleted inserted replaced
259:667156fcd3e3 260:1f9877b637e9
1 const assert = require('node:assert/strict');
2 const fs = require('node:fs');
3 const net = require('node:net');
4 const os = require('node:os');
5 const path = require('node:path');
6 const { spawn } = require('node:child_process');
7
8 const RUNFILES = process.env.JS_BINARY__RUNFILES;
9 const WORKSPACE = process.env.JS_BINARY__WORKSPACE;
10 const runfilesWorkspace = path.join(RUNFILES, WORKSPACE);
11
12 function findFreePort() {
13 return new Promise((resolve, reject) => {
14 const socket = net.createServer();
15 socket.once('error', reject);
16 socket.listen(0, '127.0.0.1', () => {
17 const address = socket.address();
18 socket.close(error => {
19 if (error) reject(error);
20 else resolve(String(address.port));
21 });
22 });
23 });
24 }
25
26 async function waitForServer(child, baseUrl, logs) {
27 const deadline = Date.now() + 15000;
28 while (Date.now() < deadline) {
29 if (child.exitCode !== null) {
30 throw new Error(`Server exited with ${child.exitCode}\n${logs.join('')}`);
31 }
32 try {
33 const response = await fetch(baseUrl);
34 if (response.ok) return;
35 } catch {
36 // Keep waiting for startup.
37 }
38 await new Promise(resolve => setTimeout(resolve, 100));
39 }
40 throw new Error(`Server startup timed out\n${logs.join('')}`);
41 }
42
43 async function stopProcess(child) {
44 if (!child || child.exitCode !== null) return;
45 child.kill('SIGTERM');
46 await new Promise(resolve => {
47 const timer = setTimeout(() => {
48 if (child.exitCode === null) child.kill('SIGKILL');
49 }, 3000);
50 child.once('exit', () => {
51 clearTimeout(timer);
52 resolve();
53 });
54 });
55 }
56
57 (async () => {
58 assert.ok(RUNFILES);
59 assert.ok(WORKSPACE);
60 const serverBinary = path.join(
61 runfilesWorkspace,
62 'mrjunejune/mrjunejune_server',
63 );
64 const fakeSidecar = path.join(
65 runfilesWorkspace,
66 'mrjunejune/test/inference_bridge_fake_sidecar',
67 );
68 const tempDirectory = fs.mkdtempSync(
69 path.join(os.tmpdir(), 'mrjunejune-conversation-api-'),
70 );
71 const databasePath = path.join(tempDirectory, 'conversations.db');
72 const port = await findFreePort();
73 const baseUrl = `http://127.0.0.1:${port}`;
74 const logs = [];
75 let server;
76
77 try {
78 server = spawn(serverBinary, [], {
79 cwd: runfilesWorkspace,
80 env: {
81 ...process.env,
82 MRJUNEJUNE_PORT: port,
83 MRJUNEJUNE_DB_PATH: databasePath,
84 MRJUNEJUNE_INFERENCE_SIDECAR_PATH: fakeSidecar,
85 MRJUNEJUNE_COPILOT_CLI_PATH: fakeSidecar,
86 MRJUNEJUNE_ALLOW_ANONYMOUS_INFERENCE: '1',
87 },
88 stdio: ['ignore', 'pipe', 'pipe'],
89 });
90 server.stdout.on('data', chunk => logs.push(chunk.toString()));
91 server.stderr.on('data', chunk => logs.push(chunk.toString()));
92 await waitForServer(server, baseUrl, logs);
93 let response = await fetch(`${baseUrl}/api/inference/health`);
94 assert.equal(response.status, 200);
95
96 response = await fetch(`${baseUrl}/api/conversations`, {
97 method: 'POST',
98 headers: {
99 'Content-Type': 'application/json',
100 Origin: 'https://attacker.invalid',
101 },
102 body: JSON.stringify({ title: 'Blocked' }),
103 });
104 assert.equal(response.status, 403);
105
106 response = await fetch(`${baseUrl}/api/conversations`, {
107 method: 'POST',
108 headers: {
109 'Content-Type': 'application/json',
110 Origin: baseUrl,
111 },
112 body: JSON.stringify({ title: 'First quest' }),
113 });
114 assert.equal(response.status, 201);
115 const created = await response.json();
116 assert.match(created.id, /^[0-9a-f-]{36}$/);
117
118 response = await fetch(`${baseUrl}/api/conversations/${created.id}`);
119 assert.equal(response.status, 200);
120 let conversation = await response.json();
121 assert.equal(conversation.title, 'First quest');
122 assert.deepEqual(conversation.turns, []);
123
124 response = await fetch(`${baseUrl}/api/conversations/${created.id}`, {
125 method: 'PATCH',
126 headers: {
127 'Content-Type': 'application/json',
128 Origin: baseUrl,
129 },
130 body: JSON.stringify({ title: 'Renamed quest' }),
131 });
132 assert.equal(response.status, 200);
133
134 response = await fetch(
135 `${baseUrl}/api/conversations/${created.id}/turns`,
136 {
137 method: 'POST',
138 headers: {
139 'Content-Type': 'application/json',
140 Origin: baseUrl,
141 },
142 body: JSON.stringify({ prompt: 'Hello Epi' }),
143 },
144 );
145 assert.equal(response.status, 200);
146 assert.match(
147 response.headers.get('content-type') || '',
148 /^text\/event-stream/,
149 );
150 const stream = await response.text();
151 assert.match(stream, /event: turn\.accepted/);
152 assert.match(stream, /event: assistant\.delta/);
153 assert.match(stream, /hello \\"traveler\\"\\\\path/);
154 assert.match(stream, /event: assistant\.completed/);
155 assert.match(stream, /event: assistant\.usage/);
156 assert.match(stream, /event: turn\.done/);
157
158 response = await fetch(`${baseUrl}/api/conversations/${created.id}`);
159 conversation = await response.json();
160 assert.equal(conversation.title, 'Renamed quest');
161 assert.equal(conversation.turns.length, 2);
162 assert.equal(conversation.turns[0].content, 'Hello Epi');
163 assert.equal(conversation.turns[1].content, 'hello traveler');
164 assert.equal(conversation.turns[1].input_tokens, 7);
165 assert.equal(conversation.turns[1].output_tokens, 3);
166
167 response = await fetch(`${baseUrl}/api/conversations/${created.id}`, {
168 method: 'DELETE',
169 headers: { Origin: baseUrl },
170 });
171 assert.equal(response.status, 204);
172 response = await fetch(`${baseUrl}/api/conversations/${created.id}`);
173 assert.equal(response.status, 404);
174 } finally {
175 await stopProcess(server);
176 fs.rmSync(tempDirectory, { recursive: true, force: true });
177 }
178 })().catch(error => {
179 console.error(error.stack || error);
180 process.exitCode = 1;
181 });