diff --git a/apps/server/src/services/sessions/playbook-session-adapter/runtime.test.ts b/apps/server/src/services/sessions/playbook-session-adapter/runtime.test.ts index 19d9b68..11c2b06 100644 --- a/apps/server/src/services/sessions/playbook-session-adapter/runtime.test.ts +++ b/apps/server/src/services/sessions/playbook-session-adapter/runtime.test.ts @@ -6,6 +6,7 @@ import { PlaybookAgentIdentityMismatchError, pickPlaybookAgent, type RemotePlaybookAgent, + readinessFromPick, } from "./runtime"; import type { PlaybookSessionDetail } from "./sessions"; @@ -77,6 +78,28 @@ describe("pickPlaybookAgent", () => { }); }); +describe("readinessFromPick", () => { + test("maps agent picks to playbook readiness states", () => { + expect(readinessFromPick({ agent: agent(), duplicates: [], identityMismatch: false }, "designer")).toEqual({ + status: "ready", + playbookId: "designer", + remoteAgentId: "agent_1", + }); + + expect(readinessFromPick({ agent: undefined, duplicates: [], identityMismatch: false }, "designer")).toEqual({ + status: "missing", + playbookId: "designer", + reason: "not_provisioned", + }); + + expect(readinessFromPick({ agent: undefined, duplicates: [], identityMismatch: true }, "designer")).toMatchObject({ + status: "blocked", + playbookId: "designer", + reason: "identity_mismatch", + }); + }); +}); + describe("createPlaybookSessionRuntime", () => { test("start ensures the selected agent, starts the provider session, attaches events, then returns detail", async () => { const calls: string[] = []; diff --git a/apps/server/src/services/sessions/playbook-session-adapter/runtime.ts b/apps/server/src/services/sessions/playbook-session-adapter/runtime.ts index cc5796f..06fad7c 100644 --- a/apps/server/src/services/sessions/playbook-session-adapter/runtime.ts +++ b/apps/server/src/services/sessions/playbook-session-adapter/runtime.ts @@ -94,6 +94,12 @@ export type PlaybookAgentPick = { identityMismatch: boolean; }; +export type PlaybookReadiness = + | { status: "ready"; playbookId: string; remoteAgentId: string } + | { status: "missing"; playbookId: string; reason: "not_provisioned" } + | { status: "unknown"; playbookId: string; reason: "not_checked" | "check_failed" } + | { status: "blocked"; playbookId: string; reason: "identity_mismatch"; message: string }; + // --------------------------------------------------------------------------- // Identity-mismatch error // --------------------------------------------------------------------------- @@ -135,6 +141,21 @@ export function pickPlaybookAgent( return { agent, duplicates, identityMismatch: false }; } +// The single source of the pick-to-readiness mapping, so identity-drift +// handling lives in one place. +export function readinessFromPick(pick: PlaybookAgentPick, playbookId: string): PlaybookReadiness { + if (pick.identityMismatch) { + return { + status: "blocked", + playbookId, + reason: "identity_mismatch", + message: playbookIdentityMismatchMessage(playbookId), + }; + } + if (!pick.agent) return { status: "missing", playbookId, reason: "not_provisioned" }; + return { status: "ready", playbookId, remoteAgentId: pick.agent.id }; +} + // --------------------------------------------------------------------------- // Dependency contract (concrete types, no generics) // ---------------------------------------------------------------------------