#!/usr/bin/env node /** * Context Runtime shadow portal E2E: one agent turn, verify shadow events in DB. * Default: poll shadow events only (do not wait for full Goose completion — saves LLM tokens). */ import { randomUUID } from 'node:crypto'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createDbPool } from '../db.mjs'; import { CONTEXT_RUNTIME_SHADOW_ENV } from '../context-runtime-profile.mjs'; import { createReporter, loginViaApi, resolvePortalBase, sleep, waitForRunTerminal, } from './scenario-test-lib.mjs'; import { waitForAgentRunWorkerIdle } from './goose-v149-worker-idle.mjs'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); for (const [key, value] of Object.entries(CONTEXT_RUNTIME_SHADOW_ENV)) { process.env[key] = process.env[key] ?? value; } process.env.HEADROOM_OUTPUT_SHAPER = process.env.HEADROOM_OUTPUT_SHAPER ?? '0'; const baseUrl = resolvePortalBase(Number(process.env.H5_PORT ?? 8081)); const timeoutMs = Number(process.env.CONTEXT_RUNTIME_SHADOW_PORTAL_TIMEOUT_MS ?? 180_000); const eventsOnly = process.env.CONTEXT_RUNTIME_SHADOW_EVENTS_ONLY !== '0'; const pollMs = Number(process.env.CONTEXT_RUNTIME_SHADOW_POLL_MS ?? 2000); async function portalReachable() { try { const response = await fetch(`${baseUrl}/auth/status`); return response.ok; } catch { return false; } } async function fetchRunEvents(pool, runId) { const [rows] = await pool.query( `SELECT event_type, data_json, created_at FROM h5_agent_run_events WHERE run_id = ? ORDER BY created_at ASC`, [runId], ); return rows.map((row) => ({ eventType: row.event_type, data: typeof row.data_json === 'string' ? JSON.parse(row.data_json) : row.data_json, createdAt: Number(row.created_at), })); } function pickEvents(events, type) { return events.filter((event) => event.eventType === type); } function evaluateShadowEvents(events) { const headroomEvents = pickEvents(events, 'headroom_context_observed'); const budgetEvents = pickEvents(events, 'context_budget_resolved'); const fusionEvents = pickEvents(events, 'recall_fusion_resolved'); const issues = []; const warnings = []; if (!headroomEvents.length) issues.push('missing headroom_context_observed'); if (!budgetEvents.length) issues.push('missing context_budget_resolved'); if (!fusionEvents.length) { warnings.push('missing recall_fusion_resolved (memory path may be off/skipped)'); } for (const [label, rows] of [ ['headroom', headroomEvents], ['budget', budgetEvents], ['fusion', fusionEvents], ]) { const mode = rows[0]?.data?.mode; if (mode && mode !== 'shadow') { issues.push(`${label} mode=${mode} (expected shadow)`); } } return { headroomEvents, budgetEvents, fusionEvents, issues, warnings }; } async function pollShadowEvents(pool, runId, deadlineMs) { while (Date.now() < deadlineMs) { const events = await fetchRunEvents(pool, runId); const evaluation = evaluateShadowEvents(events); const coreReady = evaluation.headroomEvents.length && evaluation.budgetEvents.length; if (coreReady) { return { events, evaluation, done: true }; } await sleep(pollMs); } const events = await fetchRunEvents(pool, runId); return { events, evaluation: evaluateShadowEvents(events), done: false }; } async function runPortalTurn(pool) { const username = process.env.RELEASE_GATE_SCENARIO_USERNAME ?? 'john'; const password = process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? process.env.MEMIND_PASSWORD ?? ''; if (!password) throw new Error('set JOHN_PASSWORD for portal e2e'); const reporter = createReporter(); const auth = await loginViaApi(baseUrl, { username, password }, reporter); await waitForAgentRunWorkerIdle(root, process.env, { logPrefix: '[context-runtime-shadow-e2e]', }); const startRes = await fetch(`${baseUrl}/api/agent/start`, { method: 'POST', headers: { Cookie: auth.cookie, 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); const started = await startRes.json().catch(() => ({})); if (!startRes.ok || !started?.id) { throw new Error(`agent/start failed: ${startRes.status}`); } const sessionId = started.id; const warmResume = await fetch(`${baseUrl}/api/agent/resume`, { method: 'POST', headers: { Cookie: auth.cookie, 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId, load_model_and_extensions: true, }), }); if (!warmResume.ok) { const warmBody = await warmResume.text().catch(() => ''); throw new Error(`pre-run resume failed: ${warmResume.status} ${warmBody.slice(0, 200)}`); } const requestId = randomUUID(); const runRes = await fetch(`${baseUrl}/api/agent/runs`, { method: 'POST', headers: { Cookie: auth.cookie, 'Content-Type': 'application/json' }, body: JSON.stringify({ request_id: requestId, session_id: sessionId, force_deep_reasoning: true, user_message: { id: randomUUID(), role: 'user', content: [{ type: 'text', text: process.env.MEMIND_SHADOW_E2E_MESSAGE ?? '你还记得我的偏好吗,一句话回答即可', }], metadata: { userVisible: true, agentVisible: true, displayText: process.env.MEMIND_SHADOW_E2E_DISPLAY ?? 'context runtime shadow memory ping', }, }, }), }); const runPayload = await runRes.json().catch(() => ({})); if (!runRes.ok) { throw new Error(`POST /api/agent/runs ${runRes.status}: ${JSON.stringify(runPayload).slice(0, 300)}`); } const runId = runPayload.run?.id ?? runPayload.id; if (eventsOnly) { const polled = await pollShadowEvents(pool, runId, Date.now() + Math.min(timeoutMs, 60_000)); return { sessionId, runId, terminal: { status: polled.done ? 'shadow_events_ready' : 'shadow_events_timeout' }, events: polled.events, evaluation: polled.evaluation, eventsOnly: true, }; } const terminal = await waitForRunTerminal(baseUrl, auth.cookie, runId, timeoutMs); const events = await fetchRunEvents(pool, runId); return { sessionId, runId, terminal, events, evaluation: evaluateShadowEvents(events), eventsOnly: false, }; } async function main() { if (!(await portalReachable())) { throw new Error(`Portal not reachable at ${baseUrl} — start pnpm dev first`); } const pool = createDbPool(); const portal = await runPortalTurn(pool); const { headroomEvents, budgetEvents, fusionEvents, issues, warnings } = portal.evaluation; console.log('CONTEXT_RUNTIME_SHADOW_PORTAL_E2E:'); console.log(` portal=${baseUrl}`); console.log(` run_id=${portal.runId}`); console.log(` mode=${portal.eventsOnly ? 'events_only' : 'full_terminal'}`); console.log(` terminal=${portal.terminal.status}`); console.log(` headroom_events=${headroomEvents.length}`); console.log(` budget_events=${budgetEvents.length}`); console.log(` fusion_events=${fusionEvents.length}`); if (headroomEvents[0]?.data) { console.log(` headroom_sample=${JSON.stringify(headroomEvents[0].data)}`); } if (budgetEvents[0]?.data) { console.log(` budget_sample=${JSON.stringify(budgetEvents[0].data)}`); } if (fusionEvents[0]?.data) { console.log(` fusion_sample=${JSON.stringify(fusionEvents[0].data)}`); } for (const warning of warnings) { console.warn(`CONTEXT_RUNTIME_SHADOW_PORTAL_WARN: ${warning}`); } if (issues.length) { console.error(`CONTEXT_RUNTIME_SHADOW_PORTAL_FAIL: ${issues.join('; ')}`); process.exit(1); } console.log('CONTEXT_RUNTIME_SHADOW_PORTAL_OK'); } main().catch((err) => { console.error(err instanceof Error ? err.message : err); process.exit(1); });