666db0b939
Persist goal runs in MySQL, bind agent runs to checkpoints, expose awaiting-approval UX in chat, and add admin inspection routes with local verify scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
247 lines
8.0 KiB
JavaScript
247 lines
8.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Local Goal Run MVP verify (no 103 / no production).
|
|
* - Service-layer lifecycle against local MySQL when configured
|
|
* - Optional HTTP smoke when local Portal is already running
|
|
*
|
|
* Usage:
|
|
* GOAL_RUN_ENABLED=1 GOAL_RUN_CANARY_USER_IDS=<uuid> node scripts/verify-goal-run-local.mjs
|
|
*/
|
|
import crypto from 'node:crypto';
|
|
import { loadH5Environment } from './load-env.mjs';
|
|
import { createDbPool, isDatabaseConfigured } from '../db.mjs';
|
|
import { createGoalRunService } from '../goal-run-service.mjs';
|
|
import { isGoalRunEnabledForUser } from '../goal-run-intent.mjs';
|
|
import { resolveCanaryVerifyUser } from './goal-run-verify-lib.mjs';
|
|
|
|
function envFlag(value, fallback = false) {
|
|
const raw = String(value ?? '').trim().toLowerCase();
|
|
if (!raw) return fallback;
|
|
return ['1', 'true', 'yes', 'on'].includes(raw);
|
|
}
|
|
|
|
loadH5Environment(import.meta.dirname);
|
|
|
|
const PORTAL = `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`;
|
|
const USERNAME = process.env.VERIFY_GOAL_RUN_USER ?? process.env.VERIFY_LLM_ROUTER_USER ?? 'john2';
|
|
const PASSWORD = process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? '888888';
|
|
|
|
function pass(label, detail = '') {
|
|
console.log(`PASS ${label}${detail ? `: ${detail}` : ''}`);
|
|
}
|
|
|
|
function fail(label, detail = '') {
|
|
console.error(`FAIL ${label}${detail ? `: ${detail}` : ''}`);
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
async function resolveVerifyUserId(pool) {
|
|
const verifyUser = await resolveCanaryVerifyUser(pool, { usernameFallback: USERNAME });
|
|
return verifyUser?.userId ?? null;
|
|
}
|
|
|
|
async function verifyServiceLifecycle(pool, userId) {
|
|
const service = createGoalRunService({ pool });
|
|
await service.ensureSchema();
|
|
|
|
const goal = await service.createGoalRun({
|
|
userId,
|
|
title: `[verify] Goal Run ${new Date().toISOString()}`,
|
|
intentSummary: '本地 verify:分阶段完成 smoke 任务',
|
|
sourceChannel: 'api',
|
|
checkpoints: [
|
|
{ title: '调研', description: '收集信息' },
|
|
{ title: '输出', description: '形成草案' },
|
|
],
|
|
});
|
|
if (!goal?.id || !goal.checkpoints?.length) {
|
|
fail('service.createGoalRun', 'missing goal or checkpoints');
|
|
return null;
|
|
}
|
|
pass('service.createGoalRun', goal.id);
|
|
|
|
const started = await service.startNextCheckpoint({
|
|
userId,
|
|
goalRunId: goal.id,
|
|
agentRunId: null,
|
|
});
|
|
if (!started?.checkpointId) {
|
|
fail('service.startNextCheckpoint', 'missing checkpoint');
|
|
return goal;
|
|
}
|
|
pass('service.startNextCheckpoint', started.checkpointId);
|
|
|
|
const runId = crypto.randomUUID();
|
|
const now = Date.now();
|
|
await pool.query(
|
|
`INSERT INTO h5_agent_runs
|
|
(id, user_id, agent_session_id, goal_run_id, goal_checkpoint_id, request_id,
|
|
status, attempts, user_message_json, created_at, updated_at, completed_at)
|
|
VALUES (?, ?, NULL, ?, ?, ?, 'succeeded', 1, '{}', ?, ?, ?)`,
|
|
[runId, userId, goal.id, started.checkpointId, `verify-goal-${now}`, now, now, now],
|
|
);
|
|
await service.attachAgentRunToCheckpoint({
|
|
checkpointId: started.checkpointId,
|
|
agentRunId: runId,
|
|
});
|
|
|
|
const completed = await service.onAgentRunCompleted({
|
|
agentRunId: runId,
|
|
status: 'succeeded',
|
|
outputSummary: 'verify smoke completed',
|
|
});
|
|
if (!completed?.handled) {
|
|
fail('service.onAgentRunCompleted', completed?.reason ?? 'not handled');
|
|
return goal;
|
|
}
|
|
pass('service.onAgentRunCompleted', completed.goalCompleted
|
|
? 'goal completed'
|
|
: completed.awaitingApproval
|
|
? 'awaiting approval'
|
|
: 'checkpoint advanced');
|
|
|
|
if (goal.checkpoints.length > 1 && completed.goalCompleted) {
|
|
fail('service.multiCheckpoint', 'expected goal to stay open until final checkpoint');
|
|
return goal;
|
|
}
|
|
if (goal.checkpoints.length > 1 && completed.awaitingApproval) {
|
|
const firstCheckpoint = goal.checkpoints[0];
|
|
const approved = await service.approveCheckpoint({
|
|
userId,
|
|
goalRunId: goal.id,
|
|
checkpointId: firstCheckpoint.id,
|
|
feedback: 'verify-local approve',
|
|
});
|
|
pass('service.approveCheckpoint', approved.status);
|
|
const resumed = await service.startNextCheckpoint({ userId, goalRunId: goal.id });
|
|
pass('service.resumeNextCheckpoint', resumed.checkpointId);
|
|
} else if (goal.checkpoints.length > 1 && completed.nextCheckpointId) {
|
|
pass('service.multiCheckpoint', `next=${completed.nextCheckpointId}`);
|
|
const resumed = await service.startNextCheckpoint({
|
|
userId,
|
|
goalRunId: goal.id,
|
|
});
|
|
pass('service.resumeNextCheckpoint', resumed.checkpointId);
|
|
}
|
|
|
|
const finalGoal = await service.getGoalRun({ userId, goalRunId: goal.id });
|
|
if (!finalGoal) {
|
|
fail('service.getGoalRun', 'missing after completion');
|
|
return goal;
|
|
}
|
|
pass('service.getGoalRun', `status=${finalGoal.status}`);
|
|
|
|
await pool.query('DELETE FROM h5_agent_runs WHERE id = ?', [runId]);
|
|
await pool.query('DELETE FROM h5_goal_runs WHERE id = ?', [goal.id]);
|
|
pass('service.cleanup', 'removed verify goal/run');
|
|
return goal;
|
|
}
|
|
|
|
async function loginForHttp(pool) {
|
|
const verifyUser = await resolveCanaryVerifyUser(pool, { usernameFallback: USERNAME });
|
|
if (!verifyUser) return null;
|
|
const { createUserAuth } = await import('../user-auth.mjs');
|
|
const auth = createUserAuth(pool);
|
|
const result = await auth.login({
|
|
username: verifyUser.username,
|
|
password: PASSWORD,
|
|
ip: '127.0.0.1',
|
|
});
|
|
if (!result.ok) return null;
|
|
return result.token;
|
|
}
|
|
|
|
async function verifyHttpApi(token, userId) {
|
|
try {
|
|
const health = await fetch(`${PORTAL}/api/goals`, {
|
|
headers: { Cookie: `tkmind_user_session=${token}` },
|
|
signal: AbortSignal.timeout(3000),
|
|
});
|
|
if (health.status === 503) {
|
|
console.log('SKIP http.api (Goal Run disabled in running server env)');
|
|
return;
|
|
}
|
|
if (health.status === 403) {
|
|
console.log('SKIP http.api (running server has Goal Run disabled or canary mismatch — restart pnpm dev after setting GOAL_RUN_* in .env)');
|
|
return;
|
|
}
|
|
if (!health.ok) {
|
|
fail('http.api.list', `GET /api/goals -> ${health.status}`);
|
|
return;
|
|
}
|
|
pass('http.api.list', `${health.status}`);
|
|
|
|
const createRes = await fetch(`${PORTAL}/api/goals`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Cookie: `tkmind_user_session=${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
title: `[verify-http] ${Date.now()}`,
|
|
intentSummary: 'HTTP verify goal',
|
|
}),
|
|
});
|
|
const createBody = await createRes.json().catch(() => ({}));
|
|
if (!createRes.ok) {
|
|
fail('http.api.create', `${createRes.status} ${JSON.stringify(createBody)}`);
|
|
return;
|
|
}
|
|
pass('http.api.create', createBody.goal?.id ?? 'ok');
|
|
} catch (err) {
|
|
console.log(`SKIP http.api (${err instanceof Error ? err.message : err})`);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
if (!envFlag(process.env.GOAL_RUN_ENABLED, false)) {
|
|
fail('env', 'set GOAL_RUN_ENABLED=1 before verify');
|
|
return;
|
|
}
|
|
if (!String(process.env.GOAL_RUN_CANARY_USER_IDS ?? '').trim()) {
|
|
fail('env', 'set GOAL_RUN_CANARY_USER_IDS before verify');
|
|
return;
|
|
}
|
|
pass('env', 'Goal Run feature flag enabled');
|
|
|
|
if (!isDatabaseConfigured()) {
|
|
fail('database', 'local MySQL not configured');
|
|
return;
|
|
}
|
|
|
|
const pool = createDbPool();
|
|
try {
|
|
const userId = await resolveVerifyUserId(pool);
|
|
if (!userId) {
|
|
fail('user', `cannot resolve verify user (${USERNAME})`);
|
|
return;
|
|
}
|
|
if (!isGoalRunEnabledForUser(userId, process.env)) {
|
|
fail('canary', `user ${userId} not in GOAL_RUN_CANARY_USER_IDS`);
|
|
return;
|
|
}
|
|
pass('canary', userId);
|
|
|
|
await verifyServiceLifecycle(pool, userId);
|
|
|
|
const token = await loginForHttp(pool);
|
|
if (token) {
|
|
await verifyHttpApi(token, userId);
|
|
} else {
|
|
console.log('SKIP http.api (login failed)');
|
|
}
|
|
} finally {
|
|
await pool.end?.().catch(() => {});
|
|
}
|
|
|
|
if (process.exitCode) {
|
|
console.error('\nGoal Run local verify failed.');
|
|
} else {
|
|
console.log('\nGoal Run local verify passed.');
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
fail('fatal', err instanceof Error ? err.message : String(err));
|
|
});
|