吸收数字员工PlanStep本地软租约

This commit is contained in:
baiyanyun
2026-06-10 22:29:52 +08:00
parent 909f11dae5
commit 6f2ccdf783
13 changed files with 604 additions and 49 deletions

View File

@@ -7,6 +7,8 @@ const mockState = vi.hoisted(() => ({
finishedRuns: [] as Array<Record<string, unknown>>,
dispatchEvents: [] as Array<Record<string, unknown>>,
governanceCommands: [] as Array<Record<string, unknown>>,
acquiredLeases: [] as Array<Record<string, unknown>>,
releasedLeases: [] as Array<Record<string, unknown>>,
settings: {
enabled: true,
catch_up_on_startup: true,
@@ -54,6 +56,25 @@ vi.mock("./stateService", () => ({
mockState.dispatchEvents.push(input);
return { eventId: `dispatch-${input.status}`, kind: `plan_step_dispatch_${input.status}`, payload: input };
},
acquireDigitalEmployeePlanStepLease: (input: Record<string, unknown>) => {
mockState.acquiredLeases.push(input);
const candidate = mockState.readyCandidates.find((item) => item.stepId === input.stepId);
if (candidate?.leaseActive || candidate?.skipReason === "lease_active") {
return {
ok: false,
reason: "lease_active",
lease: { lease_id: `lease-${input.stepId}`, state: "active", owner_device_id: "device-remote", acquired_at: "2026-06-07T00:59:00.000Z", expires_at: "2026-06-07T01:04:00.000Z" },
};
}
return {
ok: true,
lease: { lease_id: `lease-${input.stepId}`, state: "active", owner_device_id: "device-001", acquired_at: input.occurredAt, expires_at: "2026-06-07T01:05:10.000Z" },
};
},
releaseDigitalEmployeePlanStepLease: (input: Record<string, unknown>) => {
mockState.releasedLeases.push(input);
return { ok: true, lease: { lease_id: input.leaseId, state: "released", owner_device_id: "device-001", acquired_at: "2026-06-07T01:00:10.000Z", expires_at: "2026-06-07T01:05:10.000Z", released_at: input.occurredAt, release_reason: input.reason } };
},
recordDigitalEmployeeGovernanceCommand: (input: Record<string, unknown>) => {
mockState.governanceCommands.push(input);
return {
@@ -72,6 +93,8 @@ describe("digital employee scheduler service", () => {
mockState.finishedRuns = [];
mockState.dispatchEvents = [];
mockState.governanceCommands = [];
mockState.acquiredLeases = [];
mockState.releasedLeases = [];
mockState.dispatchPolicy = {
enabled: true,
max_per_sweep: 3,
@@ -156,6 +179,8 @@ describe("digital employee scheduler service", () => {
expect.objectContaining({ method: "POST" }),
);
expect(mockState.dispatchEvents.map((event) => event.status)).toEqual(["started", "completed"]);
expect(mockState.acquiredLeases).toEqual([expect.objectContaining({ stepId: "step-ready", triggerSource: "auto" })]);
expect(mockState.releasedLeases).toEqual([expect.objectContaining({ stepId: "step-ready", leaseId: "lease-step-ready", reason: "completed" })]);
expect(mockState.governanceCommands[0]).toMatchObject({
commandKind: "start_run",
planId: "plan-1",
@@ -179,9 +204,24 @@ describe("digital employee scheduler service", () => {
expect(result.readyStepDispatch).toMatchObject({ candidates: 1, dispatched: 0, failed: 1 });
expect(result.readyStepDispatch.errors[0]).toMatchObject({ stepId: "step-ready", error: "ComputerServer 502: bad gateway" });
expect(mockState.dispatchEvents.map((event) => event.status)).toEqual(["started", "failed"]);
expect(mockState.releasedLeases).toEqual([expect.objectContaining({ stepId: "step-ready", leaseId: "lease-step-ready", reason: "failed" })]);
expect(mockState.governanceCommands).toHaveLength(0);
});
it("skips ready plan steps with active leases", async () => {
mockState.schedules = [];
mockState.readyCandidates = [readyStepCandidate({ dispatchable: false, skipReason: "lease_active", leaseActive: true })];
const { runDigitalEmployeeSchedulerCheck } = await import("./schedulerService");
const result = await runDigitalEmployeeSchedulerCheck(new Date("2026-06-07T01:00:10.000Z"));
expect(result.readyStepDispatch).toMatchObject({ candidates: 1, dispatched: 0, skipped: 1, failed: 0 });
expect(fetch).not.toHaveBeenCalled();
expect(mockState.acquiredLeases).toEqual([expect.objectContaining({ stepId: "step-ready", triggerSource: "auto" })]);
expect(mockState.releasedLeases).toHaveLength(0);
expect(mockState.dispatchEvents).toHaveLength(0);
});
it("honors disabled ready step dispatch policy", async () => {
mockState.readyCandidates = [readyStepCandidate()];
mockState.dispatchPolicy = { ...mockState.dispatchPolicy, enabled: false };