2021 lines
66 KiB
TypeScript
2021 lines
66 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||
|
||
const mockState = vi.hoisted(() => ({
|
||
db: null as TestDb | null,
|
||
ensureSchemaCalls: 0,
|
||
settings: new Map<string, unknown>(),
|
||
}));
|
||
|
||
vi.mock("../../db", () => ({
|
||
ensureDigitalEmployeeSchema: () => {
|
||
mockState.ensureSchemaCalls += 1;
|
||
return true;
|
||
},
|
||
getDb: () => mockState.db,
|
||
readSetting: (key: string) => mockState.settings.get(key) ?? null,
|
||
writeSetting: (key: string, value: unknown) => mockState.settings.set(key, value),
|
||
}));
|
||
|
||
describe("digital employee state service", () => {
|
||
beforeEach(() => {
|
||
vi.resetModules();
|
||
vi.setSystemTime(new Date("2026-06-07T08:00:00.000Z"));
|
||
mockState.db = new TestDb();
|
||
mockState.ensureSchemaCalls = 0;
|
||
mockState.settings = new Map<string, unknown>();
|
||
});
|
||
|
||
it("extracts artifacts and approvals from runtime event payload into syncable records", async () => {
|
||
const { recordDigitalEmployeeRuntimeEvent } = await import("./stateService");
|
||
|
||
recordDigitalEmployeeRuntimeEvent({
|
||
kind: "tool_result",
|
||
request_id: "req-1",
|
||
message: "生成交付产物并等待确认",
|
||
status: "completed",
|
||
payload: {
|
||
artifacts: [
|
||
{
|
||
id: "summary-csv",
|
||
name: "summary.csv",
|
||
uri: "file:///tmp/summary.csv",
|
||
},
|
||
],
|
||
outputPath: "/tmp/final-report.txt",
|
||
approval_requests: [
|
||
{
|
||
id: "approval-1",
|
||
title: "确认发送日报给管理端",
|
||
},
|
||
],
|
||
},
|
||
});
|
||
|
||
expect(Array.from(mockState.db?.artifacts.values() ?? [])).toEqual([
|
||
expect.objectContaining({
|
||
id: "computer-chat-run-req-1:artifact:summary-csv",
|
||
name: "summary.csv",
|
||
uri: "file:///tmp/summary.csv",
|
||
}),
|
||
expect.objectContaining({
|
||
id: "computer-chat-run-req-1:artifact:tmp-final-report-txt",
|
||
name: "final-report.txt",
|
||
uri: "/tmp/final-report.txt",
|
||
}),
|
||
]);
|
||
expect(Array.from(mockState.db?.approvals.values() ?? [])).toEqual([
|
||
expect.objectContaining({
|
||
id: "computer-chat-run-req-1:approval:approval-1",
|
||
title: "确认发送日报给管理端",
|
||
status: "pending",
|
||
}),
|
||
]);
|
||
expect(mockState.db?.outbox.get("artifact:upsert:computer-chat-run-req-1:artifact:summary-csv")).toMatchObject({
|
||
entity_type: "artifact",
|
||
entity_id: "computer-chat-run-req-1:artifact:summary-csv",
|
||
status: "pending",
|
||
});
|
||
expect(mockState.db?.outbox.get("approval:upsert:computer-chat-run-req-1:approval:approval-1")).toMatchObject({
|
||
entity_type: "approval",
|
||
entity_id: "computer-chat-run-req-1:approval:approval-1",
|
||
status: "pending",
|
||
});
|
||
expect(mockState.ensureSchemaCalls).toBeGreaterThan(0);
|
||
});
|
||
|
||
it("normalizes file server workspace events into delivery artifact payload", async () => {
|
||
const { recordDigitalEmployeeRuntimeEvent } = await import("./stateService");
|
||
|
||
recordDigitalEmployeeRuntimeEvent({
|
||
kind: "file_server.workspace.created",
|
||
request_id: "file-workspace-1",
|
||
message: "create-workspace 完成",
|
||
status: "completed",
|
||
payload: {
|
||
source: "qiming-file-server",
|
||
toolName: "create_workspace",
|
||
workspaceId: "workspace-123",
|
||
projectId: "project-file",
|
||
path: "/tmp/qiming/workspace-123",
|
||
status: "completed",
|
||
},
|
||
});
|
||
|
||
const artifacts = Array.from(mockState.db?.artifacts.values() ?? []);
|
||
expect(artifacts).toHaveLength(1);
|
||
expect(artifacts[0]).toMatchObject({
|
||
kind: "workspace",
|
||
name: "workspace-123",
|
||
uri: "/tmp/qiming/workspace-123",
|
||
});
|
||
expect(JSON.parse(artifacts[0]?.payload ?? "{}")).toMatchObject({
|
||
delivery: {
|
||
stage: "workspace",
|
||
status: "completed",
|
||
workspace_id: "workspace-123",
|
||
project_id: "project-file",
|
||
uri: "/tmp/qiming/workspace-123",
|
||
source_tool: "create_workspace",
|
||
source_event_kind: "file_server.workspace.created",
|
||
},
|
||
});
|
||
expect(mockState.db?.outbox.get(`artifact:upsert:${artifacts[0]?.id}`)).toMatchObject({
|
||
entity_type: "artifact",
|
||
status: "pending",
|
||
});
|
||
});
|
||
|
||
it("upserts file server file delivery status without duplicating artifacts", async () => {
|
||
const { recordDigitalEmployeeRuntimeEvent } = await import("./stateService");
|
||
|
||
const basePayload = {
|
||
source: "qiming-file-server",
|
||
workspaceId: "workspace-123",
|
||
projectId: "project-file",
|
||
fileId: "file-1",
|
||
filename: "report.pdf",
|
||
version: "v2",
|
||
size: 2048,
|
||
path: "/tmp/qiming/workspace-123/report.pdf",
|
||
};
|
||
recordDigitalEmployeeRuntimeEvent({
|
||
kind: "file_server.upload.completed",
|
||
request_id: "file-delivery-1",
|
||
message: "upload-file 完成",
|
||
status: "completed",
|
||
payload: {
|
||
...basePayload,
|
||
toolName: "upload_file",
|
||
status: "uploaded",
|
||
},
|
||
});
|
||
recordDigitalEmployeeRuntimeEvent({
|
||
kind: "file_server.download.completed",
|
||
request_id: "file-delivery-1",
|
||
message: "download-all-files 完成",
|
||
status: "completed",
|
||
payload: {
|
||
...basePayload,
|
||
toolName: "download_all_files",
|
||
status: "downloaded",
|
||
downloadUrl: "file:///tmp/qiming/workspace-123/report.pdf",
|
||
},
|
||
});
|
||
|
||
const artifacts = Array.from(mockState.db?.artifacts.values() ?? []);
|
||
expect(artifacts).toHaveLength(1);
|
||
expect(artifacts[0]).toMatchObject({
|
||
kind: "download",
|
||
name: "report.pdf",
|
||
uri: "file:///tmp/qiming/workspace-123/report.pdf",
|
||
});
|
||
expect(JSON.parse(artifacts[0]?.payload ?? "{}")).toMatchObject({
|
||
delivery: {
|
||
stage: "download",
|
||
status: "downloaded",
|
||
workspace_id: "workspace-123",
|
||
project_id: "project-file",
|
||
file_id: "file-1",
|
||
version: "v2",
|
||
filename: "report.pdf",
|
||
size: 2048,
|
||
uri: "file:///tmp/qiming/workspace-123/report.pdf",
|
||
source_tool: "download_all_files",
|
||
source_event_kind: "file_server.download.completed",
|
||
},
|
||
});
|
||
expect(Array.from(mockState.db?.outbox.values() ?? []).filter((row) => row.entity_type === "artifact")).toHaveLength(1);
|
||
});
|
||
|
||
it("records daily reports as syncable artifact and event history", async () => {
|
||
const { recordDigitalEmployeeDailyReport } = await import("./stateService");
|
||
|
||
const first = recordDigitalEmployeeDailyReport({
|
||
date: "2026-06-07",
|
||
title: "2026-06-07 数字员工日报",
|
||
summary: "今日完成任务汇总。",
|
||
totals: {
|
||
task_count: 2,
|
||
execution_count: 3,
|
||
success_count: 2,
|
||
failure_count: 1,
|
||
running_count: 0,
|
||
},
|
||
detailLines: ["09:00 任务 A:完成", "10:00 任务 B:异常"],
|
||
artifactSources: [{ id: "artifact-1", name: "summary.csv" }],
|
||
filename: "qimingclaw-digital-report-2026-06-07.txt",
|
||
textContent: "日报正文",
|
||
generatedAt: "2026-06-07T08:00:00.000Z",
|
||
reportScheduleTime: "18:00",
|
||
source: "qimingclaw",
|
||
model: "qimingclaw-adapter",
|
||
});
|
||
const second = recordDigitalEmployeeDailyReport({
|
||
date: "2026-06-07",
|
||
title: "2026-06-07 数字员工日报",
|
||
summary: "第二次生成。",
|
||
totals: { task_count: 2, execution_count: 4, success_count: 3, failure_count: 1 },
|
||
detailLines: ["18:00 重新生成日报"],
|
||
filename: "qimingclaw-digital-report-2026-06-07.txt",
|
||
textContent: "第二版日报正文",
|
||
generatedAt: "2026-06-07T09:00:00.000Z",
|
||
});
|
||
|
||
expect(first).toMatchObject({
|
||
reportId: "daily-report:2026-06-07:2026-06-07T08-00-00-000Z",
|
||
planId: "daily-report:2026-06-07",
|
||
taskId: "daily-report:2026-06-07:generate",
|
||
runId: "daily-report:2026-06-07:2026-06-07T08-00-00-000Z",
|
||
artifactId: "daily-report:2026-06-07:2026-06-07T08-00-00-000Z:artifact:text",
|
||
eventId: "daily-report:2026-06-07:2026-06-07T08-00-00-000Z:event:generated",
|
||
});
|
||
expect(second?.reportId).toBe("daily-report:2026-06-07:2026-06-07T09-00-00-000Z");
|
||
expect(mockState.db?.plans.get("daily-report:2026-06-07")).toMatchObject({ status: "completed" });
|
||
expect(mockState.db?.tasks.get("daily-report:2026-06-07:generate")).toMatchObject({
|
||
status: "completed",
|
||
assigned_skill_id: "qimingclaw-artifact-report",
|
||
});
|
||
expect(mockState.db?.runs.get(first!.runId)).toMatchObject({
|
||
status: "completed",
|
||
finished_at: "2026-06-07T08:00:00.000Z",
|
||
});
|
||
expect(Array.from(mockState.db?.artifacts.values() ?? []).filter((row) => row.kind === "daily_report")).toHaveLength(2);
|
||
expect(mockState.db?.events.get(first!.eventId)).toMatchObject({
|
||
kind: "daily_report_generated",
|
||
message: "数字员工日报已生成:2026-06-07",
|
||
run_id: first!.runId,
|
||
});
|
||
expect(JSON.parse(mockState.db?.artifacts.get(first!.artifactId)?.payload ?? "{}")).toMatchObject({
|
||
report_id: first!.reportId,
|
||
date: "2026-06-07",
|
||
filename: "qimingclaw-digital-report-2026-06-07.txt",
|
||
format: "text",
|
||
text_content: "日报正文",
|
||
totals: { task_count: 2, execution_count: 3 },
|
||
artifact_sources: [{ id: "artifact-1", name: "summary.csv" }],
|
||
});
|
||
expect(mockState.db?.outbox.get(`artifact:upsert:${first!.artifactId}`)).toMatchObject({
|
||
entity_type: "artifact",
|
||
status: "pending",
|
||
});
|
||
expect(mockState.db?.outbox.get(`event:insert:${first!.eventId}`)).toMatchObject({
|
||
entity_type: "event",
|
||
status: "pending",
|
||
});
|
||
});
|
||
|
||
it("records and lists route decisions as syncable events", async () => {
|
||
const {
|
||
listDigitalEmployeeRouteDecisions,
|
||
recordDigitalEmployeeRouteDecision,
|
||
} = await import("./stateService");
|
||
|
||
const first = recordDigitalEmployeeRouteDecision({
|
||
idempotencyKey: "route-key-1",
|
||
correlationId: "corr-1",
|
||
routeKind: "PlanExecution",
|
||
intentKind: "execute",
|
||
reasonCodes: ["ready_step_context"],
|
||
candidateSkills: ["qimingclaw-computer-control"],
|
||
contextRefs: { plan_id: "plan-1", step_id: "step-ready" },
|
||
createdCommandId: "qimingclaw-digital-start_run-1",
|
||
createsTaskRun: true,
|
||
entrypoint: "task_center",
|
||
actor: "operator",
|
||
envelope: { text: "开始执行这个步骤" },
|
||
recordedAt: "2026-06-07T08:01:00.000Z",
|
||
});
|
||
const second = recordDigitalEmployeeRouteDecision({
|
||
idempotencyKey: "route-key-2",
|
||
correlationId: "corr-2",
|
||
routeKind: "ProjectionQuery",
|
||
intentKind: "query",
|
||
reasonCodes: ["projection_query_text"],
|
||
candidateSkills: ["qimingclaw-artifact-report"],
|
||
contextRefs: { plan_id: "plan-1" },
|
||
recordedAt: "2026-06-07T08:02:00.000Z",
|
||
});
|
||
|
||
expect(first).toMatchObject({
|
||
id: "route-decision:route-key-1",
|
||
route_kind: "PlanExecution",
|
||
created_command_id: "qimingclaw-digital-start_run-1",
|
||
creates_task_run: true,
|
||
context_refs: { step_id: "step-ready" },
|
||
});
|
||
expect(mockState.db?.events.get(first.id)).toMatchObject({
|
||
kind: "route_decision",
|
||
run_id: null,
|
||
plan_id: null,
|
||
});
|
||
expect(JSON.parse(mockState.db?.events.get(first.id)?.payload ?? "{}")).toMatchObject({
|
||
source: "qimingclaw-route-decision",
|
||
routeDecision: {
|
||
id: first.id,
|
||
route_kind: "PlanExecution",
|
||
intent_kind: "execute",
|
||
created_command_id: "qimingclaw-digital-start_run-1",
|
||
},
|
||
envelope: { text: "开始执行这个步骤" },
|
||
});
|
||
expect(mockState.db?.outbox.get(`event:insert:${first.id}`)).toMatchObject({
|
||
entity_type: "event",
|
||
operation: "insert",
|
||
status: "pending",
|
||
});
|
||
expect(listDigitalEmployeeRouteDecisions()).toEqual([
|
||
expect.objectContaining({ id: second.id, route_kind: "ProjectionQuery" }),
|
||
expect.objectContaining({ id: first.id, route_kind: "PlanExecution", created_command_id: "qimingclaw-digital-start_run-1" }),
|
||
]);
|
||
});
|
||
|
||
it("keeps route decision ids stable for idempotency keys", async () => {
|
||
const { listDigitalEmployeeRouteDecisions, recordDigitalEmployeeRouteDecision } = await import("./stateService");
|
||
|
||
const first = recordDigitalEmployeeRouteDecision({
|
||
idempotencyKey: "repeat-route-key",
|
||
routeKind: "ChatOnly",
|
||
intentKind: "chat",
|
||
recordedAt: "2026-06-07T08:01:00.000Z",
|
||
});
|
||
const second = recordDigitalEmployeeRouteDecision({
|
||
idempotencyKey: "repeat-route-key",
|
||
routeKind: "PlanExecution",
|
||
intentKind: "execute",
|
||
recordedAt: "2026-06-07T08:02:00.000Z",
|
||
});
|
||
|
||
expect(second.id).toBe(first.id);
|
||
expect(Array.from(mockState.db?.events.values() ?? []).filter((event) => event.kind === "route_decision")).toHaveLength(1);
|
||
expect(listDigitalEmployeeRouteDecisions()).toEqual([
|
||
expect.objectContaining({ id: first.id, route_kind: "ChatOnly" }),
|
||
]);
|
||
});
|
||
|
||
it("updates formal approval records when UI approval decisions are saved", async () => {
|
||
const { saveDigitalEmployeeUiState } = await import("./stateService");
|
||
mockState.db?.approvals.set("approval-1", {
|
||
id: "approval-1",
|
||
plan_id: "plan-1",
|
||
task_id: "task-1",
|
||
run_id: "run-1",
|
||
title: "确认发送日报给管理端",
|
||
status: "pending",
|
||
payload: JSON.stringify({ source: "test" }),
|
||
sync_status: "synced",
|
||
created_at: "2026-06-07T07:00:00.000Z",
|
||
updated_at: "2026-06-07T07:00:00.000Z",
|
||
});
|
||
|
||
saveDigitalEmployeeUiState({
|
||
action: "decide_approval",
|
||
date: "2026-06-07",
|
||
metadata: {
|
||
decision_id: "approval-1",
|
||
decision: "approved",
|
||
acp_permission: {
|
||
permission_id: "permission-1",
|
||
session_id: "session-1",
|
||
response: "once",
|
||
success: true,
|
||
},
|
||
},
|
||
state: {
|
||
selectedDutyIdsByDate: {},
|
||
confirmedDates: {},
|
||
reportScheduleTime: "18:00",
|
||
reportGeneratedAtByDate: {},
|
||
decisionResultsByDate: {
|
||
"2026-06-07": {
|
||
"approval-1": "approved",
|
||
},
|
||
},
|
||
profile: {
|
||
name: "飞天数字员工",
|
||
company: "qimingclaw 客户端",
|
||
position: "客户端任务执行员",
|
||
currentStatus: "working",
|
||
},
|
||
},
|
||
});
|
||
|
||
expect(mockState.db?.approvals.get("approval-1")).toMatchObject({
|
||
id: "approval-1",
|
||
status: "approved",
|
||
sync_status: "pending",
|
||
updated_at: "2026-06-07T08:00:00.000Z",
|
||
});
|
||
expect(JSON.parse(mockState.db?.approvals.get("approval-1")?.payload ?? "{}")).toMatchObject({
|
||
source: "test",
|
||
decision: "approved",
|
||
decidedAt: "2026-06-07T08:00:00.000Z",
|
||
previousStatus: "pending",
|
||
acpPermission: {
|
||
permission_id: "permission-1",
|
||
session_id: "session-1",
|
||
response: "once",
|
||
success: true,
|
||
},
|
||
});
|
||
expect(mockState.db?.outbox.get("approval:upsert:approval-1")).toMatchObject({
|
||
entity_type: "approval",
|
||
entity_id: "approval-1",
|
||
status: "pending",
|
||
});
|
||
const decisionEvents = Array.from(mockState.db?.events.values() ?? []).filter((event) => event.kind === "approval_decision");
|
||
expect(decisionEvents).toHaveLength(1);
|
||
expect(decisionEvents[0]).toMatchObject({
|
||
id: "approval-1:decision:approved:2026-06-07T08-00-00-000Z",
|
||
plan_id: "plan-1",
|
||
task_id: "task-1",
|
||
run_id: "run-1",
|
||
message: "审批处理:确认发送日报给管理端 -> approved",
|
||
sync_status: "pending",
|
||
occurred_at: "2026-06-07T08:00:00.000Z",
|
||
});
|
||
expect(JSON.parse(decisionEvents[0]?.payload ?? "{}")).toMatchObject({
|
||
source: "qimingclaw-approval-decision",
|
||
approvalId: "approval-1",
|
||
decision: "approved",
|
||
previousStatus: "pending",
|
||
nextStatus: "approved",
|
||
title: "确认发送日报给管理端",
|
||
planId: "plan-1",
|
||
taskId: "task-1",
|
||
runId: "run-1",
|
||
decidedAt: "2026-06-07T08:00:00.000Z",
|
||
acpPermission: {
|
||
permission_id: "permission-1",
|
||
session_id: "session-1",
|
||
},
|
||
});
|
||
expect(mockState.db?.outbox.get("event:insert:approval-1:decision:approved:2026-06-07T08-00-00-000Z")).toMatchObject({
|
||
entity_type: "event",
|
||
entity_id: "approval-1:decision:approved:2026-06-07T08-00-00-000Z",
|
||
operation: "insert",
|
||
status: "pending",
|
||
});
|
||
expect(mockState.ensureSchemaCalls).toBeGreaterThan(0);
|
||
});
|
||
|
||
it("persists console role and role preferences in UI state", async () => {
|
||
const { readDigitalEmployeeUiState, saveDigitalEmployeeUiState } = await import("./stateService");
|
||
|
||
saveDigitalEmployeeUiState({
|
||
action: "update_role_preferences",
|
||
metadata: { source: "ops-settings" },
|
||
state: {
|
||
selectedDutyIdsByDate: {},
|
||
confirmedDates: {},
|
||
reportScheduleTime: "17:30",
|
||
reportGeneratedAtByDate: {},
|
||
decisionResultsByDate: {},
|
||
consoleRole: "engineering",
|
||
rolePreferences: {
|
||
workStart: "10:00",
|
||
workEnd: "19:00",
|
||
notification: "system",
|
||
},
|
||
profile: {
|
||
name: "飞天数字员工",
|
||
company: "qimingclaw 客户端",
|
||
position: "客户端任务执行员",
|
||
currentStatus: "working",
|
||
},
|
||
},
|
||
});
|
||
|
||
expect(readDigitalEmployeeUiState()).toMatchObject({
|
||
reportScheduleTime: "17:30",
|
||
consoleRole: "engineering",
|
||
rolePreferences: {
|
||
workStart: "10:00",
|
||
workEnd: "19:00",
|
||
notification: "system",
|
||
},
|
||
});
|
||
const preferenceEvent = Array.from(mockState.db?.events.values() ?? [])
|
||
.find((event) => event.kind === "digital_workday_update_role_preferences");
|
||
expect(preferenceEvent).toMatchObject({
|
||
kind: "digital_workday_update_role_preferences",
|
||
message: "数字员工运营偏好已更新",
|
||
});
|
||
});
|
||
|
||
it("records governance commands as formal runtime records", async () => {
|
||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||
|
||
const ids = recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "create_plan",
|
||
commandId: "command-1",
|
||
correlationId: "corr-1",
|
||
accepted: true,
|
||
message: "已创建计划草稿",
|
||
payload: {
|
||
title: "生成今日客户跟进计划",
|
||
objective: "把今日客户跟进事项整理成可执行计划",
|
||
steps: [
|
||
{
|
||
step_id: "collect-crm",
|
||
title: "整理客户列表",
|
||
skills: ["qimingclaw-mcp-tool-crm-crm-search"],
|
||
tasks: [
|
||
{
|
||
task_id: "task-collect-crm",
|
||
title: "拉取 CRM 客户记录",
|
||
description: "读取今日需要跟进的客户",
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
result: {
|
||
plan_id: "plan-customer-followup",
|
||
status: "draft",
|
||
},
|
||
});
|
||
|
||
expect(ids).toEqual({
|
||
planId: "plan-customer-followup",
|
||
taskId: "governance-task-command-1",
|
||
runId: "governance-run-command-1",
|
||
});
|
||
expect(mockState.db?.plans.get("plan-customer-followup")).toMatchObject({
|
||
id: "plan-customer-followup",
|
||
title: "生成今日客户跟进计划",
|
||
status: "draft",
|
||
sync_status: "pending",
|
||
});
|
||
expect(mockState.db?.tasks.get("governance-task-command-1")).toMatchObject({
|
||
plan_id: "plan-customer-followup",
|
||
title: "创建数字员工计划",
|
||
assigned_skill_id: "qimingclaw-governance-command",
|
||
});
|
||
expect(mockState.db?.tasks.get("task-collect-crm")).toMatchObject({
|
||
plan_id: "plan-customer-followup",
|
||
title: "拉取 CRM 客户记录",
|
||
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",
|
||
status: "draft",
|
||
finished_at: "2026-06-07T08:00:00.000Z",
|
||
});
|
||
expect(mockState.db?.events.get("governance-run-command-1:create_plan:command-1")).toMatchObject({
|
||
kind: "governance_create_plan",
|
||
message: "已创建计划草稿",
|
||
});
|
||
expect(mockState.db?.outbox.get("plan:upsert:plan-customer-followup")).toMatchObject({
|
||
entity_type: "plan",
|
||
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");
|
||
|
||
recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "create_plan",
|
||
commandId: "command-create",
|
||
accepted: true,
|
||
message: "已创建计划草稿",
|
||
payload: { title: "客户回访重新执行", objective: "重新执行客户回访流程" },
|
||
result: { plan_id: "plan-restart", status: "draft" },
|
||
});
|
||
|
||
recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "approve_plan",
|
||
commandId: "command-approve",
|
||
accepted: true,
|
||
message: "已批准计划",
|
||
result: { plan_id: "plan-restart", status: "approved" },
|
||
});
|
||
|
||
expect(mockState.db?.plans.get("plan-restart")).toMatchObject({
|
||
title: "客户回访重新执行",
|
||
objective: "重新执行客户回访流程",
|
||
status: "approved",
|
||
});
|
||
});
|
||
|
||
it("records plan action governance commands as local run facts", async () => {
|
||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||
|
||
recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "pause_plan",
|
||
commandId: "command-pause-plan",
|
||
planId: "plan-action",
|
||
accepted: true,
|
||
message: "暂停计划执行",
|
||
payload: { plan_id: "plan-action", requested_action: "plan_pause" },
|
||
result: { plan_id: "plan-action", status: "paused" },
|
||
});
|
||
|
||
expect(mockState.db?.plans.get("plan-action")).toMatchObject({
|
||
status: "paused",
|
||
sync_status: "pending",
|
||
});
|
||
expect(mockState.db?.tasks.get("governance-task-command-pause-plan")).toMatchObject({
|
||
plan_id: "plan-action",
|
||
status: "paused",
|
||
assigned_skill_id: "qimingclaw-governance-command",
|
||
});
|
||
expect(mockState.db?.runs.get("governance-run-command-pause-plan")).toMatchObject({
|
||
plan_id: "plan-action",
|
||
status: "paused",
|
||
finished_at: "2026-06-07T08:00:00.000Z",
|
||
});
|
||
expect(mockState.db?.events.get("governance-run-command-pause-plan:pause_plan:command-pause-plan")).toMatchObject({
|
||
kind: "governance_pause_plan",
|
||
message: "暂停计划执行",
|
||
});
|
||
expect(mockState.db?.outbox.get("plan:upsert:plan-action")).toMatchObject({ entity_type: "plan" });
|
||
expect(mockState.db?.outbox.get("event:insert:governance-run-command-pause-plan:pause_plan:command-pause-plan")).toMatchObject({ entity_type: "event" });
|
||
});
|
||
|
||
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" })],
|
||
});
|
||
});
|
||
|
||
it("applies skip governance to the real task, plan step, event, and outbox", async () => {
|
||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||
|
||
createGovernableTask();
|
||
|
||
const ids = recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "skip_task",
|
||
commandId: "command-skip-task",
|
||
planId: "plan-governance",
|
||
taskId: "task-governance",
|
||
runId: "run-governance",
|
||
stepId: "step-governance",
|
||
accepted: true,
|
||
message: "跳过当前任务",
|
||
payload: { reason: "用户选择跳过" },
|
||
result: { task_id: "task-governance", run_id: "run-governance", step_id: "step-governance" },
|
||
});
|
||
|
||
expect(ids).toEqual({
|
||
planId: "plan-governance",
|
||
taskId: "task-governance",
|
||
runId: "run-governance",
|
||
stepId: "step-governance",
|
||
});
|
||
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({
|
||
status: "skipped",
|
||
sync_status: "pending",
|
||
});
|
||
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({
|
||
status: "skipped",
|
||
sync_status: "pending",
|
||
});
|
||
expect(mockState.db?.runs.get("run-governance")).toMatchObject({
|
||
status: "skipped",
|
||
finished_at: "2026-06-07T08:00:00.000Z",
|
||
sync_status: "pending",
|
||
});
|
||
expect(JSON.parse(mockState.db?.tasks.get("task-governance")?.payload ?? "{}")).toMatchObject({
|
||
lastGovernanceCommand: {
|
||
commandKind: "skip_task",
|
||
taskId: "task-governance",
|
||
stepId: "step-governance",
|
||
status: "skipped",
|
||
},
|
||
});
|
||
expect(mockState.db?.events.get("run-governance:skip_task:command-skip-task")).toMatchObject({
|
||
kind: "governance_skip_task",
|
||
message: "跳过当前任务",
|
||
task_id: "task-governance",
|
||
});
|
||
expect(mockState.db?.outbox.get("task:upsert:task-governance")).toMatchObject({ entity_type: "task" });
|
||
expect(mockState.db?.outbox.get("plan_step:upsert:step-governance")).toMatchObject({ entity_type: "plan_step" });
|
||
expect(mockState.db?.outbox.get("run:upsert:run-governance")).toMatchObject({ entity_type: "run" });
|
||
});
|
||
|
||
it("marks dependent plan steps ready when skipped dependencies are satisfied", async () => {
|
||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||
|
||
createGovernableTask();
|
||
addPlanStep({ id: "step-dependent", title: "后续执行步骤", seq: 2, dependsOn: ["step-governance"] });
|
||
|
||
recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "skip_task",
|
||
commandId: "command-unlock-dependent",
|
||
planId: "plan-governance",
|
||
taskId: "task-governance",
|
||
runId: "run-governance",
|
||
stepId: "step-governance",
|
||
accepted: true,
|
||
payload: { reason: "跳过前置步骤" },
|
||
});
|
||
|
||
expect(mockState.db?.planSteps.get("step-dependent")).toMatchObject({
|
||
status: "ready",
|
||
sync_status: "pending",
|
||
});
|
||
expect(JSON.parse(mockState.db?.planSteps.get("step-dependent")?.payload ?? "{}")).toMatchObject({
|
||
dependencyGraph: {
|
||
status: "ready",
|
||
triggeredByStepId: "step-governance",
|
||
satisfiedDependencies: ["step-governance"],
|
||
},
|
||
});
|
||
expect(Array.from(mockState.db?.events.values() ?? [])).toEqual(expect.arrayContaining([
|
||
expect.objectContaining({ kind: "plan_step_ready", plan_id: "plan-governance" }),
|
||
]));
|
||
expect(mockState.db?.outbox.get("plan_step:upsert:step-dependent")).toMatchObject({
|
||
entity_type: "plan_step",
|
||
entity_id: "step-dependent",
|
||
status: "pending",
|
||
});
|
||
});
|
||
|
||
it("blocks dependent plan steps on cancellation and returns them to pending on retry", async () => {
|
||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||
|
||
createGovernableTask();
|
||
addPlanStep({ id: "step-dependent", title: "后续执行步骤", seq: 2, dependsOn: ["step-governance"] });
|
||
|
||
recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "cancel_task",
|
||
commandId: "command-block-dependent",
|
||
planId: "plan-governance",
|
||
taskId: "task-governance",
|
||
runId: "run-governance",
|
||
stepId: "step-governance",
|
||
accepted: true,
|
||
payload: { reason: "取消前置步骤" },
|
||
});
|
||
|
||
expect(mockState.db?.planSteps.get("step-dependent")).toMatchObject({ status: "blocked" });
|
||
expect(Array.from(mockState.db?.events.values() ?? [])).toEqual(expect.arrayContaining([
|
||
expect.objectContaining({ kind: "plan_step_blocked" }),
|
||
]));
|
||
|
||
recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "retry_task",
|
||
commandId: "command-wait-dependent",
|
||
planId: "plan-governance",
|
||
taskId: "task-governance",
|
||
runId: "run-governance",
|
||
stepId: "step-governance",
|
||
accepted: true,
|
||
payload: { reason: "重试前置步骤" },
|
||
});
|
||
|
||
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({ status: "running" });
|
||
expect(mockState.db?.planSteps.get("step-dependent")).toMatchObject({ status: "pending" });
|
||
expect(Array.from(mockState.db?.events.values() ?? [])).toEqual(expect.arrayContaining([
|
||
expect.objectContaining({ kind: "plan_step_waiting" }),
|
||
]));
|
||
expect(JSON.parse(mockState.db?.planSteps.get("step-dependent")?.payload ?? "{}")).toMatchObject({
|
||
dependencyGraph: {
|
||
status: "pending",
|
||
waitingDependencies: ["step-governance"],
|
||
},
|
||
});
|
||
});
|
||
|
||
it("waits for all dependencies before marking a plan step ready", async () => {
|
||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||
|
||
createGovernableTask();
|
||
addPlanStep({ id: "step-other", title: "第二个前置步骤", seq: 2 });
|
||
addGovernableTaskForStep({ taskId: "task-other", runId: "run-other", stepId: "step-other" });
|
||
addPlanStep({ id: "step-dependent", title: "汇总步骤", seq: 3, dependsOn: ["step-governance", "step-other"] });
|
||
|
||
recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "skip_task",
|
||
commandId: "command-first-dependency",
|
||
planId: "plan-governance",
|
||
taskId: "task-governance",
|
||
runId: "run-governance",
|
||
stepId: "step-governance",
|
||
accepted: true,
|
||
});
|
||
|
||
expect(mockState.db?.planSteps.get("step-dependent")).toMatchObject({ status: "pending" });
|
||
|
||
recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "skip_task",
|
||
commandId: "command-second-dependency",
|
||
planId: "plan-governance",
|
||
taskId: "task-other",
|
||
runId: "run-other",
|
||
stepId: "step-other",
|
||
accepted: true,
|
||
});
|
||
|
||
expect(mockState.db?.planSteps.get("step-dependent")).toMatchObject({ status: "ready" });
|
||
expect(JSON.parse(mockState.db?.planSteps.get("step-dependent")?.payload ?? "{}")).toMatchObject({
|
||
dependencyGraph: {
|
||
status: "ready",
|
||
satisfiedDependencies: ["step-governance", "step-other"],
|
||
},
|
||
});
|
||
});
|
||
|
||
it("does not overwrite already running or completed dependent plan steps", async () => {
|
||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||
|
||
createGovernableTask();
|
||
addPlanStep({ id: "step-running-dependent", status: "running", seq: 2, dependsOn: ["step-governance"] });
|
||
addPlanStep({ id: "step-completed-dependent", status: "completed", seq: 3, dependsOn: ["step-governance"] });
|
||
|
||
recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "skip_task",
|
||
commandId: "command-preserve-dependent",
|
||
planId: "plan-governance",
|
||
taskId: "task-governance",
|
||
runId: "run-governance",
|
||
stepId: "step-governance",
|
||
accepted: true,
|
||
});
|
||
|
||
expect(mockState.db?.planSteps.get("step-running-dependent")).toMatchObject({
|
||
status: "running",
|
||
sync_status: "synced",
|
||
});
|
||
expect(mockState.db?.planSteps.get("step-completed-dependent")).toMatchObject({
|
||
status: "completed",
|
||
sync_status: "synced",
|
||
});
|
||
expect(mockState.db?.outbox.has("plan_step:upsert:step-running-dependent")).toBe(false);
|
||
expect(mockState.db?.outbox.has("plan_step:upsert:step-completed-dependent")).toBe(false);
|
||
});
|
||
|
||
it("creates a retry run and preserves the failed source run", async () => {
|
||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||
|
||
createGovernableTask({ taskStatus: "failed", runStatus: "failed", finishedAt: "2026-06-07T07:30:00.000Z" });
|
||
|
||
const ids = recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "retry_task",
|
||
commandId: "command-retry-task",
|
||
planId: "plan-governance",
|
||
taskId: "task-governance",
|
||
runId: "run-governance",
|
||
stepId: "step-governance",
|
||
accepted: true,
|
||
payload: { reason: "失败后重试", prompt: "重新执行任务" },
|
||
result: { task_id: "task-governance", run_id: "run-governance", step_id: "step-governance" },
|
||
});
|
||
|
||
expect(ids.runId).toBe("task-governance:run:retry_task:command-retry-task");
|
||
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({ status: "running" });
|
||
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({ status: "running" });
|
||
expect(mockState.db?.runs.get("run-governance")).toMatchObject({
|
||
status: "failed",
|
||
finished_at: "2026-06-07T07:30:00.000Z",
|
||
});
|
||
expect(mockState.db?.runs.get(ids.runId)).toMatchObject({
|
||
plan_id: "plan-governance",
|
||
task_id: "task-governance",
|
||
status: "running",
|
||
finished_at: null,
|
||
});
|
||
expect(JSON.parse(mockState.db?.runs.get(ids.runId)?.payload ?? "{}")).toMatchObject({
|
||
commandKind: "retry_task",
|
||
sourceRunId: "run-governance",
|
||
attemptNo: 2,
|
||
});
|
||
expect(mockState.db?.outbox.get(`run:upsert:${ids.runId}`)).toMatchObject({ entity_type: "run" });
|
||
});
|
||
|
||
it("cancels the current task run without deleting run history", async () => {
|
||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||
|
||
createGovernableTask();
|
||
mockState.db?.runs.set("run-history", {
|
||
id: "run-history",
|
||
plan_id: "plan-governance",
|
||
task_id: "task-governance",
|
||
status: "completed",
|
||
started_at: "2026-06-07T07:00:00.000Z",
|
||
finished_at: "2026-06-07T07:05:00.000Z",
|
||
payload: JSON.stringify({ source: "history" }),
|
||
sync_status: "synced",
|
||
created_at: "2026-06-07T07:00:00.000Z",
|
||
updated_at: "2026-06-07T07:05:00.000Z",
|
||
});
|
||
|
||
recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "cancel_task",
|
||
commandId: "command-cancel-task",
|
||
planId: "plan-governance",
|
||
taskId: "task-governance",
|
||
runId: "run-governance",
|
||
stepId: "step-governance",
|
||
accepted: true,
|
||
payload: { reason: "用户取消" },
|
||
});
|
||
|
||
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({ status: "cancelled" });
|
||
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({ status: "cancelled" });
|
||
expect(mockState.db?.runs.get("run-governance")).toMatchObject({
|
||
status: "cancelled",
|
||
finished_at: "2026-06-07T08:00:00.000Z",
|
||
});
|
||
expect(mockState.db?.runs.get("run-history")).toMatchObject({
|
||
status: "completed",
|
||
finished_at: "2026-06-07T07:05:00.000Z",
|
||
});
|
||
});
|
||
|
||
it("resumes paused tasks and records provided task input", async () => {
|
||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||
|
||
createGovernableTask({ taskStatus: "paused", stepStatus: "paused", runStatus: "paused" });
|
||
|
||
recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "provide_task_input",
|
||
commandId: "command-provide-input",
|
||
planId: "plan-governance",
|
||
taskId: "task-governance",
|
||
runId: "run-governance",
|
||
stepId: "step-governance",
|
||
accepted: true,
|
||
payload: { input: "继续使用最新客户名单" },
|
||
});
|
||
|
||
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({ status: "paused" });
|
||
expect(JSON.parse(mockState.db?.tasks.get("task-governance")?.payload ?? "{}")).toMatchObject({
|
||
lastGovernanceCommand: {
|
||
commandKind: "provide_task_input",
|
||
payload: { input: "继续使用最新客户名单" },
|
||
},
|
||
});
|
||
|
||
recordDigitalEmployeeGovernanceCommand({
|
||
commandKind: "resume_task",
|
||
commandId: "command-resume-task",
|
||
planId: "plan-governance",
|
||
taskId: "task-governance",
|
||
runId: "run-governance",
|
||
stepId: "step-governance",
|
||
accepted: true,
|
||
payload: { reason: "资料已补充" },
|
||
});
|
||
|
||
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({ status: "running" });
|
||
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({ status: "running" });
|
||
expect(mockState.db?.runs.get("run-governance")).toMatchObject({ status: "running" });
|
||
expect(mockState.db?.events.get("run-governance:provide_task_input:command-provide-input")).toMatchObject({
|
||
kind: "governance_provide_task_input",
|
||
});
|
||
expect(mockState.db?.events.get("run-governance:resume_task:command-resume-task")).toMatchObject({
|
||
kind: "governance_resume_task",
|
||
});
|
||
});
|
||
|
||
it("upserts, lists, and syncs digital employee memories", async () => {
|
||
const {
|
||
listDigitalEmployeeMemories,
|
||
readDigitalEmployeeRuntimeRecords,
|
||
syncQimingclawMemoryEntries,
|
||
upsertDigitalEmployeeMemory,
|
||
} = await import("./stateService");
|
||
|
||
const localMemory = upsertDigitalEmployeeMemory({
|
||
key: "preferred-report-style",
|
||
content: "用户偏好日报先给结论再列证据。",
|
||
category: "preference",
|
||
source: "qimingclaw-digital-local",
|
||
score: 0.88,
|
||
payload: { source: "test" },
|
||
});
|
||
|
||
expect(localMemory).toMatchObject({
|
||
id: "memory-preferred-report-style",
|
||
key: "preferred-report-style",
|
||
category: "preference",
|
||
status: "active",
|
||
syncStatus: "pending",
|
||
});
|
||
expect(mockState.db?.outbox.get("memory:upsert:memory-preferred-report-style")).toMatchObject({
|
||
entity_type: "memory",
|
||
entity_id: "memory-preferred-report-style",
|
||
operation: "upsert",
|
||
status: "pending",
|
||
});
|
||
|
||
syncQimingclawMemoryEntries([
|
||
{
|
||
id: "mem-from-service",
|
||
text: "用户希望所有提交信息使用中文。",
|
||
category: "preference",
|
||
source: "core",
|
||
sourcePath: "MEMORY.md",
|
||
importance: 0.9,
|
||
status: "active",
|
||
createdAt: 1780800000000,
|
||
updatedAt: 1780800300000,
|
||
},
|
||
]);
|
||
|
||
expect(listDigitalEmployeeMemories({ query: "中文" })).toEqual([
|
||
expect.objectContaining({
|
||
id: "mem-from-service",
|
||
key: "mem-from-service",
|
||
content: "用户希望所有提交信息使用中文。",
|
||
}),
|
||
]);
|
||
expect(listDigitalEmployeeMemories({ category: "preference" })).toHaveLength(2);
|
||
expect(readDigitalEmployeeRuntimeRecords().memories).toEqual(expect.arrayContaining([
|
||
expect.objectContaining({ id: "mem-from-service" }),
|
||
expect.objectContaining({ id: "memory-preferred-report-style" }),
|
||
]));
|
||
});
|
||
|
||
it("soft deletes digital employee memories and writes delete outbox", async () => {
|
||
const {
|
||
deleteDigitalEmployeeMemory,
|
||
listDigitalEmployeeMemories,
|
||
upsertDigitalEmployeeMemory,
|
||
} = await import("./stateService");
|
||
|
||
upsertDigitalEmployeeMemory({
|
||
id: "memory-delete-me",
|
||
key: "delete-me",
|
||
content: "这条记忆将被删除。",
|
||
category: "fact",
|
||
});
|
||
|
||
const deleted = deleteDigitalEmployeeMemory("delete-me");
|
||
|
||
expect(deleted).toMatchObject({
|
||
id: "memory-delete-me",
|
||
key: "delete-me",
|
||
status: "deleted",
|
||
deletedAt: "2026-06-07T08:00:00.000Z",
|
||
syncStatus: "pending",
|
||
});
|
||
expect(listDigitalEmployeeMemories({ includeDeleted: false })).toEqual([]);
|
||
expect(listDigitalEmployeeMemories({ includeDeleted: true })).toEqual([
|
||
expect.objectContaining({ id: "memory-delete-me", status: "deleted" }),
|
||
]);
|
||
expect(mockState.db?.outbox.get("memory:delete:memory-delete-me")).toMatchObject({
|
||
entity_type: "memory",
|
||
entity_id: "memory-delete-me",
|
||
operation: "delete",
|
||
status: "pending",
|
||
});
|
||
});
|
||
});
|
||
|
||
function createGovernableTask(options: {
|
||
taskStatus?: string;
|
||
stepStatus?: string;
|
||
runStatus?: string;
|
||
finishedAt?: string | null;
|
||
} = {}): void {
|
||
mockState.db?.plans.set("plan-governance", {
|
||
id: "plan-governance",
|
||
title: "步骤治理计划",
|
||
objective: "验证任务治理闭环",
|
||
status: "running",
|
||
payload: JSON.stringify({ source: "test" }),
|
||
sync_status: "synced",
|
||
created_at: "2026-06-07T07:00:00.000Z",
|
||
updated_at: "2026-06-07T07:00:00.000Z",
|
||
});
|
||
mockState.db?.planSteps.set("step-governance", {
|
||
id: "step-governance",
|
||
plan_id: "plan-governance",
|
||
title: "执行治理步骤",
|
||
status: options.stepStatus ?? options.taskStatus ?? "running",
|
||
seq: 1,
|
||
depends_on: JSON.stringify([]),
|
||
preferred_skill_ids: JSON.stringify(["qimingclaw-task-agent"]),
|
||
payload: JSON.stringify({ source: "test" }),
|
||
sync_status: "synced",
|
||
created_at: "2026-06-07T07:00:00.000Z",
|
||
updated_at: "2026-06-07T07:00:00.000Z",
|
||
});
|
||
mockState.db?.tasks.set("task-governance", {
|
||
id: "task-governance",
|
||
plan_id: "plan-governance",
|
||
title: "执行治理任务",
|
||
status: options.taskStatus ?? "running",
|
||
assigned_skill_id: "qimingclaw-task-agent",
|
||
payload: JSON.stringify({ source: "test", stepId: "step-governance" }),
|
||
sync_status: "synced",
|
||
created_at: "2026-06-07T07:00:00.000Z",
|
||
updated_at: "2026-06-07T07:00:00.000Z",
|
||
});
|
||
mockState.db?.runs.set("run-governance", {
|
||
id: "run-governance",
|
||
plan_id: "plan-governance",
|
||
task_id: "task-governance",
|
||
status: options.runStatus ?? "running",
|
||
started_at: "2026-06-07T07:10:00.000Z",
|
||
finished_at: options.finishedAt ?? null,
|
||
payload: JSON.stringify({ source: "test" }),
|
||
sync_status: "synced",
|
||
created_at: "2026-06-07T07:10:00.000Z",
|
||
updated_at: "2026-06-07T07:10:00.000Z",
|
||
});
|
||
}
|
||
|
||
function addPlanStep(options: {
|
||
id: string;
|
||
title?: string;
|
||
status?: string;
|
||
seq?: number;
|
||
dependsOn?: string[];
|
||
}): void {
|
||
mockState.db?.planSteps.set(options.id, {
|
||
id: options.id,
|
||
plan_id: "plan-governance",
|
||
title: options.title ?? options.id,
|
||
status: options.status ?? "pending",
|
||
seq: options.seq ?? 2,
|
||
depends_on: JSON.stringify(options.dependsOn ?? []),
|
||
preferred_skill_ids: JSON.stringify([]),
|
||
payload: JSON.stringify({ source: "test" }),
|
||
sync_status: "synced",
|
||
created_at: "2026-06-07T07:00:00.000Z",
|
||
updated_at: "2026-06-07T07:00:00.000Z",
|
||
});
|
||
}
|
||
|
||
function addGovernableTaskForStep(options: {
|
||
taskId: string;
|
||
runId: string;
|
||
stepId: string;
|
||
status?: string;
|
||
}): void {
|
||
mockState.db?.tasks.set(options.taskId, {
|
||
id: options.taskId,
|
||
plan_id: "plan-governance",
|
||
title: `${options.stepId} 任务`,
|
||
status: options.status ?? "running",
|
||
assigned_skill_id: "qimingclaw-task-agent",
|
||
payload: JSON.stringify({ source: "test", stepId: options.stepId }),
|
||
sync_status: "synced",
|
||
created_at: "2026-06-07T07:00:00.000Z",
|
||
updated_at: "2026-06-07T07:00:00.000Z",
|
||
});
|
||
mockState.db?.runs.set(options.runId, {
|
||
id: options.runId,
|
||
plan_id: "plan-governance",
|
||
task_id: options.taskId,
|
||
status: options.status ?? "running",
|
||
started_at: "2026-06-07T07:10:00.000Z",
|
||
finished_at: null,
|
||
payload: JSON.stringify({ source: "test" }),
|
||
sync_status: "synced",
|
||
created_at: "2026-06-07T07:10:00.000Z",
|
||
updated_at: "2026-06-07T07:10:00.000Z",
|
||
});
|
||
}
|
||
|
||
interface TestPlanRow {
|
||
id: string;
|
||
title: string;
|
||
objective: string;
|
||
status: string;
|
||
payload: string;
|
||
sync_status: string;
|
||
created_at: string;
|
||
updated_at: string;
|
||
}
|
||
|
||
interface TestTaskRow {
|
||
id: string;
|
||
plan_id: string;
|
||
title: string;
|
||
status: string;
|
||
assigned_skill_id: string;
|
||
payload: string;
|
||
sync_status: string;
|
||
created_at: string;
|
||
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;
|
||
task_id: string;
|
||
status: string;
|
||
started_at: string;
|
||
finished_at: string | null;
|
||
payload: string;
|
||
sync_status: string;
|
||
created_at: string;
|
||
updated_at: string;
|
||
}
|
||
|
||
interface TestEventRow {
|
||
id: string;
|
||
plan_id: string;
|
||
task_id: string;
|
||
run_id: string | null;
|
||
kind: string;
|
||
message: string;
|
||
payload: string;
|
||
sync_status: string;
|
||
occurred_at: string;
|
||
created_at: string;
|
||
}
|
||
|
||
interface TestArtifactRow {
|
||
id: string;
|
||
plan_id: string;
|
||
task_id: string;
|
||
run_id: string;
|
||
kind: string;
|
||
name: string | null;
|
||
uri: string | null;
|
||
payload: string;
|
||
sync_status: string;
|
||
created_at: string;
|
||
}
|
||
|
||
interface TestApprovalRow {
|
||
id: string;
|
||
plan_id: string;
|
||
task_id: string;
|
||
run_id: string;
|
||
title: string;
|
||
status: string;
|
||
payload: string;
|
||
sync_status: string;
|
||
created_at: string;
|
||
updated_at: string;
|
||
}
|
||
|
||
interface TestMemoryRow {
|
||
id: string;
|
||
key: string;
|
||
content: string;
|
||
category: string;
|
||
source: string;
|
||
source_path: string | null;
|
||
session_id: string | null;
|
||
score: number | null;
|
||
status: string;
|
||
payload: string;
|
||
sync_status: string;
|
||
created_at: string;
|
||
updated_at: string;
|
||
deleted_at: string | null;
|
||
}
|
||
|
||
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;
|
||
entity_id: string;
|
||
operation: string;
|
||
payload: string;
|
||
status: string;
|
||
attempts: number;
|
||
created_at: string;
|
||
updated_at: string;
|
||
error: string | null;
|
||
}
|
||
|
||
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>();
|
||
artifacts = new Map<string, TestArtifactRow>();
|
||
approvals = new Map<string, TestApprovalRow>();
|
||
memories = new Map<string, TestMemoryRow>();
|
||
schedules = new Map<string, TestScheduleRow>();
|
||
scheduleRuns = new Map<string, TestScheduleRunRow>();
|
||
outbox = new Map<string, TestOutboxRow>();
|
||
|
||
transaction<T>(callback: () => T): () => T {
|
||
return callback;
|
||
}
|
||
|
||
prepare(sql: string): {
|
||
all: (...args: unknown[]) => unknown[];
|
||
get: (...args: unknown[]) => unknown;
|
||
run: (...args: unknown[]) => unknown;
|
||
} {
|
||
const normalized = sql.replace(/\s+/g, " ").trim();
|
||
return {
|
||
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")) {
|
||
const rows = Array.from(this.planSteps.values());
|
||
if (sql.includes("WHERE plan_id = ?")) {
|
||
return rows
|
||
.filter((step) => step.plan_id === args[0])
|
||
.sort((left, right) => left.seq - right.seq || left.created_at.localeCompare(right.created_at));
|
||
}
|
||
return rows;
|
||
}
|
||
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")) {
|
||
const rows = Array.from(this.events.values());
|
||
const filtered = sql.includes("WHERE kind = 'route_decision'")
|
||
? rows.filter((event) => event.kind === "route_decision")
|
||
: rows;
|
||
return filtered.sort((left, right) => right.occurred_at.localeCompare(left.occurred_at) || right.created_at.localeCompare(left.created_at));
|
||
}
|
||
if (sql.includes("FROM digital_artifacts")) return Array.from(this.artifacts.values());
|
||
if (sql.includes("FROM digital_approvals")) return Array.from(this.approvals.values());
|
||
if (sql.includes("FROM digital_memories")) return Array.from(this.memories.values());
|
||
return [];
|
||
}
|
||
|
||
private get(sql: string, args: unknown[]): unknown {
|
||
if (sql.startsWith("SELECT COUNT(*) AS count FROM digital_runs")) {
|
||
return { count: Array.from(this.runs.values()).filter((run) => run.task_id === args[0]).length };
|
||
}
|
||
if (sql.startsWith("SELECT title, objective FROM digital_plans")) {
|
||
return this.plans.get(args[0] as string);
|
||
}
|
||
if (sql.includes("FROM digital_tasks") && sql.includes("WHERE id = ?")) {
|
||
return this.tasks.get(args[0] as string);
|
||
}
|
||
if (sql.includes("FROM digital_runs") && sql.includes("WHERE id = ?")) {
|
||
return this.runs.get(args[0] as string);
|
||
}
|
||
if (sql.includes("FROM digital_runs") && sql.includes("WHERE task_id = ?")) {
|
||
return Array.from(this.runs.values())
|
||
.filter((run) => run.task_id === args[0])
|
||
.sort((left, right) => right.updated_at.localeCompare(left.updated_at) || right.created_at.localeCompare(left.created_at))[0];
|
||
}
|
||
if (sql.startsWith("SELECT id FROM digital_plan_steps")) {
|
||
return Array.from(this.planSteps.values())
|
||
.filter((step) => step.plan_id === args[0])
|
||
.sort((left, right) => left.seq - right.seq || left.created_at.localeCompare(right.created_at))[0];
|
||
}
|
||
if (sql.includes("FROM digital_plan_steps") && sql.includes("WHERE id = ?")) {
|
||
return this.planSteps.get(args[0] as string);
|
||
}
|
||
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);
|
||
}
|
||
if (sql.includes("FROM digital_memories") && sql.includes("WHERE id = ? OR key = ?")) {
|
||
return Array.from(this.memories.values())
|
||
.filter((memory) => memory.id === args[0] || memory.key === args[1])
|
||
.sort((left, right) => {
|
||
const leftDeleted = left.status === "deleted" ? 1 : 0;
|
||
const rightDeleted = right.status === "deleted" ? 1 : 0;
|
||
return leftDeleted - rightDeleted || right.updated_at.localeCompare(left.updated_at) || right.created_at.localeCompare(left.created_at);
|
||
})[0];
|
||
}
|
||
if (sql.includes("FROM digital_memories") && sql.includes("WHERE id = ?")) {
|
||
return this.memories.get(args[0] as string);
|
||
}
|
||
return { count: 0 };
|
||
}
|
||
|
||
private run(sql: string, args: unknown[]): unknown {
|
||
if (sql.startsWith("INSERT INTO digital_plans")) {
|
||
const [id, title, objective, status, payload, createdAt, updatedAt] = args as [
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
];
|
||
const previous = this.plans.get(id);
|
||
this.plans.set(id, {
|
||
id,
|
||
title,
|
||
objective,
|
||
status,
|
||
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_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_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,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
];
|
||
const previous = this.tasks.get(id);
|
||
this.tasks.set(id, {
|
||
id,
|
||
plan_id: planId,
|
||
title,
|
||
status,
|
||
assigned_skill_id: assignedSkillId,
|
||
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_runs")) {
|
||
const [id, planId, taskId, status, startedAt, payload, createdAt, updatedAt] = args as [
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
];
|
||
const previous = this.runs.get(id);
|
||
this.runs.set(id, {
|
||
id,
|
||
plan_id: planId,
|
||
task_id: taskId,
|
||
status,
|
||
started_at: previous?.started_at ?? startedAt,
|
||
finished_at: previous?.finished_at ?? 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("UPDATE digital_tasks")) {
|
||
const [status, payload, updatedAt, id] = args as [string, string, string, string];
|
||
const previous = this.tasks.get(id);
|
||
if (previous) {
|
||
this.tasks.set(id, {
|
||
...previous,
|
||
status,
|
||
payload,
|
||
updated_at: updatedAt,
|
||
sync_status: "pending",
|
||
});
|
||
}
|
||
return { changes: previous ? 1 : 0 };
|
||
}
|
||
|
||
if (sql.startsWith("UPDATE digital_plan_steps")) {
|
||
const [status, payload, updatedAt, id] = args as [string, string, string, string];
|
||
const previous = this.planSteps.get(id);
|
||
if (previous) {
|
||
this.planSteps.set(id, {
|
||
...previous,
|
||
status,
|
||
payload,
|
||
updated_at: updatedAt,
|
||
sync_status: "pending",
|
||
});
|
||
}
|
||
return { changes: previous ? 1 : 0 };
|
||
}
|
||
|
||
if (sql.startsWith("UPDATE digital_runs") && sql.includes("payload") && sql.includes("finished_at")) {
|
||
const [status, payload, finishedAt, updatedAt, id] = args as [string, string, string, string, string];
|
||
const previous = this.runs.get(id);
|
||
if (previous) {
|
||
this.runs.set(id, {
|
||
...previous,
|
||
status,
|
||
payload,
|
||
finished_at: finishedAt,
|
||
updated_at: updatedAt,
|
||
sync_status: "pending",
|
||
});
|
||
}
|
||
return { changes: previous ? 1 : 0 };
|
||
}
|
||
|
||
if (sql.startsWith("UPDATE digital_runs") && sql.includes("payload")) {
|
||
const [status, payload, updatedAt, id] = args as [string, string, string, string];
|
||
const previous = this.runs.get(id);
|
||
if (previous) {
|
||
this.runs.set(id, {
|
||
...previous,
|
||
status,
|
||
payload,
|
||
updated_at: updatedAt,
|
||
sync_status: "pending",
|
||
});
|
||
}
|
||
return { changes: previous ? 1 : 0 };
|
||
}
|
||
|
||
if (sql.startsWith("UPDATE digital_runs")) {
|
||
const [status, finishedAt, updatedAt, id] = args as [string, string, string, string];
|
||
const previous = this.runs.get(id);
|
||
if (previous) {
|
||
this.runs.set(id, {
|
||
...previous,
|
||
status,
|
||
finished_at: finishedAt,
|
||
updated_at: updatedAt,
|
||
sync_status: "pending",
|
||
});
|
||
}
|
||
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,
|
||
string,
|
||
string,
|
||
string | null,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
];
|
||
if (!this.events.has(id)) {
|
||
this.events.set(id, {
|
||
id,
|
||
plan_id: planId,
|
||
task_id: taskId,
|
||
run_id: runId,
|
||
kind,
|
||
message,
|
||
payload,
|
||
sync_status: "pending",
|
||
occurred_at: occurredAt,
|
||
created_at: createdAt,
|
||
});
|
||
}
|
||
return { changes: 1 };
|
||
}
|
||
|
||
if (sql.startsWith("INSERT INTO digital_artifacts")) {
|
||
const [id, planId, taskId, runId, kind, name, uri, payload, createdAt] = args as [
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string | null,
|
||
string | null,
|
||
string,
|
||
string,
|
||
];
|
||
this.artifacts.set(id, {
|
||
id,
|
||
plan_id: planId,
|
||
task_id: taskId,
|
||
run_id: runId,
|
||
kind,
|
||
name,
|
||
uri,
|
||
payload,
|
||
sync_status: "pending",
|
||
created_at: createdAt,
|
||
});
|
||
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,
|
||
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_memories")) {
|
||
const [id, key, content, category, source, sourcePath, sessionId, score, status, payload, createdAt, updatedAt, deletedAt] = args as [
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string | null,
|
||
string | null,
|
||
number | null,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string | null,
|
||
];
|
||
const previous = this.memories.get(id);
|
||
this.memories.set(id, {
|
||
id,
|
||
key,
|
||
content,
|
||
category,
|
||
source,
|
||
source_path: sourcePath,
|
||
session_id: sessionId,
|
||
score,
|
||
status,
|
||
payload,
|
||
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("UPDATE digital_memories")) {
|
||
const [deletedAt, updatedAt, id] = args as [string, string, string];
|
||
const previous = this.memories.get(id);
|
||
if (previous) {
|
||
this.memories.set(id, {
|
||
...previous,
|
||
status: "deleted",
|
||
deleted_at: deletedAt,
|
||
updated_at: updatedAt,
|
||
sync_status: "pending",
|
||
});
|
||
}
|
||
return { changes: previous ? 1 : 0 };
|
||
}
|
||
|
||
if (sql.startsWith("INSERT INTO digital_sync_outbox")) {
|
||
const [id, entityType, entityId, operation, payload, createdAt, updatedAt] = args as [
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
string,
|
||
];
|
||
this.outbox.set(id, {
|
||
id,
|
||
entity_type: entityType,
|
||
entity_id: entityId,
|
||
operation,
|
||
payload,
|
||
status: "pending",
|
||
attempts: 0,
|
||
created_at: createdAt,
|
||
updated_at: updatedAt,
|
||
error: null,
|
||
});
|
||
return { changes: 1 };
|
||
}
|
||
|
||
throw new Error(`Unhandled run query: ${sql}`);
|
||
}
|
||
}
|