#!/usr/bin/env node /** * Phase 2 smoke: POST /sessions/{id}/reply + GET /sessions/{id}/events (SSE). * Validates the event bus pipeline; Finish or Error both count as SSE_OK. */ import { randomUUID } from 'node:crypto'; import { Readable } from 'node:stream'; import { Agent, fetch } from 'undici'; import { enforceRealLlmGate } from './goose-v149-real-llm-gate.mjs'; enforceRealLlmGate('check-goosed-v149-reply-smoke.mjs'); const port = process.env.GOOSE_V149_PORT || '18049'; const host = process.env.GOOSE_V149_HOST || '127.0.0.1'; const secret = process.env.GOOSE_SERVER__SECRET_KEY || 'local-v149-dev-secret'; const workingDir = process.env.GOOSE_V149_WORKING_DIR || process.cwd(); const timeoutMs = Number(process.env.GOOSE_V149_REPLY_TIMEOUT_MS || 30_000); const blockedHosts = ['58.38.22.103', '120.26.184.105']; if (blockedHosts.includes(host)) { console.error(`GOOSE_V149_REPLY_SMOKE_FAIL: refusing production host ${host}`); process.exit(1); } const base = `https://${host}:${port}`; const dispatcher = new Agent({ connect: { rejectUnauthorized: false } }); async function apiFetch(pathname, init = {}) { const headers = { ...(init.headers ?? {}), 'X-Secret-Key': secret, }; if (init.body && !headers['Content-Type']) { headers['Content-Type'] = 'application/json'; } return fetch(`${base}${pathname}`, { ...init, headers, dispatcher, }); } async function apiJson(pathname, body) { const response = await apiFetch(pathname, { method: 'POST', body: JSON.stringify(body), }); const text = await response.text(); if (!response.ok) { throw new Error(`${pathname} ${response.status}: ${text.slice(0, 800)}`); } return text.trim() ? JSON.parse(text) : {}; } async function waitForTerminalEvent(sessionId, requestId) { const eventsResponse = await apiFetch(`/sessions/${sessionId}/events`, { method: 'GET', headers: { Accept: 'text/event-stream' }, }); if (!eventsResponse.ok || !eventsResponse.body) { const text = await eventsResponse.text().catch(() => ''); throw new Error(`events ${eventsResponse.status}: ${text.slice(0, 400)}`); } const replyResponse = await apiFetch(`/sessions/${sessionId}/reply`, { method: 'POST', body: JSON.stringify({ request_id: requestId, user_message: { role: 'user', created: Date.now(), content: [{ type: 'text', text: 'ping' }], metadata: { userVisible: true, agentVisible: true, displayText: 'ping' }, }, }), }); if (!replyResponse.ok) { const text = await replyResponse.text().catch(() => ''); throw new Error(`reply ${replyResponse.status}: ${text.slice(0, 400)}`); } replyResponse.body?.cancel?.(); const reader = Readable.fromWeb(eventsResponse.body); const decoder = new TextDecoder(); let buffer = ''; const deadline = Date.now() + timeoutMs; for await (const chunk of reader) { if (Date.now() > deadline) { throw new Error(`timeout waiting for Finish/Error (${timeoutMs}ms)`); } buffer += decoder.decode(chunk, { stream: true }); const frames = buffer.split('\n\n'); buffer = frames.pop() ?? ''; for (const frame of frames) { let data = ''; for (const line of frame.split('\n')) { if (line.startsWith('data:')) data += line.slice(5).trim(); } if (!data) continue; let event; try { event = JSON.parse(data); } catch { continue; } const routingId = event.chat_request_id ?? event.request_id; if (routingId && routingId !== requestId) continue; if (event.type === 'Finish') { console.log('GOOSE_V149_REPLY_EVENT: type=Finish'); return 'finish'; } if (event.type === 'Error') { console.log(`GOOSE_V149_REPLY_EVENT: type=Error error=${String(event.error ?? '').slice(0, 200)}`); return 'error'; } if (event.type === 'Message') { console.log('GOOSE_V149_REPLY_EVENT: type=Message'); } } } throw new Error(`SSE stream closed before Finish/Error (${timeoutMs}ms)`); } try { const session = await apiJson('/agent/start', { working_dir: workingDir }); if (!session?.id) throw new Error('missing session id from /agent/start'); const provider = process.env.GOOSE_V149_TEST_PROVIDER; const model = process.env.GOOSE_V149_TEST_MODEL; if (provider && model) { await apiJson('/agent/update_provider', { session_id: session.id, provider, model, }); console.log(`GOOSE_V149_REPLY_PROVIDER_OK: ${provider}/${model}`); } const requestId = randomUUID(); const outcome = await waitForTerminalEvent(session.id, requestId); console.log(`GOOSE_V149_REPLY_SMOKE_OK: session=${session.id} request=${requestId} outcome=${outcome}`); } catch (error) { console.error(`GOOSE_V149_REPLY_SMOKE_FAIL: ${error.message}`); process.exit(1); }