吸收数字员工多步审批跨端回写

This commit is contained in:
baiyanyun
2026-06-11 10:44:12 +08:00
parent e9c76c59ca
commit 43418d0453
16 changed files with 1240 additions and 200 deletions

View File

@@ -297,6 +297,84 @@ describe("digital employee sync service", () => {
expect(result.policy.approval_required_actions).toEqual(["managed_service.restart.fileServer"]);
});
it("pulls remote approval updates into local approval workflow records", async () => {
mockState.fetch.mockResolvedValue(jsonResponse({
success: true,
data: {
approvals: [
{
approval_id: "approval-remote-1",
title: "高风险动作审批",
status: "pending",
plan_id: "plan-1",
task_id: "task-1",
run_id: "run-1",
updated_at: "2026-06-07T08:03:00.000Z",
payload: {
workflow: {
workflow_id: "workflow-remote-1",
current_step_id: "step-2",
steps: [
{ step_id: "step-1", label: "主管审批", status: "approved" },
{ step_id: "step-2", label: "风控复核", status: "pending", participant_label: "风控" },
],
},
},
},
],
},
}));
const { pullDigitalEmployeeApprovalUpdates } = await import("./syncService");
const result = await pullDigitalEmployeeApprovalUpdates({ force: true });
expect(result).toMatchObject({
ok: true,
endpoint: "https://manage.example.com/api/digital-employee/approvals/updates",
applied: 1,
ignored: 0,
});
expect(mockState.fetch).toHaveBeenCalledWith(
"https://manage.example.com/api/digital-employee/approvals/updates",
expect.objectContaining({ method: "GET" }),
);
const approval = mockState.db?.approvals.get("approval-remote-1");
expect(approval).toMatchObject({ title: "高风险动作审批", status: "pending", sync_status: "pending" });
expect(JSON.parse(approval?.payload ?? "{}")).toMatchObject({
workflow: {
workflow_id: "workflow-remote-1",
current_step_id: "step-2",
},
remote_updated_at: "2026-06-07T08:03:00.000Z",
});
expect(Array.from(mockState.db?.events.values() ?? [])).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "digital_workday_approval_updates_pulled" }),
]));
});
it("submits approval decisions to management and records submission events", async () => {
mockState.fetch.mockResolvedValue(jsonResponse({ success: true, data: { accepted: true } }));
const { submitDigitalEmployeeApprovalDecision } = await import("./syncService");
const result = await submitDigitalEmployeeApprovalDecision({ approval_id: "approval-1", decision: "approved" });
expect(result).toMatchObject({
ok: true,
endpoint: "https://manage.example.com/api/digital-employee/approvals/decisions",
submitted_at: "2026-06-07T08:00:00.000Z",
});
expect(mockState.fetch).toHaveBeenCalledWith(
"https://manage.example.com/api/digital-employee/approvals/decisions",
expect.objectContaining({
method: "POST",
body: expect.stringContaining("approval-1"),
}),
);
expect(Array.from(mockState.db?.events.values() ?? [])).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "digital_workday_approval_decision_submitted" }),
]));
});
it.each([
["skillPolicies"],
["policies"],
@@ -1501,6 +1579,8 @@ interface TestSyncEntityRow {
sync_status: string;
sync_error: string | null;
last_synced_at: string | null;
created_at?: string;
updated_at?: string;
}
interface TestAttemptRow {
@@ -1646,6 +1726,11 @@ class TestDb {
return this.approvals.get(id);
}
if (sql.startsWith("SELECT id, plan_id, task_id, run_id, title, status, payload, created_at, updated_at FROM digital_approvals")) {
const [id] = args as [string];
return this.approvals.get(id);
}
if (sql.startsWith("SELECT key AS title, status, content AS summary, payload FROM digital_memories")) {
const [id] = args as [string];
return this.memories.get(id);
@@ -1817,6 +1902,37 @@ class TestDb {
return { changes: 1 };
}
if (sql.startsWith("INSERT INTO digital_approvals")) {
const [id, planId, taskId, runId, title, status, payload, createdAt, updatedAt] = args as [
string,
string,
string,
string,
string,
string,
string,
string,
string,
];
const previous = this.approvals.get(id);
this.approvals.set(id, {
id,
plan_id: planId,
task_id: taskId,
run_id: runId,
title,
status,
payload,
remote_id: previous?.remote_id ?? null,
sync_status: previous?.sync_status === "synced" ? "pending" : previous?.sync_status ?? "pending",
sync_error: previous?.sync_error ?? null,
last_synced_at: previous?.last_synced_at ?? null,
created_at: previous?.created_at ?? createdAt,
updated_at: updatedAt,
});
return { changes: 1 };
}
if (sql.startsWith("INSERT INTO digital_sync_outbox")) {
const [id, entityType, entityId, operation, payload, createdAt, updatedAt] = args as [
string,