吸收数字员工入口路由决策

This commit is contained in:
baiyanyun
2026-06-09 15:32:38 +08:00
parent fd130f1f60
commit 702ea05d4d
9 changed files with 598 additions and 144 deletions

View File

@@ -83,6 +83,91 @@ describe("digital employee state service", () => {
expect(mockState.ensureSchemaCalls).toBeGreaterThan(0);
});
it("records and lists route decisions as syncable events", async () => {
const {
listDigitalEmployeeRouteDecisions,
recordDigitalEmployeeRouteDecision,
} = await import("./stateService");
const first = recordDigitalEmployeeRouteDecision({
idempotencyKey: "route-key-1",
correlationId: "corr-1",
routeKind: "PlanExecution",
intentKind: "execute",
reasonCodes: ["ready_step_context"],
candidateSkills: ["qimingclaw-computer-control"],
contextRefs: { plan_id: "plan-1", step_id: "step-ready" },
createsTaskRun: true,
entrypoint: "task_center",
actor: "operator",
envelope: { text: "开始执行这个步骤" },
recordedAt: "2026-06-07T08:01:00.000Z",
});
const second = recordDigitalEmployeeRouteDecision({
idempotencyKey: "route-key-2",
correlationId: "corr-2",
routeKind: "ProjectionQuery",
intentKind: "query",
reasonCodes: ["projection_query_text"],
candidateSkills: ["qimingclaw-artifact-report"],
contextRefs: { plan_id: "plan-1" },
recordedAt: "2026-06-07T08:02:00.000Z",
});
expect(first).toMatchObject({
id: "route-decision:route-key-1",
route_kind: "PlanExecution",
creates_task_run: true,
context_refs: { step_id: "step-ready" },
});
expect(mockState.db?.events.get(first.id)).toMatchObject({
kind: "route_decision",
run_id: null,
plan_id: null,
});
expect(JSON.parse(mockState.db?.events.get(first.id)?.payload ?? "{}")).toMatchObject({
source: "qimingclaw-route-decision",
routeDecision: {
id: first.id,
route_kind: "PlanExecution",
intent_kind: "execute",
},
envelope: { text: "开始执行这个步骤" },
});
expect(mockState.db?.outbox.get(`event:insert:${first.id}`)).toMatchObject({
entity_type: "event",
operation: "insert",
status: "pending",
});
expect(listDigitalEmployeeRouteDecisions()).toEqual([
expect.objectContaining({ id: second.id, route_kind: "ProjectionQuery" }),
expect.objectContaining({ id: first.id, route_kind: "PlanExecution" }),
]);
});
it("keeps route decision ids stable for idempotency keys", async () => {
const { listDigitalEmployeeRouteDecisions, recordDigitalEmployeeRouteDecision } = await import("./stateService");
const first = recordDigitalEmployeeRouteDecision({
idempotencyKey: "repeat-route-key",
routeKind: "ChatOnly",
intentKind: "chat",
recordedAt: "2026-06-07T08:01:00.000Z",
});
const second = recordDigitalEmployeeRouteDecision({
idempotencyKey: "repeat-route-key",
routeKind: "PlanExecution",
intentKind: "execute",
recordedAt: "2026-06-07T08:02:00.000Z",
});
expect(second.id).toBe(first.id);
expect(Array.from(mockState.db?.events.values() ?? []).filter((event) => event.kind === "route_decision")).toHaveLength(1);
expect(listDigitalEmployeeRouteDecisions()).toEqual([
expect.objectContaining({ id: first.id, route_kind: "ChatOnly" }),
]);
});
it("updates formal approval records when UI approval decisions are saved", async () => {
const { saveDigitalEmployeeUiState } = await import("./stateService");
mockState.db?.approvals.set("approval-1", {
@@ -1166,7 +1251,13 @@ class TestDb {
if (sql.includes("FROM digital_schedule_runs")) return Array.from(this.scheduleRuns.values());
if (sql.includes("FROM digital_tasks")) return Array.from(this.tasks.values());
if (sql.includes("FROM digital_runs")) return Array.from(this.runs.values());
if (sql.includes("FROM digital_events")) return Array.from(this.events.values());
if (sql.includes("FROM digital_events")) {
const rows = Array.from(this.events.values());
const filtered = sql.includes("WHERE kind = 'route_decision'")
? rows.filter((event) => event.kind === "route_decision")
: rows;
return filtered.sort((left, right) => right.occurred_at.localeCompare(left.occurred_at) || right.created_at.localeCompare(left.created_at));
}
if (sql.includes("FROM digital_artifacts")) return Array.from(this.artifacts.values());
if (sql.includes("FROM digital_approvals")) return Array.from(this.approvals.values());
if (sql.includes("FROM digital_memories")) return Array.from(this.memories.values());