#!/usr/bin/env node /** * HTTP-focused Goal Run verify against local Portal (no 103). * Requires Portal already running with GOAL_RUN_* enabled for canary user. * * Usage: * GOAL_RUN_ENABLED=1 GOAL_RUN_CANARY_USER_IDS= node scripts/verify-goal-run-http-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'; loadH5Environment(import.meta.dirname); const PORTAL = `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`; const PASSWORD = process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? '888888'; function envFlag(value, fallback = false) { const raw = String(value ?? '').trim().toLowerCase(); if (!raw) return fallback; return ['1', 'true', 'yes', 'on'].includes(raw); } function pass(label, detail = '') { console.log(`PASS ${label}${detail ? `: ${detail}` : ''}`); } function fail(label, detail = '') { console.error(`FAIL ${label}${detail ? `: ${detail}` : ''}`); process.exitCode = 1; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function waitForPortal() { const started = Date.now(); while (Date.now() - started < 60_000) { try { const response = await fetch(`${PORTAL}/api/status`, { signal: AbortSignal.timeout(2000) }); if (response.ok) return; } catch { // retry } await sleep(1000); } throw new Error(`Portal ${PORTAL} 未在 60s 内就绪`); } async function login(pool) { const verifyUser = await resolveCanaryVerifyUser(pool); if (!verifyUser) { throw new Error('cannot resolve canary verify user'); } 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) { throw new Error(`login failed for ${verifyUser.username}: ${result.message ?? 'unknown'}`); } return { token: result.token, userId: verifyUser.userId, username: verifyUser.username, }; } async function apiFetch(path, { token, method = 'GET', body = null } = {}) { const response = await fetch(`${PORTAL}/api${path}`, { method, headers: { ...(body ? { 'Content-Type': 'application/json' } : {}), Cookie: `tkmind_user_session=${token}`, }, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(15_000), }); const payload = await response.json().catch(() => ({})); return { response, payload }; } async function portalFetch(path, { token, method = 'GET', body = null } = {}) { const response = await fetch(`${PORTAL}${path}`, { method, headers: { ...(body ? { 'Content-Type': 'application/json' } : {}), Cookie: `tkmind_user_session=${token}`, }, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(15_000), }); const payload = await response.json().catch(() => ({})); return { response, payload }; } async function startAgentSession(token) { const started = await apiFetch('/agent/start', { token, method: 'POST', body: {}, }); const sessionId = started.payload?.session?.id ?? started.payload?.id ?? null; if (!started.response.ok || !sessionId) { throw new Error(`agent/start failed: ${started.response.status}`); } return String(sessionId); } async function seedAwaitingGoal(pool, userId) { const service = createGoalRunService({ pool }); await service.ensureSchema(); const goal = await service.createGoalRun({ userId, title: `[verify-http] ${new Date().toISOString()}`, intentSummary: 'HTTP verify awaiting approval', sourceChannel: 'api', checkpoints: [ { title: '调研', description: '收集信息' }, { title: '输出', description: '形成草案' }, ], }); const started = await service.startNextCheckpoint({ userId, goalRunId: goal.id }); 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-http-${now}`, now, now, now], ); await service.attachAgentRunToCheckpoint({ checkpointId: started.checkpointId, agentRunId: runId }); const completed = await service.onAgentRunCompleted({ agentRunId: runId, status: 'succeeded', outputSummary: 'HTTP verify stage-1 done', }); if (!completed?.awaitingApproval) { throw new Error(`expected awaitingApproval, got ${JSON.stringify(completed)}`); } return { goal, firstCheckpointId: started.checkpointId, runId }; } async function cleanup(pool, { goalId, runId, extraRunIds = [] }) { for (const id of extraRunIds) { if (id) await pool.query('DELETE FROM h5_agent_runs WHERE id = ?', [id]); } if (runId) await pool.query('DELETE FROM h5_agent_runs WHERE id = ?', [runId]); if (goalId) await pool.query('DELETE FROM h5_goal_runs WHERE id = ?', [goalId]); } 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; } if (!isDatabaseConfigured()) { fail('database', 'local MySQL not configured'); return; } const pool = createDbPool(); let seeded = null; const extraRunIds = []; try { await waitForPortal(); pass('portal', PORTAL); const { token, userId, username } = await login(pool); if (!isGoalRunEnabledForUser(userId, process.env)) { fail('canary', `${userId} not in GOAL_RUN_CANARY_USER_IDS`); return; } pass('login', `${username} (${userId})`); const authStatus = await portalFetch('/auth/status', { token }); if (!authStatus.payload?.goalRun?.enabled) { fail('auth.status', 'goalRun.enabled missing — restart pnpm dev with GOAL_RUN_*'); return; } pass('auth.status', 'goalRun.enabled=true'); const list = await apiFetch('/goals?status=awaiting_user', { token }); if (list.response.status === 503) { fail('http.list', 'Goal Run disabled in running Portal — restart pnpm dev with GOAL_RUN_*'); return; } if (list.response.status === 403) { fail('http.list', 'canary mismatch in running Portal — restart pnpm dev'); return; } if (!list.response.ok) { fail('http.list', `${list.response.status}`); return; } pass('http.list', `${list.payload.goals?.length ?? 0} awaiting`); const create = await apiFetch('/goals', { token, method: 'POST', body: { title: `[verify-http-create] ${Date.now()}`, intentSummary: 'HTTP create smoke', checkpoints: [{ title: '一步', description: '单阶段' }], }, }); if (!create.response.ok) { fail('http.create', `${create.response.status} ${JSON.stringify(create.payload)}`); return; } const createdGoalId = create.payload.goal?.id; pass('http.create', createdGoalId); await pool.query('DELETE FROM h5_goal_runs WHERE id = ?', [createdGoalId]); seeded = await seedAwaitingGoal(pool, userId); pass('seed.awaiting', seeded.goal.id); const awaitingList = await apiFetch('/goals?status=awaiting_user', { token }); const found = (awaitingList.payload.goals ?? []).some((item) => item.id === seeded.goal.id); if (!found) { fail('http.awaiting', 'seeded goal not listed'); return; } pass('http.awaiting', seeded.goal.id); const detail = await apiFetch(`/goals/${encodeURIComponent(seeded.goal.id)}`, { token }); const awaitingCheckpoint = (detail.payload.goal?.checkpoints ?? []).find( (item) => item.status === 'awaiting_approval', ); if (!awaitingCheckpoint) { fail('http.detail', 'missing awaiting_approval checkpoint'); return; } pass('http.detail', awaitingCheckpoint.id); const sessionId = await startAgentSession(token); pass('http.session', sessionId); const approve = await apiFetch( `/goals/${encodeURIComponent(seeded.goal.id)}/checkpoints/${encodeURIComponent(awaitingCheckpoint.id)}/approve`, { token, method: 'POST', body: { feedback: 'verify-http approve', session_id: sessionId, request_id: crypto.randomUUID(), }, }, ); if (!approve.response.ok) { fail('http.approve', `${approve.response.status} ${JSON.stringify(approve.payload)}`); return; } pass('http.approve', approve.payload.goal?.status ?? 'ok'); if (!approve.payload.run?.id) { fail('http.approve.dispatch', 'missing auto-dispatched run'); return; } extraRunIds.push(approve.payload.run.id); pass('http.approve.dispatch', approve.payload.run.id); const [dispatchRows] = await pool.query( 'SELECT goal_run_id, goal_checkpoint_id FROM h5_agent_runs WHERE id = ? LIMIT 1', [approve.payload.run.id], ); if (String(dispatchRows[0]?.goal_run_id) !== seeded.goal.id) { fail('http.approve.dispatch.binding', `goal_run_id=${dispatchRows[0]?.goal_run_id}`); return; } pass('http.approve.dispatch.binding', dispatchRows[0]?.goal_checkpoint_id ?? 'bound'); const bindSessionId = await startAgentSession(token); const bindGoal = await apiFetch('/goals', { token, method: 'POST', body: { title: `[verify-http-bind] ${Date.now()}`, intentSummary: 'HTTP verify goal_run_id binding', checkpoints: [ { title: '执行', description: '单阶段绑定 smoke' }, ], }, }); if (!bindGoal.response.ok || !bindGoal.payload.goal?.id) { fail('http.bind.create', `${bindGoal.response.status}`); return; } const bindGoalId = bindGoal.payload.goal.id; pass('http.bind.create', bindGoalId); const agentRun = await apiFetch('/agent/runs', { token, method: 'POST', body: { session_id: bindSessionId, request_id: crypto.randomUUID(), goal_run_id: bindGoalId, user_message: { id: crypto.randomUUID(), role: 'user', content: [{ type: 'text', text: '开始执行目标' }], metadata: { userVisible: true, displayText: '开始执行目标' }, }, }, }); if (!agentRun.response.ok && agentRun.response.status !== 202) { fail('http.agentRun', `${agentRun.response.status} ${JSON.stringify(agentRun.payload)}`); return; } const runId = agentRun.payload.run?.id; if (!runId) { fail('http.agentRun', 'missing run id'); return; } extraRunIds.push(runId); pass('http.agentRun', runId); const [rows] = await pool.query( 'SELECT goal_run_id, goal_checkpoint_id FROM h5_agent_runs WHERE id = ? LIMIT 1', [runId], ); if (String(rows[0]?.goal_run_id) !== bindGoalId) { fail('http.agentRun.binding', `goal_run_id=${rows[0]?.goal_run_id}`); return; } pass('http.agentRun.binding', rows[0]?.goal_checkpoint_id ?? 'bound'); await pool.query('DELETE FROM h5_goal_runs WHERE id = ?', [bindGoalId]); } catch (err) { fail('fatal', err instanceof Error ? err.message : String(err)); } finally { if (seeded) { await cleanup(pool, { goalId: seeded.goal.id, runId: seeded.runId, extraRunIds, }).catch(() => {}); } await pool.end?.().catch(() => {}); } if (process.exitCode) { console.error('\nGoal Run HTTP local verify failed.'); } else { console.log('\nGoal Run HTTP local verify passed.'); } } main().catch((err) => { fail('fatal', err instanceof Error ? err.message : String(err)); });