Files
memind/scripts/run-context-runtime-shadow-portal-e2e.mjs
T
john 0daf40d950
Memind CI / Test, build, and release guards (push) Failing after 3m15s
docs(context): note agent memory shadow setup for fusion E2E
Allow custom shadow portal probe messages and document admin-db steps to
enable recall_fusion_resolved without turning on active injection.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 09:33:47 +08:00

238 lines
7.7 KiB
JavaScript

#!/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);
});