import assert from 'node:assert/strict'; import test from 'node:test'; import { createGoalRunService, ensureGoalRunSchema } from './goal-run-service.mjs'; test('ensureGoalRunSchema creates goal tables and agent run columns', async () => { const queries = []; const pool = { async query(sql) { queries.push(String(sql)); return [[]]; }, }; const columnExists = async (table, column) => { if (table === 'h5_agent_runs' && ['goal_run_id', 'goal_checkpoint_id'].includes(column)) { return false; } return true; }; const indexExists = async () => false; await ensureGoalRunSchema(pool, { columnExists, indexExists }); assert.ok(queries.some((sql) => sql.includes('h5_goal_runs'))); assert.ok(queries.some((sql) => sql.includes('h5_goal_checkpoints'))); assert.ok(queries.some((sql) => sql.includes('goal_run_id'))); }); test('createGoalRun persists goal and default checkpoint', async () => { const store = { goals: new Map(), checkpoints: new Map() }; const pool = { async query(sql, params) { if (String(sql).startsWith('INSERT INTO h5_goal_runs')) { store.goals.set(params[0], { id: params[0], user_id: params[1], title: params[2], intent_summary: params[3], status: 'active', source_channel: params[5], current_checkpoint_id: null, context_json: params[8], memory_snapshot_json: params[9], created_at: params[10], updated_at: params[11], completed_at: null, priority: 5, source_session_id: params[6], source_message_id: params[7], }); return [{ affectedRows: 1 }]; } if (String(sql).startsWith('INSERT INTO h5_goal_checkpoints')) { store.checkpoints.set(params[0], { id: params[0], goal_run_id: params[1], sequence: params[2], title: params[3], description: params[4], status: params[5], agent_run_id: null, output_summary: null, output_artifact_ids: null, user_feedback: null, approved_at: null, created_at: params[6], updated_at: params[7], started_at: null, completed_at: null, }); return [{ affectedRows: 1 }]; } if (String(sql).startsWith('UPDATE h5_goal_runs SET current_checkpoint_id')) { const goal = store.goals.get(params[2]); if (goal) goal.current_checkpoint_id = params[0]; return [{ affectedRows: 1 }]; } if (String(sql).includes('FROM h5_goal_runs WHERE id =')) { const goal = store.goals.get(params[0]); return [[goal].filter(Boolean)]; } if (String(sql).includes('FROM h5_goal_checkpoints WHERE goal_run_id')) { return [[...store.checkpoints.values()].filter((item) => item.goal_run_id === params[0])]; } if (String(sql).includes('FROM h5_goal_runs') && String(sql).includes('status IN')) { return [[...store.goals.values()].filter((item) => item.user_id === params[0])]; } throw new Error(`Unexpected query: ${sql}`); }, }; const service = createGoalRunService({ pool, now: () => 1000 }); const created = await service.createGoalRun({ userId: 'user-1', title: '准备下季度产品规划', intentSummary: '收集竞品信息并输出规划草案', sourceSessionId: 'session-1', }); assert.equal(created.title, '准备下季度产品规划'); assert.equal(created.checkpoints.length, 1); assert.equal(created.checkpoints[0].title, '启动'); assert.equal(created.currentCheckpointId, created.checkpoints[0].id); const listed = await service.listGoalRuns({ userId: 'user-1' }); assert.equal(listed.length, 1); }); test('startNextCheckpoint and onAgentRunCompleted advance goal lifecycle', async () => { const store = { goals: new Map([ ['goal-1', { id: 'goal-1', user_id: 'user-1', title: '长期任务', intent_summary: '分阶段完成', status: 'active', source_channel: 'h5', current_checkpoint_id: 'cp-1', context_json: null, memory_snapshot_json: null, created_at: 1000, updated_at: 1000, completed_at: null, priority: 5, source_session_id: null, source_message_id: null, }], ]), checkpoints: new Map([ ['cp-1', { id: 'cp-1', goal_run_id: 'goal-1', sequence: 1, title: '启动', description: null, status: 'pending', agent_run_id: null, output_summary: null, output_artifact_ids: null, user_feedback: null, approved_at: null, created_at: 1000, updated_at: 1000, started_at: null, completed_at: null, }], ]), runs: new Map([ ['run-1', { id: 'run-1', goal_run_id: 'goal-1', goal_checkpoint_id: 'cp-1', status: 'succeeded', }], ]), }; const pool = { async query(sql, params) { const text = String(sql); if (text.includes('FROM h5_agent_runs') && text.includes('WHERE id = ?')) { return [[store.runs.get(params[0])].filter(Boolean)]; } if (text.startsWith('UPDATE h5_goal_checkpoints') && text.includes("SET status = 'running'")) { const checkpoint = store.checkpoints.get(params[3]); checkpoint.status = 'running'; checkpoint.agent_run_id = params[0]; checkpoint.started_at = params[1]; checkpoint.updated_at = params[2]; return [{ affectedRows: 1 }]; } if (text.startsWith('UPDATE h5_goal_runs') && text.includes("status = 'active'")) { const goal = store.goals.get(params[2]); goal.current_checkpoint_id = params[0]; goal.status = 'active'; goal.updated_at = params[1]; return [{ affectedRows: 1 }]; } if (text.includes("status = 'approved'") && text.includes('h5_goal_checkpoints')) { const checkpoint = store.checkpoints.get(params[4]); checkpoint.status = 'approved'; checkpoint.output_summary = params[0]; checkpoint.completed_at = params[1]; checkpoint.approved_at = params[2]; checkpoint.updated_at = params[3]; return [{ affectedRows: 1 }]; } if (text.includes('FROM h5_goal_checkpoints') && text.includes("status IN ('pending', 'failed')")) { return [[]]; } if (text.includes("SET status = 'completed'") && text.includes('h5_goal_runs')) { const goal = store.goals.get(params[3]); goal.status = 'completed'; goal.completed_at = params[2]; goal.updated_at = params[2]; return [{ affectedRows: 1 }]; } if (text.includes('FROM h5_goal_runs WHERE id = ? AND user_id = ?')) { const goal = store.goals.get(params[0]); if (!goal || goal.user_id !== params[1]) return [[]]; return [[goal]]; } if (text.includes('FROM h5_goal_checkpoints WHERE goal_run_id = ?')) { return [[...store.checkpoints.values()].filter((item) => item.goal_run_id === params[0])]; } if (text.includes('FROM h5_goal_checkpoints c') && text.includes('INNER JOIN')) { const checkpoint = store.checkpoints.get(params[0]); const goal = store.goals.get(checkpoint?.goal_run_id); if (!checkpoint || !goal) return [[]]; return [[{ ...checkpoint, user_id: goal.user_id, goal_status: goal.status }]]; } throw new Error(`Unexpected query: ${sql}`); }, }; const service = createGoalRunService({ pool, now: () => 2000 }); const started = await service.startNextCheckpoint({ userId: 'user-1', goalRunId: 'goal-1' }); assert.equal(started.checkpointId, 'cp-1'); assert.equal(store.checkpoints.get('cp-1').status, 'running'); const completed = await service.onAgentRunCompleted({ agentRunId: 'run-1', status: 'succeeded', outputSummary: '完成第一阶段', }); assert.equal(completed.handled, true); assert.equal(completed.goalCompleted, true); assert.equal(store.checkpoints.get('cp-1').status, 'approved'); assert.equal(store.goals.get('goal-1').status, 'completed'); }); test('onAgentRunCompleted requires approval between multi-checkpoint stages', async () => { const store = { goals: new Map([ ['goal-2', { id: 'goal-2', user_id: 'user-1', title: '多阶段任务', intent_summary: '两阶段', status: 'active', source_channel: 'h5', current_checkpoint_id: 'cp-1', context_json: null, memory_snapshot_json: null, created_at: 1000, updated_at: 1000, completed_at: null, priority: 5, source_session_id: null, source_message_id: null, }], ]), checkpoints: new Map([ ['cp-1', { id: 'cp-1', goal_run_id: 'goal-2', sequence: 1, title: '调研', description: null, status: 'running', agent_run_id: 'run-1', output_summary: null, output_artifact_ids: null, user_feedback: null, approved_at: null, created_at: 1000, updated_at: 1000, started_at: 1000, completed_at: null, }], ['cp-2', { id: 'cp-2', goal_run_id: 'goal-2', sequence: 2, title: '输出草案', description: null, status: 'pending', agent_run_id: null, output_summary: null, output_artifact_ids: null, user_feedback: null, approved_at: null, created_at: 1000, updated_at: 1000, started_at: null, completed_at: null, }], ]), runs: new Map([ ['run-1', { id: 'run-1', goal_run_id: 'goal-2', goal_checkpoint_id: 'cp-1', status: 'succeeded', }], ]), }; const pool = { async query(sql, params) { const text = String(sql); if (text.includes('FROM h5_agent_runs') && text.includes('WHERE id = ?')) { return [[store.runs.get(params[0])].filter(Boolean)]; } if (text.includes('COUNT(*) AS total')) { return [[{ total: store.checkpoints.size }]]; } if (text.includes('FROM h5_goal_checkpoints') && text.includes("status IN ('pending', 'failed')")) { return [[store.checkpoints.get('cp-2')].filter((item) => ['pending', 'failed'].includes(item.status))]; } if (text.includes("SET status = 'awaiting_approval'")) { store.checkpoints.get(params[3]).status = 'awaiting_approval'; return [{ affectedRows: 1 }]; } if (text.includes("SET status = 'awaiting_user'")) { store.goals.get('goal-2').status = 'awaiting_user'; return [{ affectedRows: 1 }]; } if (text.includes('FROM h5_goal_checkpoints c') && text.includes('INNER JOIN')) { const checkpoint = store.checkpoints.get(params[0]); const goal = store.goals.get(checkpoint?.goal_run_id); return [[{ ...checkpoint, user_id: goal.user_id, goal_status: goal.status }]]; } throw new Error(`Unexpected query: ${sql}`); }, }; const service = createGoalRunService({ pool, now: () => 2000 }); const completed = await service.onAgentRunCompleted({ agentRunId: 'run-1', status: 'succeeded', outputSummary: '调研完成', }); assert.equal(completed.handled, true); assert.equal(completed.awaitingApproval, true); assert.equal(store.goals.get('goal-2').status, 'awaiting_user'); assert.equal(store.checkpoints.get('cp-1').status, 'awaiting_approval'); assert.equal(store.checkpoints.get('cp-2').status, 'pending'); });