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>
335 lines
12 KiB
JavaScript
335 lines
12 KiB
JavaScript
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');
|
|
});
|