// Repository-layout fixture for the conversation view (#1507, CHAT-02). The // board's history reader reads only /.pi/state//sessions, // with the project named after the root directory and a Pi header whose cwd is // inside the project. Real board, real routes, real WebUI; temporary data only. import { mkdtempSync, mkdirSync, writeFileSync, appendFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { startServer as startBoard } from '../../control-board/src/serve.mjs'; import { makeRegistration, writeRegistration } from '../../seat/src/seat.mjs'; import { startServer } from '../src/serve.mjs'; import { close } from './fixture.mjs'; export const at = s => new Date(Date.now() + s * 1000).toISOString(); // Appends Pi entries to one session file, each the child of the one before. export function session(file, cwd, { id = 'sess-1', timestamp = at(-600) } = {}) { let n = 0, parent = null; writeFileSync(file, JSON.stringify({ type: 'session', version: 3, id, timestamp, cwd }) + '\n'); return { add(message, timestamp = at(0)) { const entry = `${id}-e${++n}`; appendFileSync(file, JSON.stringify({ type: 'message', id: entry, parentId: parent, timestamp, message }) + '\n'); parent = entry; return entry; }, raw(text) { appendFileSync(file, text + '\n'); }, }; } export const user = text => ({ role: 'user', content: [{ type: 'text', text }] }); export const assistant = (content, stopReason = 'stop') => ({ role: 'assistant', stopReason, content: typeof content === 'string' ? [{ type: 'text', text: content }] : content }); export const toolResult = (toolCallId, text, isError = false) => ({ role: 'toolResult', toolCallId, toolName: 'read', content: [{ type: 'text', text }], isError }); export async function historyFixture({ seat = 'fixture', project = 'repo' } = {}) { const root = mkdtempSync(join(tmpdir(), 'webui-history-')); const projectRoot = join(root, project), sessionsDir = join(projectRoot, '.pi', 'state', seat, 'sessions'); mkdirSync(sessionsDir, { recursive: true }); const seatsDir = join(root, 'seats'), captures = []; writeRegistration(seatsDir, makeRegistration({ resolved: { seat, project, sessionsDir, seatDir: root, launchScript: join(root, 'unused.sh'), layout: 'repo', defaultWorkspace: projectRoot }, task: 'Fixed task', tmux: { session: seat, socket: null }, pid: process.pid, now: () => new Date(Date.now() - 3600000) })); const board = await startBoard({ port: 0, specs: [{ agent: seat, project, sessionsDir, tmux: { session: seat } }], boardDir: join(root, 'board'), seatsDir, isAlive: () => true, isPidAlive: () => true, exec: (f, args) => { captures.push(args); return { status: 0, stdout: '', stderr: '' }; } }); const web = await startServer({ port: 0, board: `http://127.0.0.1:${board.address().port}` }); return { root, projectRoot, sessionsDir, board, web, base: `http://127.0.0.1:${web.address().port}`, captures, async close() { await close(web); await close(board); rmSync(root, { recursive: true, force: true }); }, }; } // Browser helpers shared by the history tests. export function waiter(b) { return (expression, ms = 25000) => b.evaluate(`(async()=>{for(const end=Date.now()+${ms};Date.now()setTimeout(r,100))}throw Error('timeout: '+${JSON.stringify(expression)})})()`); } export const turns = b => b.evaluate('[...document.querySelectorAll("#conv-log > li")].map(li => [li.querySelector(".who").textContent, li.querySelector(".conv-text")?.textContent ?? [...li.querySelectorAll("summary")].map(s => s.textContent).join(" | ")])');