feat(client): persist digital governance events

This commit is contained in:
baiyanyun
2026-06-07 18:59:59 +08:00
parent 5e76828afe
commit e37024bdbe
14 changed files with 925 additions and 158 deletions

View File

@@ -2,11 +2,15 @@ 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: () => true,
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),
@@ -17,6 +21,7 @@ describe("digital employee state service", () => {
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>();
});
@@ -75,6 +80,7 @@ describe("digital employee state service", () => {
entity_id: "computer-chat-run-req-1:approval:approval-1",
status: "pending",
});
expect(mockState.ensureSchemaCalls).toBeGreaterThan(0);
});
it("updates formal approval records when UI approval decisions are saved", async () => {
@@ -135,9 +141,112 @@ describe("digital employee state service", () => {
entity_id: "approval-1",
status: "pending",
});
expect(mockState.ensureSchemaCalls).toBeGreaterThan(0);
});
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: "把今日客户跟进事项整理成可执行计划",
},
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?.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.ensureSchemaCalls).toBeGreaterThan(0);
});
});
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 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;
kind: string;
message: string;
payload: string;
sync_status: string;
occurred_at: string;
created_at: string;
}
interface TestArtifactRow {
id: string;
plan_id: string;
@@ -178,6 +287,10 @@ interface TestOutboxRow {
}
class TestDb {
plans = new Map<string, TestPlanRow>();
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>();
outbox = new Map<string, TestOutboxRow>();
@@ -207,11 +320,126 @@ class TestDb {
}
private run(sql: string, args: unknown[]): unknown {
if (sql.startsWith("INSERT INTO digital_plans")) return { changes: 1 };
if (sql.startsWith("INSERT INTO digital_tasks")) return { changes: 1 };
if (sql.startsWith("INSERT INTO digital_runs")) return { changes: 1 };
if (sql.startsWith("UPDATE digital_runs")) return { changes: 1 };
if (sql.startsWith("INSERT OR IGNORE INTO digital_events")) return { changes: 1 };
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_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_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("INSERT OR IGNORE INTO digital_events")) {
const [id, planId, taskId, runId, kind, message, payload, occurredAt, createdAt] = args as [
string,
string,
string,
string,
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 [