feat(client): project digital employee runtime into UI

This commit is contained in:
baiyanyun
2026-06-07 17:46:07 +08:00
parent 8ff1c57b83
commit b07f133ed6
15 changed files with 2639 additions and 252 deletions

View File

@@ -158,10 +158,62 @@ describe("digital employee sync service", () => {
],
});
});
it("marks artifact and approval acknowledgements as synced", async () => {
insertEntity("artifact", "artifact-1");
insertEntity("approval", "approval-1");
insertOutbox("artifact:upsert:artifact-1", "artifact", "artifact-1");
insertOutbox("approval:upsert:approval-1", "approval", "approval-1");
mockState.fetch.mockResolvedValue(
jsonResponse({
success: true,
acked: [
{
entity_type: "artifact",
entity_id: "artifact-1",
remote_id: "remote-artifact-1",
},
{
entity_type: "approval",
entity_id: "approval-1",
remote_id: "remote-approval-1",
},
],
}),
);
const { flushDigitalEmployeeSyncOutbox } = await import("./syncService");
const status = await flushDigitalEmployeeSyncOutbox({ force: true });
expect(status.pending).toBe(0);
expect(status.failed).toBe(0);
expect(readOutbox("artifact:upsert:artifact-1")).toMatchObject({
status: "synced",
attempts: 1,
});
expect(readOutbox("approval:upsert:approval-1")).toMatchObject({
status: "synced",
attempts: 1,
});
expect(readEntity("artifact", "artifact-1")).toMatchObject({
sync_status: "synced",
remote_id: "remote-artifact-1",
sync_error: null,
});
expect(readEntity("approval", "approval-1")).toMatchObject({
sync_status: "synced",
remote_id: "remote-approval-1",
sync_error: null,
});
});
});
function insertPlan(id: string): void {
mockState.db?.plans.set(id, {
insertEntity("plan", id);
}
function insertEntity(entityType: string, id: string): void {
entityMap(entityType)?.set(id, {
id,
remote_id: null,
sync_status: "pending",
@@ -191,7 +243,24 @@ function readOutbox(id: string): Record<string, unknown> | undefined {
}
function readPlan(id: string): Record<string, unknown> | undefined {
return mockState.db?.plans.get(id);
return readEntity("plan", id);
}
function readEntity(
entityType: string,
id: string,
): Record<string, unknown> | undefined {
return entityMap(entityType)?.get(id);
}
function entityMap(
entityType: string,
): Map<string, TestSyncEntityRow> | undefined {
if (!mockState.db) return undefined;
if (entityType === "plan") return mockState.db.plans;
if (entityType === "artifact") return mockState.db.artifacts;
if (entityType === "approval") return mockState.db.approvals;
return undefined;
}
function readAttempts(): Array<Record<string, unknown>> {
@@ -220,7 +289,7 @@ interface TestOutboxRow {
updated_at: string;
}
interface TestPlanRow {
interface TestSyncEntityRow {
id: string;
remote_id: string | null;
sync_status: string;
@@ -244,7 +313,9 @@ interface TestAttemptRow {
class TestDb {
outbox = new Map<string, TestOutboxRow>();
plans = new Map<string, TestPlanRow>();
plans = new Map<string, TestSyncEntityRow>();
artifacts = new Map<string, TestSyncEntityRow>();
approvals = new Map<string, TestSyncEntityRow>();
attempts: TestAttemptRow[] = [];
private nextAttemptId = 1;
@@ -427,9 +498,9 @@ class TestDb {
return { changes: before - this.attempts.length };
}
if (sql.startsWith("UPDATE digital_plans SET sync_status = 'synced'")) {
if (/^UPDATE digital_(plans|artifacts|approvals) SET sync_status = 'synced'/.test(sql)) {
const [remoteId, lastSyncedAt, id] = args as [string | null, string, string];
const row = this.plans.get(id);
const row = this.entityRowsForUpdate(sql).get(id);
if (row) {
row.sync_status = "synced";
row.remote_id = remoteId ?? row.remote_id;
@@ -439,9 +510,9 @@ class TestDb {
return { changes: row ? 1 : 0 };
}
if (sql.startsWith("UPDATE digital_plans SET sync_status = 'failed'")) {
if (/^UPDATE digital_(plans|artifacts|approvals) SET sync_status = 'failed'/.test(sql)) {
const [syncError, id] = args as [string, string];
const row = this.plans.get(id);
const row = this.entityRowsForUpdate(sql).get(id);
if (row) {
row.sync_status = "failed";
row.sync_error = syncError;
@@ -451,6 +522,12 @@ class TestDb {
throw new Error(`Unhandled run query: ${sql}`);
}
private entityRowsForUpdate(sql: string): Map<string, TestSyncEntityRow> {
if (sql.startsWith("UPDATE digital_artifacts")) return this.artifacts;
if (sql.startsWith("UPDATE digital_approvals")) return this.approvals;
return this.plans;
}
}
function copyRow<T extends object>(row: T): T {