吸收数字员工步骤治理闭环

This commit is contained in:
baiyanyun
2026-06-09 11:31:29 +08:00
parent 5ebc0a640b
commit ef8252e2dd
9 changed files with 1061 additions and 154 deletions

View File

@@ -425,8 +425,241 @@ describe("digital employee state service", () => {
scheduleRuns: [expect.objectContaining({ scheduleId: "schedule-morning", triggerKind: "manual" })],
});
});
it("applies skip governance to the real task, plan step, event, and outbox", async () => {
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
createGovernableTask();
const ids = recordDigitalEmployeeGovernanceCommand({
commandKind: "skip_task",
commandId: "command-skip-task",
planId: "plan-governance",
taskId: "task-governance",
runId: "run-governance",
stepId: "step-governance",
accepted: true,
message: "跳过当前任务",
payload: { reason: "用户选择跳过" },
result: { task_id: "task-governance", run_id: "run-governance", step_id: "step-governance" },
});
expect(ids).toEqual({
planId: "plan-governance",
taskId: "task-governance",
runId: "run-governance",
stepId: "step-governance",
});
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({
status: "skipped",
sync_status: "pending",
});
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({
status: "skipped",
sync_status: "pending",
});
expect(mockState.db?.runs.get("run-governance")).toMatchObject({
status: "skipped",
finished_at: "2026-06-07T08:00:00.000Z",
sync_status: "pending",
});
expect(JSON.parse(mockState.db?.tasks.get("task-governance")?.payload ?? "{}")).toMatchObject({
lastGovernanceCommand: {
commandKind: "skip_task",
taskId: "task-governance",
stepId: "step-governance",
status: "skipped",
},
});
expect(mockState.db?.events.get("run-governance:skip_task:command-skip-task")).toMatchObject({
kind: "governance_skip_task",
message: "跳过当前任务",
task_id: "task-governance",
});
expect(mockState.db?.outbox.get("task:upsert:task-governance")).toMatchObject({ entity_type: "task" });
expect(mockState.db?.outbox.get("plan_step:upsert:step-governance")).toMatchObject({ entity_type: "plan_step" });
expect(mockState.db?.outbox.get("run:upsert:run-governance")).toMatchObject({ entity_type: "run" });
});
it("creates a retry run and preserves the failed source run", async () => {
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
createGovernableTask({ taskStatus: "failed", runStatus: "failed", finishedAt: "2026-06-07T07:30:00.000Z" });
const ids = recordDigitalEmployeeGovernanceCommand({
commandKind: "retry_task",
commandId: "command-retry-task",
planId: "plan-governance",
taskId: "task-governance",
runId: "run-governance",
stepId: "step-governance",
accepted: true,
payload: { reason: "失败后重试", prompt: "重新执行任务" },
result: { task_id: "task-governance", run_id: "run-governance", step_id: "step-governance" },
});
expect(ids.runId).toBe("task-governance:run:retry_task:command-retry-task");
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({ status: "running" });
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({ status: "running" });
expect(mockState.db?.runs.get("run-governance")).toMatchObject({
status: "failed",
finished_at: "2026-06-07T07:30:00.000Z",
});
expect(mockState.db?.runs.get(ids.runId)).toMatchObject({
plan_id: "plan-governance",
task_id: "task-governance",
status: "running",
finished_at: null,
});
expect(JSON.parse(mockState.db?.runs.get(ids.runId)?.payload ?? "{}")).toMatchObject({
commandKind: "retry_task",
sourceRunId: "run-governance",
attemptNo: 2,
});
expect(mockState.db?.outbox.get(`run:upsert:${ids.runId}`)).toMatchObject({ entity_type: "run" });
});
it("cancels the current task run without deleting run history", async () => {
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
createGovernableTask();
mockState.db?.runs.set("run-history", {
id: "run-history",
plan_id: "plan-governance",
task_id: "task-governance",
status: "completed",
started_at: "2026-06-07T07:00:00.000Z",
finished_at: "2026-06-07T07:05:00.000Z",
payload: JSON.stringify({ source: "history" }),
sync_status: "synced",
created_at: "2026-06-07T07:00:00.000Z",
updated_at: "2026-06-07T07:05:00.000Z",
});
recordDigitalEmployeeGovernanceCommand({
commandKind: "cancel_task",
commandId: "command-cancel-task",
planId: "plan-governance",
taskId: "task-governance",
runId: "run-governance",
stepId: "step-governance",
accepted: true,
payload: { reason: "用户取消" },
});
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({ status: "cancelled" });
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({ status: "cancelled" });
expect(mockState.db?.runs.get("run-governance")).toMatchObject({
status: "cancelled",
finished_at: "2026-06-07T08:00:00.000Z",
});
expect(mockState.db?.runs.get("run-history")).toMatchObject({
status: "completed",
finished_at: "2026-06-07T07:05:00.000Z",
});
});
it("resumes paused tasks and records provided task input", async () => {
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
createGovernableTask({ taskStatus: "paused", stepStatus: "paused", runStatus: "paused" });
recordDigitalEmployeeGovernanceCommand({
commandKind: "provide_task_input",
commandId: "command-provide-input",
planId: "plan-governance",
taskId: "task-governance",
runId: "run-governance",
stepId: "step-governance",
accepted: true,
payload: { input: "继续使用最新客户名单" },
});
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({ status: "paused" });
expect(JSON.parse(mockState.db?.tasks.get("task-governance")?.payload ?? "{}")).toMatchObject({
lastGovernanceCommand: {
commandKind: "provide_task_input",
payload: { input: "继续使用最新客户名单" },
},
});
recordDigitalEmployeeGovernanceCommand({
commandKind: "resume_task",
commandId: "command-resume-task",
planId: "plan-governance",
taskId: "task-governance",
runId: "run-governance",
stepId: "step-governance",
accepted: true,
payload: { reason: "资料已补充" },
});
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({ status: "running" });
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({ status: "running" });
expect(mockState.db?.runs.get("run-governance")).toMatchObject({ status: "running" });
expect(mockState.db?.events.get("run-governance:provide_task_input:command-provide-input")).toMatchObject({
kind: "governance_provide_task_input",
});
expect(mockState.db?.events.get("run-governance:resume_task:command-resume-task")).toMatchObject({
kind: "governance_resume_task",
});
});
});
function createGovernableTask(options: {
taskStatus?: string;
stepStatus?: string;
runStatus?: string;
finishedAt?: string | null;
} = {}): void {
mockState.db?.plans.set("plan-governance", {
id: "plan-governance",
title: "步骤治理计划",
objective: "验证任务治理闭环",
status: "running",
payload: JSON.stringify({ source: "test" }),
sync_status: "synced",
created_at: "2026-06-07T07:00:00.000Z",
updated_at: "2026-06-07T07:00:00.000Z",
});
mockState.db?.planSteps.set("step-governance", {
id: "step-governance",
plan_id: "plan-governance",
title: "执行治理步骤",
status: options.stepStatus ?? options.taskStatus ?? "running",
seq: 1,
depends_on: JSON.stringify([]),
preferred_skill_ids: JSON.stringify(["qimingclaw-task-agent"]),
payload: JSON.stringify({ source: "test" }),
sync_status: "synced",
created_at: "2026-06-07T07:00:00.000Z",
updated_at: "2026-06-07T07:00:00.000Z",
});
mockState.db?.tasks.set("task-governance", {
id: "task-governance",
plan_id: "plan-governance",
title: "执行治理任务",
status: options.taskStatus ?? "running",
assigned_skill_id: "qimingclaw-task-agent",
payload: JSON.stringify({ source: "test", stepId: "step-governance" }),
sync_status: "synced",
created_at: "2026-06-07T07:00:00.000Z",
updated_at: "2026-06-07T07:00:00.000Z",
});
mockState.db?.runs.set("run-governance", {
id: "run-governance",
plan_id: "plan-governance",
task_id: "task-governance",
status: options.runStatus ?? "running",
started_at: "2026-06-07T07:10:00.000Z",
finished_at: options.finishedAt ?? null,
payload: JSON.stringify({ source: "test" }),
sync_status: "synced",
created_at: "2026-06-07T07:10:00.000Z",
updated_at: "2026-06-07T07:10:00.000Z",
});
}
interface TestPlanRow {
id: string;
title: string;
@@ -615,9 +848,31 @@ class TestDb {
}
private get(sql: string, args: unknown[]): unknown {
if (sql.startsWith("SELECT COUNT(*) AS count FROM digital_runs")) {
return { count: Array.from(this.runs.values()).filter((run) => run.task_id === args[0]).length };
}
if (sql.startsWith("SELECT title, objective FROM digital_plans")) {
return this.plans.get(args[0] as string);
}
if (sql.includes("FROM digital_tasks") && sql.includes("WHERE id = ?")) {
return this.tasks.get(args[0] as string);
}
if (sql.includes("FROM digital_runs") && sql.includes("WHERE id = ?")) {
return this.runs.get(args[0] as string);
}
if (sql.includes("FROM digital_runs") && sql.includes("WHERE task_id = ?")) {
return Array.from(this.runs.values())
.filter((run) => run.task_id === args[0])
.sort((left, right) => right.updated_at.localeCompare(left.updated_at) || right.created_at.localeCompare(left.created_at))[0];
}
if (sql.startsWith("SELECT id FROM digital_plan_steps")) {
return Array.from(this.planSteps.values())
.filter((step) => step.plan_id === args[0])
.sort((left, right) => left.seq - right.seq || left.created_at.localeCompare(right.created_at))[0];
}
if (sql.includes("FROM digital_plan_steps") && sql.includes("WHERE id = ?")) {
return this.planSteps.get(args[0] as string);
}
if (sql.startsWith("SELECT id, plan_id, task_id, run_id, title, status, payload, created_at FROM digital_approvals")) {
return this.approvals.get(args[0] as string);
}
@@ -826,6 +1081,67 @@ class TestDb {
return { changes: 1 };
}
if (sql.startsWith("UPDATE digital_tasks")) {
const [status, payload, updatedAt, id] = args as [string, string, string, string];
const previous = this.tasks.get(id);
if (previous) {
this.tasks.set(id, {
...previous,
status,
payload,
updated_at: updatedAt,
sync_status: "pending",
});
}
return { changes: previous ? 1 : 0 };
}
if (sql.startsWith("UPDATE digital_plan_steps")) {
const [status, payload, updatedAt, id] = args as [string, string, string, string];
const previous = this.planSteps.get(id);
if (previous) {
this.planSteps.set(id, {
...previous,
status,
payload,
updated_at: updatedAt,
sync_status: "pending",
});
}
return { changes: previous ? 1 : 0 };
}
if (sql.startsWith("UPDATE digital_runs") && sql.includes("payload") && sql.includes("finished_at")) {
const [status, payload, finishedAt, updatedAt, id] = args as [string, string, string, string, string];
const previous = this.runs.get(id);
if (previous) {
this.runs.set(id, {
...previous,
status,
payload,
finished_at: finishedAt,
updated_at: updatedAt,
sync_status: "pending",
});
}
return { changes: previous ? 1 : 0 };
}
if (sql.startsWith("UPDATE digital_runs") && sql.includes("payload")) {
const [status, payload, updatedAt, id] = args as [string, string, string, string];
const previous = this.runs.get(id);
if (previous) {
this.runs.set(id, {
...previous,
status,
payload,
updated_at: updatedAt,
sync_status: "pending",
});
}
return { changes: previous ? 1 : 0 };
}
if (sql.startsWith("UPDATE digital_runs")) {
const [status, finishedAt, updatedAt, id] = args as [string, string, string, string];
const previous = this.runs.get(id);