feat(client): project digital employee runtime into UI
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
db: null as TestDb | null,
|
||||
settings: new Map<string, unknown>(),
|
||||
}));
|
||||
|
||||
vi.mock("../../db", () => ({
|
||||
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.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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 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 {
|
||||
artifacts = new Map<string, TestArtifactRow>();
|
||||
approvals = new Map<string, TestApprovalRow>();
|
||||
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: () => [],
|
||||
get: () => ({ count: 0 }),
|
||||
run: (...args: unknown[]) => this.run(normalized, args),
|
||||
};
|
||||
}
|
||||
|
||||
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_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,
|
||||
];
|
||||
this.approvals.set(id, {
|
||||
id,
|
||||
plan_id: planId,
|
||||
task_id: taskId,
|
||||
run_id: runId,
|
||||
title,
|
||||
status,
|
||||
payload,
|
||||
sync_status: "pending",
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt,
|
||||
});
|
||||
return { changes: 1 };
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user