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;
+});