feat(client): persist digital governance events
This commit is contained in:
@@ -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 [
|
||||
|
||||
Reference in New Issue
Block a user