feat(goal-run): add multi-checkpoint goal orchestration with H5 and admin surfaces.
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>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
export function parseCanaryUserIds(raw = process.env.GOAL_RUN_CANARY_USER_IDS) {
|
||||
return String(raw ?? '')
|
||||
.split(/[,;\s]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export async function resolveCanaryVerifyUser(pool, {
|
||||
usernameFallback = process.env.VERIFY_GOAL_RUN_USER
|
||||
?? process.env.VERIFY_LLM_ROUTER_USER
|
||||
?? 'john2',
|
||||
} = {}) {
|
||||
const canaryIds = parseCanaryUserIds();
|
||||
const preferredId = canaryIds[0] ?? null;
|
||||
if (preferredId) {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id, username FROM h5_users WHERE id = ? LIMIT 1',
|
||||
[preferredId],
|
||||
);
|
||||
if (rows[0]?.id) {
|
||||
return {
|
||||
userId: String(rows[0].id),
|
||||
username: String(rows[0].username ?? usernameFallback),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id, username FROM h5_users WHERE username = ? LIMIT 1',
|
||||
[usernameFallback],
|
||||
);
|
||||
if (!rows[0]?.id) return null;
|
||||
return {
|
||||
userId: String(rows[0].id),
|
||||
username: String(rows[0].username ?? usernameFallback),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
#!/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=<uuid> 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));
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
#!/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));
|
||||
});
|
||||
Reference in New Issue
Block a user