吸收数字员工周期调度闭环
This commit is contained in:
@@ -354,6 +354,77 @@ describe("digital employee state service", () => {
|
||||
status: "approved",
|
||||
});
|
||||
});
|
||||
|
||||
it("records schedule lifecycle commands and manual runs", async () => {
|
||||
const {
|
||||
readDigitalEmployeeRuntimeRecords,
|
||||
recordDigitalEmployeeGovernanceCommand,
|
||||
} = await import("./stateService");
|
||||
|
||||
recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "create_schedule",
|
||||
commandId: "command-schedule-create",
|
||||
planId: "plan-scheduled",
|
||||
scheduleId: "schedule-morning",
|
||||
accepted: true,
|
||||
payload: {
|
||||
name: "晨间巡检",
|
||||
prompt: "执行晨间巡检",
|
||||
cron_expr: "0 9 * * *",
|
||||
},
|
||||
result: { schedule_id: "schedule-morning", status: "enabled" },
|
||||
});
|
||||
|
||||
expect(mockState.db?.schedules.get("schedule-morning")).toMatchObject({
|
||||
id: "schedule-morning",
|
||||
plan_id: "plan-scheduled",
|
||||
name: "晨间巡检",
|
||||
cron_expr: "0 9 * * *",
|
||||
status: "enabled",
|
||||
enabled: 1,
|
||||
});
|
||||
expect(mockState.db?.outbox.get("schedule:upsert:schedule-morning")).toMatchObject({
|
||||
entity_type: "schedule",
|
||||
entity_id: "schedule-morning",
|
||||
});
|
||||
|
||||
recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "pause_schedule",
|
||||
commandId: "command-schedule-pause",
|
||||
planId: "plan-scheduled",
|
||||
scheduleId: "schedule-morning",
|
||||
accepted: true,
|
||||
result: { schedule_id: "schedule-morning", status: "paused" },
|
||||
});
|
||||
expect(mockState.db?.schedules.get("schedule-morning")).toMatchObject({
|
||||
status: "paused",
|
||||
enabled: 0,
|
||||
});
|
||||
|
||||
recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "run_schedule_now",
|
||||
commandId: "command-schedule-run",
|
||||
planId: "plan-scheduled",
|
||||
scheduleId: "schedule-morning",
|
||||
accepted: true,
|
||||
payload: { source: "test" },
|
||||
result: { schedule_id: "schedule-morning", status: "running" },
|
||||
});
|
||||
|
||||
expect(mockState.db?.scheduleRuns.get("schedule-morning:2026-06-07T08-00-00-000Z:1")).toMatchObject({
|
||||
schedule_id: "schedule-morning",
|
||||
plan_id: "plan-scheduled",
|
||||
status: "running",
|
||||
trigger_kind: "manual",
|
||||
});
|
||||
expect(mockState.db?.outbox.get("schedule_run:upsert:schedule-morning:2026-06-07T08-00-00-000Z:1")).toMatchObject({
|
||||
entity_type: "schedule_run",
|
||||
});
|
||||
expect(readDigitalEmployeeRuntimeRecords()).toMatchObject({
|
||||
schedules: [expect.objectContaining({ id: "schedule-morning", status: "paused" })],
|
||||
scheduleRuns: [expect.objectContaining({ scheduleId: "schedule-morning", triggerKind: "manual" })],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
interface TestPlanRow {
|
||||
@@ -445,6 +516,49 @@ interface TestApprovalRow {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface TestScheduleRow {
|
||||
id: string;
|
||||
plan_id: string | null;
|
||||
name: string;
|
||||
description: string | null;
|
||||
cron_expr: string;
|
||||
timezone: string;
|
||||
status: string;
|
||||
enabled: number;
|
||||
prompt: string | null;
|
||||
command: string | null;
|
||||
payload: string;
|
||||
next_run_at: string | null;
|
||||
last_run_at: string | null;
|
||||
catch_up_on_startup: number;
|
||||
max_catch_up_runs: number;
|
||||
max_run_history: number;
|
||||
failure_count: number;
|
||||
last_error: string | null;
|
||||
sync_status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at: string | null;
|
||||
}
|
||||
|
||||
interface TestScheduleRunRow {
|
||||
id: string;
|
||||
schedule_id: string;
|
||||
plan_id: string | null;
|
||||
run_id: string | null;
|
||||
due_at: string;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
status: string;
|
||||
attempt_no: number;
|
||||
trigger_kind: string;
|
||||
error: string | null;
|
||||
payload: string;
|
||||
sync_status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface TestOutboxRow {
|
||||
id: string;
|
||||
entity_type: string;
|
||||
@@ -466,6 +580,8 @@ class TestDb {
|
||||
events = new Map<string, TestEventRow>();
|
||||
artifacts = new Map<string, TestArtifactRow>();
|
||||
approvals = new Map<string, TestApprovalRow>();
|
||||
schedules = new Map<string, TestScheduleRow>();
|
||||
scheduleRuns = new Map<string, TestScheduleRunRow>();
|
||||
outbox = new Map<string, TestOutboxRow>();
|
||||
|
||||
transaction<T>(callback: () => T): () => T {
|
||||
@@ -488,6 +604,8 @@ class TestDb {
|
||||
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_schedules")) return Array.from(this.schedules.values());
|
||||
if (sql.includes("FROM digital_schedule_runs")) return Array.from(this.scheduleRuns.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());
|
||||
@@ -503,6 +621,12 @@ class TestDb {
|
||||
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);
|
||||
}
|
||||
if (sql.includes("FROM digital_schedules") && sql.includes("WHERE id = ?")) {
|
||||
return this.schedules.get(args[0] as string);
|
||||
}
|
||||
if (sql.includes("FROM digital_schedule_runs") && sql.includes("WHERE id = ?")) {
|
||||
return this.scheduleRuns.get(args[0] as string);
|
||||
}
|
||||
return { count: 0 };
|
||||
}
|
||||
|
||||
@@ -561,6 +685,94 @@ class TestDb {
|
||||
return { changes: 1 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("INSERT INTO digital_schedules")) {
|
||||
const [
|
||||
id,
|
||||
planId,
|
||||
name,
|
||||
description,
|
||||
cronExpr,
|
||||
timezone,
|
||||
status,
|
||||
enabled,
|
||||
prompt,
|
||||
command,
|
||||
payload,
|
||||
nextRunAt,
|
||||
lastRunAt,
|
||||
catchUpOnStartup,
|
||||
maxCatchUpRuns,
|
||||
maxRunHistory,
|
||||
failureCount,
|
||||
lastError,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
] = args as [string, string | null, string, string | null, string, string, string, number, string | null, string | null, string, string | null, string | null, number, number, number, number, string | null, string, string, string | null];
|
||||
const previous = this.schedules.get(id);
|
||||
this.schedules.set(id, {
|
||||
id,
|
||||
plan_id: planId,
|
||||
name,
|
||||
description,
|
||||
cron_expr: cronExpr,
|
||||
timezone,
|
||||
status,
|
||||
enabled,
|
||||
prompt,
|
||||
command,
|
||||
payload,
|
||||
next_run_at: nextRunAt,
|
||||
last_run_at: lastRunAt,
|
||||
catch_up_on_startup: catchUpOnStartup,
|
||||
max_catch_up_runs: maxCatchUpRuns,
|
||||
max_run_history: maxRunHistory,
|
||||
failure_count: failureCount,
|
||||
last_error: lastError,
|
||||
sync_status: previous?.sync_status === "synced" ? "pending" : previous?.sync_status ?? "pending",
|
||||
created_at: previous?.created_at ?? createdAt,
|
||||
updated_at: updatedAt,
|
||||
deleted_at: deletedAt,
|
||||
});
|
||||
return { changes: 1 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("INSERT INTO digital_schedule_runs")) {
|
||||
const [id, scheduleId, planId, runId, dueAt, startedAt, status, attemptNo, triggerKind, payload, createdAt, updatedAt] = args as [
|
||||
string,
|
||||
string,
|
||||
string | null,
|
||||
string | null,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
number,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
];
|
||||
const previous = this.scheduleRuns.get(id);
|
||||
this.scheduleRuns.set(id, {
|
||||
id,
|
||||
schedule_id: scheduleId,
|
||||
plan_id: planId,
|
||||
run_id: runId,
|
||||
due_at: dueAt,
|
||||
started_at: previous?.started_at ?? startedAt,
|
||||
finished_at: previous?.finished_at ?? null,
|
||||
status,
|
||||
attempt_no: attemptNo,
|
||||
trigger_kind: triggerKind,
|
||||
error: previous?.error ?? null,
|
||||
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,
|
||||
@@ -629,6 +841,23 @@ class TestDb {
|
||||
return { changes: previous ? 1 : 0 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("UPDATE digital_schedule_runs")) {
|
||||
const [status, finishedAt, error, payload, updatedAt, id] = args as [string, string, string | null, string, string, string];
|
||||
const previous = this.scheduleRuns.get(id);
|
||||
if (previous) {
|
||||
this.scheduleRuns.set(id, {
|
||||
...previous,
|
||||
status,
|
||||
finished_at: finishedAt,
|
||||
error,
|
||||
payload,
|
||||
updated_at: updatedAt,
|
||||
sync_status: "pending",
|
||||
});
|
||||
}
|
||||
return { changes: previous ? 1 : 0 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("INSERT OR IGNORE INTO digital_events")) {
|
||||
const [id, planId, taskId, runId, kind, message, payload, occurredAt, createdAt] = args as [
|
||||
string,
|
||||
|
||||
Reference in New Issue
Block a user