吸收数字员工计划步骤依赖图

This commit is contained in:
baiyanyun
2026-06-09 10:21:12 +08:00
parent e4715bbabb
commit f35d945d9c
10 changed files with 641 additions and 74 deletions

View File

@@ -255,6 +255,16 @@ describe("digital employee state service", () => {
status: "pending",
assigned_skill_id: "qimingclaw-mcp-tool-crm-crm-search",
});
expect(mockState.db?.planSteps.get("collect-crm")).toMatchObject({
plan_id: "plan-customer-followup",
title: "整理客户列表",
status: "pending",
seq: 1,
preferred_skill_ids: JSON.stringify(["qimingclaw-mcp-tool-crm-crm-search"]),
});
expect(JSON.parse(mockState.db?.tasks.get("task-collect-crm")?.payload ?? "{}")).toMatchObject({
stepId: "collect-crm",
});
expect(mockState.db?.runs.get("governance-run-command-1")).toMatchObject({
plan_id: "plan-customer-followup",
task_id: "governance-task-command-1",
@@ -270,9 +280,54 @@ describe("digital employee state service", () => {
entity_id: "plan-customer-followup",
status: "pending",
});
expect(mockState.db?.outbox.get("plan_step:upsert:collect-crm")).toMatchObject({
entity_type: "plan_step",
entity_id: "collect-crm",
status: "pending",
});
expect(mockState.ensureSchemaCalls).toBeGreaterThan(0);
});
it("creates a default plan step for top-level plan tasks", async () => {
const {
readDigitalEmployeeRuntimeRecords,
recordDigitalEmployeeGovernanceCommand,
} = await import("./stateService");
recordDigitalEmployeeGovernanceCommand({
commandKind: "create_plan",
commandId: "command-default-step",
accepted: true,
payload: {
title: "客户回访计划",
tasks: [{ task_id: "task-call", title: "拨打客户电话" }],
},
result: { plan_id: "plan-default-step", status: "draft" },
});
expect(mockState.db?.planSteps.get("plan-default-step:step:default")).toMatchObject({
plan_id: "plan-default-step",
title: "默认执行步骤",
seq: 1,
});
expect(JSON.parse(mockState.db?.tasks.get("task-call")?.payload ?? "{}")).toMatchObject({
stepId: "plan-default-step:step:default",
});
expect(mockState.db?.outbox.get("plan_step:upsert:plan-default-step:step:default")).toMatchObject({
entity_type: "plan_step",
entity_id: "plan-default-step:step:default",
});
expect(readDigitalEmployeeRuntimeRecords()).toMatchObject({
planSteps: [
expect.objectContaining({
id: "plan-default-step:step:default",
planId: "plan-default-step",
title: "默认执行步骤",
}),
],
});
});
it("preserves created plan title across follow-up governance commands", async () => {
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
@@ -324,6 +379,20 @@ interface TestTaskRow {
updated_at: string;
}
interface TestPlanStepRow {
id: string;
plan_id: string;
title: string;
status: string;
seq: number;
depends_on: string;
preferred_skill_ids: string;
payload: string;
sync_status: string;
created_at: string;
updated_at: string;
}
interface TestRunRow {
id: string;
plan_id: string;
@@ -391,6 +460,7 @@ interface TestOutboxRow {
class TestDb {
plans = new Map<string, TestPlanRow>();
planSteps = new Map<string, TestPlanStepRow>();
tasks = new Map<string, TestTaskRow>();
runs = new Map<string, TestRunRow>();
events = new Map<string, TestEventRow>();
@@ -409,12 +479,23 @@ class TestDb {
} {
const normalized = sql.replace(/\s+/g, " ").trim();
return {
all: () => [],
all: (...args: unknown[]) => this.all(normalized, args),
get: (...args: unknown[]) => this.get(normalized, args),
run: (...args: unknown[]) => this.run(normalized, args),
};
}
private all(sql: string, _args: unknown[]): unknown[] {
if (sql.includes("FROM digital_plans")) return Array.from(this.plans.values());
if (sql.includes("FROM digital_plan_steps")) return Array.from(this.planSteps.values());
if (sql.includes("FROM digital_tasks")) return Array.from(this.tasks.values());
if (sql.includes("FROM digital_runs")) return Array.from(this.runs.values());
if (sql.includes("FROM digital_events")) return Array.from(this.events.values());
if (sql.includes("FROM digital_artifacts")) return Array.from(this.artifacts.values());
if (sql.includes("FROM digital_approvals")) return Array.from(this.approvals.values());
return [];
}
private get(sql: string, args: unknown[]): unknown {
if (sql.startsWith("SELECT title, objective FROM digital_plans")) {
return this.plans.get(args[0] as string);
@@ -450,6 +531,36 @@ class TestDb {
return { changes: 1 };
}
if (sql.startsWith("INSERT INTO digital_plan_steps")) {
const [
id,
planId,
title,
status,
seq,
dependsOn,
preferredSkillIds,
payload,
createdAt,
updatedAt,
] = args as [string, string, string, string, number, string, string, string, string, string];
const previous = this.planSteps.get(id);
this.planSteps.set(id, {
id,
plan_id: planId,
title,
status,
seq,
depends_on: dependsOn,
preferred_skill_ids: preferredSkillIds,
payload,
sync_status: previous?.sync_status === "synced" ? "pending" : previous?.sync_status ?? "pending",
created_at: previous?.created_at ?? createdAt,
updated_at: updatedAt,
});
return { changes: 1 };
}
if (sql.startsWith("INSERT INTO digital_tasks")) {
const [id, planId, title, status, assignedSkillId, payload, createdAt, updatedAt] = args as [
string,