feat(client): project digital employee runtime into UI
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user