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