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

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

@@ -320,6 +320,80 @@ describe("digital employee sync service", () => {
sync_error: null,
});
});
it("syncs plan step business views and acknowledgements", async () => {
insertEntity("plan_step", "step-1");
insertOutbox(
"plan_step:upsert:step-1",
"plan_step",
"step-1",
JSON.stringify({
planId: "plan-1",
title: "整理客户列表",
status: "pending",
seq: 2,
dependsOn: ["step-0"],
preferredSkillIds: ["qimingclaw-mcp-tool-crm-crm-search"],
}),
);
mockState.fetch.mockResolvedValue(
jsonResponse({
success: true,
acked: [
{
entity_type: "plan_step",
entity_id: "step-1",
remote_id: "remote-step-1",
},
],
}),
);
const { flushDigitalEmployeeSyncOutbox } = await import("./syncService");
const status = await flushDigitalEmployeeSyncOutbox({ force: true });
expect(lastSyncRequestBody()).toMatchObject({
items: [
expect.objectContaining({
entity_type: "plan_step",
business_view: expect.objectContaining({
entity_type: "plan_step",
local_id: "step-1",
plan_id: "plan-1",
step_id: "step-1",
title: "整理客户列表",
status: "pending",
seq: 2,
depends_on: ["step-0"],
preferred_skill_ids: ["qimingclaw-mcp-tool-crm-crm-search"],
}),
}),
],
});
expect(status.failed).toBe(0);
expect(readEntity("plan_step", "step-1")).toMatchObject({
sync_status: "synced",
remote_id: "remote-step-1",
sync_error: null,
});
});
it("marks plan step business failures as failed", async () => {
insertEntity("plan_step", "step-rejected");
insertOutbox("plan_step:upsert:step-rejected", "plan_step", "step-rejected");
mockState.fetch.mockResolvedValue(
jsonResponse({ code: "0001", message: "步骤同步被拒绝", data: null }),
);
const { flushDigitalEmployeeSyncOutbox } = await import("./syncService");
const status = await flushDigitalEmployeeSyncOutbox({ force: true });
expect(status.failed).toBe(1);
expect(readEntity("plan_step", "step-rejected")).toMatchObject({
sync_status: "failed",
sync_error: "步骤同步被拒绝",
});
});
});
function insertPlan(id: string): void {
@@ -340,13 +414,18 @@ function insertEntity(entityType: string, id: string): void {
});
}
function insertOutbox(id: string, entityType: string, entityId: string): void {
function insertOutbox(
id: string,
entityType: string,
entityId: string,
payload = '{"ok":true}',
): void {
mockState.db?.outbox.set(id, {
id,
entity_type: entityType,
entity_id: entityId,
operation: "upsert",
payload: '{"ok":true}',
payload,
status: "pending",
attempts: 0,
last_attempt_at: null,
@@ -376,6 +455,7 @@ function entityMap(
): Map<string, TestSyncEntityRow> | undefined {
if (!mockState.db) return undefined;
if (entityType === "plan") return mockState.db.plans;
if (entityType === "plan_step") return mockState.db.planSteps;
if (entityType === "artifact") return mockState.db.artifacts;
if (entityType === "approval") return mockState.db.approvals;
return undefined;
@@ -441,6 +521,7 @@ interface TestAttemptRow {
class TestDb {
outbox = new Map<string, TestOutboxRow>();
plans = new Map<string, TestSyncEntityRow>();
planSteps = new Map<string, TestSyncEntityRow>();
artifacts = new Map<string, TestSyncEntityRow>();
approvals = new Map<string, TestSyncEntityRow>();
attempts: TestAttemptRow[] = [];
@@ -543,6 +624,11 @@ class TestDb {
return this.approvals.get(id);
}
if (sql.startsWith("SELECT title, status, NULL AS objective, payload FROM digital_plan_steps")) {
const [id] = args as [string];
return this.planSteps.get(id);
}
if (
sql.includes("COUNT(*) AS count FROM digital_sync_outbox WHERE status = ?")
) {
@@ -640,7 +726,7 @@ class TestDb {
return { changes: before - this.attempts.length };
}
if (/^UPDATE digital_(plans|artifacts|approvals) SET sync_status = 'synced'/.test(sql)) {
if (/^UPDATE digital_(plans|plan_steps|artifacts|approvals) SET sync_status = 'synced'/.test(sql)) {
const [remoteId, lastSyncedAt, id] = args as [string | null, string, string];
const row = this.entityRowsForUpdate(sql).get(id);
if (row) {
@@ -652,7 +738,7 @@ class TestDb {
return { changes: row ? 1 : 0 };
}
if (/^UPDATE digital_(plans|artifacts|approvals) SET sync_status = 'failed'/.test(sql)) {
if (/^UPDATE digital_(plans|plan_steps|artifacts|approvals) SET sync_status = 'failed'/.test(sql)) {
const [syncError, id] = args as [string, string];
const row = this.entityRowsForUpdate(sql).get(id);
if (row) {
@@ -668,6 +754,7 @@ class TestDb {
private entityRowsForUpdate(sql: string): Map<string, TestSyncEntityRow> {
if (sql.startsWith("UPDATE digital_artifacts")) return this.artifacts;
if (sql.startsWith("UPDATE digital_approvals")) return this.approvals;
if (sql.startsWith("UPDATE digital_plan_steps")) return this.planSteps;
return this.plans;
}
}