|
| 1 | +import assert from "node:assert/strict"; |
| 2 | +import { spawn } from "node:child_process"; |
| 3 | +import { once } from "node:events"; |
| 4 | +import { resolve } from "node:path"; |
| 5 | +import { createInterface } from "node:readline"; |
| 6 | +import { resolveTestPython } from "../../../../scripts/test-python.mjs"; |
| 7 | +import { fetchChatHistory } from "../src/data/chat"; |
| 8 | + |
| 9 | +const repoRoot = resolve(process.cwd(), "../../.."); |
| 10 | +const child = spawn(resolveTestPython({ repoRoot }), ["-u", "apps/presentation/dashboard/smoke/conversation-history-http-fixture.py"], |
| 11 | + { cwd: repoRoot, stdio: ["pipe", "pipe", "pipe"] }); |
| 12 | +const exited = once(child, "exit"); |
| 13 | +let stderr = ""; |
| 14 | +child.stderr.on("data", chunk => { stderr += String(chunk); }); |
| 15 | +const output = createInterface({ input: child.stdout }); |
| 16 | +const lines = output[Symbol.asyncIterator](); |
| 17 | +const originalFetch = globalThis.fetch; |
| 18 | +async function next() { |
| 19 | + const line = await lines.next(); |
| 20 | + assert.equal(line.done, false, stderr); |
| 21 | + return JSON.parse(line.value!); |
| 22 | +} |
| 23 | +try { |
| 24 | + const { origin } = await next(); |
| 25 | + const requests: string[] = []; |
| 26 | + globalThis.fetch = (input, init) => { |
| 27 | + const path = String(input); |
| 28 | + assert.ok(!init?.method || init.method === "GET", "History recovery is read-only"); |
| 29 | + requests.push(path); |
| 30 | + return originalFetch(new URL(path, origin), init); |
| 31 | + }; |
| 32 | + const options = { agentId: "codex", goalId: "research", channelId: "goal.research" }; |
| 33 | + const partial = await fetchChatHistory(options); |
| 34 | + assert.deepEqual(partial.unavailableSessionIds, ["old"]); |
| 35 | + assert.deepEqual(partial.messages.map(row => row.text), ["Current public report"]); |
| 36 | + assert.equal(partial.sessions[0].session_id, "current"); |
| 37 | + assert.equal(partial.sessions.some(row => row.session_id === "other-channel"), false); |
| 38 | + child.stdin.write("recover\n"); |
| 39 | + assert.equal((await next()).recovered, true); |
| 40 | + requests.length = 0; |
| 41 | + const restored = await fetchChatHistory(options, partial); |
| 42 | + assert.deepEqual(requests, ["/api/chat/sessions/old"], "Recovery reads only the missing session"); |
| 43 | + assert.deepEqual(restored.unavailableSessionIds, []); |
| 44 | + assert.deepEqual(restored.messages.map(row => row.text), ["Earlier public report", "Current public report"]); |
| 45 | + assert.deepEqual(restored.messages.map(row => row.session_id), ["old", "current"], "Colliding IDs preserve both sessions"); |
| 46 | + requests.length = 0; |
| 47 | + await fetchChatHistory(options, restored); |
| 48 | + assert.deepEqual(requests, [], "A complete cached history does not poll"); |
| 49 | + child.stdin.end("inspect\n"); |
| 50 | + assert.deepEqual(await next(), { store_unchanged: true, turn_count: 0 }); |
| 51 | + const [exitCode] = await exited; |
| 52 | + assert.equal(exitCode, 0, stderr); |
| 53 | + console.log("conversation-history: passed (real HTTP/store, partial read, channel isolation, missing-only recovery, scoped identity, zero writes or Turns)"); |
| 54 | +} finally { |
| 55 | + globalThis.fetch = originalFetch; |
| 56 | + output.close(); |
| 57 | + if (child.exitCode === null) { child.kill(); await exited; } |
| 58 | +} |
0 commit comments