0daf40d950
Memind CI / Test, build, and release guards (push) Failing after 3m15s
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>
238 lines
7.7 KiB
JavaScript
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);
|
|
});
|