feat: add selected-step binding client

This commit is contained in:
lda
2026-08-09 23:02:42 +07:00 Verified
parent e719c40da7
commit 96298161d2
6 changed files with 179 additions and 3 deletions
@@ -69,6 +69,8 @@ const authoringClient: DraftAuthoringClient = {
createFromCapability: vi.fn(),
addCapabilityStep: vi.fn(),
updateCapabilityStep: vi.fn(),
setStepInputBindings: vi.fn(),
setStepOutputBindings: vi.fn(),
setRoute: vi.fn(),
validate: vi.fn(),
};
@@ -55,6 +55,8 @@ const createEmpty = vi.fn<DraftAuthoringClient["createEmpty"]>();
const createFromCapability = vi.fn<DraftAuthoringClient["createFromCapability"]>();
const addCapabilityStep = vi.fn<DraftAuthoringClient["addCapabilityStep"]>();
const updateCapabilityStep = vi.fn<DraftAuthoringClient["updateCapabilityStep"]>();
const setStepInputBindings = vi.fn<DraftAuthoringClient["setStepInputBindings"]>();
const setStepOutputBindings = vi.fn<DraftAuthoringClient["setStepOutputBindings"]>();
const setRoute = vi.fn<DraftAuthoringClient["setRoute"]>();
const validate = vi.fn<DraftAuthoringClient["validate"]>();
const list = vi.fn<DraftWorkspaceClient["list"]>();
@@ -64,6 +66,8 @@ const authoringClient = {
createFromCapability,
addCapabilityStep,
updateCapabilityStep,
setStepInputBindings,
setStepOutputBindings,
setRoute,
validate,
} satisfies DraftAuthoringClient;
@@ -9,6 +9,8 @@ import {
type InputPathBinding,
type InputValueBinding,
type SetDraftRouteInput,
type SetStepInputBindingsInput,
type SetStepOutputBindingsInput,
type UpdateCapabilityStepInput,
} from "./draft-workspace-models.js";
import { createDraftAuthoringClient } from "./draft-authoring-client.js";
@@ -258,4 +260,135 @@ describe("DraftAuthoringClient", () => {
/DraftWorkspace is malformed/,
);
});
it("sets input bindings with canonical structural paths and preserves null values", async () => {
const { executor: writeExecutor, run } = createExecutor();
const client = createDraftAuthoringClient(writeExecutor);
const bindings = [
{
path: { root: "context", parts: ["request", "text"] },
target: { root: "local", parts: ["text"] },
},
{
target: { root: "local", parts: ["optional"] },
value: null,
},
] satisfies SetStepInputBindingsInput["bindings"];
const input = {
workspaceId: " report ",
revision: 7,
stepId: " render ",
bindings,
} satisfies SetStepInputBindingsInput;
await expect(client.setStepInputBindings(input)).resolves.toEqual(canonicalWorkspace);
expect(run).toHaveBeenCalledWith(
"workflow.draft_workspaces.set_step_input_bindings",
{
workspace_id: "report",
revision: 7,
step_id: "render",
bindings,
},
decodeDraftWorkspace,
);
bindings.splice(0, bindings.length);
expect(run).toHaveBeenCalledWith(
"workflow.draft_workspaces.set_step_input_bindings",
{
workspace_id: "report",
revision: 7,
step_id: "render",
bindings: [
{
path: { root: "context", parts: ["request", "text"] },
target: { root: "local", parts: ["text"] },
},
{
target: { root: "local", parts: ["optional"] },
value: null,
},
],
},
decodeDraftWorkspace,
);
});
it("sets output bindings with an exact ordered copy, including empty lists", async () => {
const { executor: writeExecutor, run } = createExecutor();
const client = createDraftAuthoringClient(writeExecutor);
const bindings = [
{ source: "text", target: "state.report" },
{ source: "text", target: "state.audit.latest" },
] satisfies SetStepOutputBindingsInput["bindings"];
const input = {
workspaceId: "report",
revision: 7,
stepId: "render",
bindings,
} satisfies SetStepOutputBindingsInput;
await client.setStepOutputBindings(input);
expect(run).toHaveBeenCalledWith(
"workflow.draft_workspaces.set_step_output_bindings",
{
workspace_id: "report",
revision: 7,
step_id: "render",
bindings: [
{ source: "text", target: "state.report" },
{ source: "text", target: "state.audit.latest" },
],
},
decodeDraftWorkspace,
);
bindings.splice(0, bindings.length);
expect(run).toHaveBeenCalledWith(
"workflow.draft_workspaces.set_step_output_bindings",
{
workspace_id: "report",
revision: 7,
step_id: "render",
bindings: [
{ source: "text", target: "state.report" },
{ source: "text", target: "state.audit.latest" },
],
},
decodeDraftWorkspace,
);
await client.setStepOutputBindings({ ...input, bindings: [] });
expect(run).toHaveBeenLastCalledWith(
"workflow.draft_workspaces.set_step_output_bindings",
{
workspace_id: "report",
revision: 7,
step_id: "render",
bindings: [],
},
decodeDraftWorkspace,
);
});
it("rejects blank identifiers for focused binding methods before transport", async () => {
const { executor: writeExecutor, run } = createExecutor();
const client = createDraftAuthoringClient(writeExecutor);
const input = {
workspaceId: "report",
revision: 7,
stepId: "render",
bindings: [],
} satisfies SetStepOutputBindingsInput;
await expect(client.setStepInputBindings({ ...input, workspaceId: " " })).rejects.toMatchObject({
kind: "operation",
operation: "workflow.draft_workspaces.set_step_input_bindings",
});
await expect(client.setStepOutputBindings({ ...input, stepId: "\t" })).rejects.toMatchObject({
kind: "operation",
operation: "workflow.draft_workspaces.set_step_output_bindings",
});
expect(run).not.toHaveBeenCalled();
});
});
@@ -6,7 +6,10 @@ import {
type CreateFromCapabilityInput,
type DraftWorkspace,
type InputBinding,
type OutputBinding,
type SetDraftRouteInput,
type SetStepInputBindingsInput,
type SetStepOutputBindingsInput,
type UpdateCapabilityStepInput,
} from "./draft-workspace-models.js";
import { ConsoleClientError } from "./errors.js";
@@ -17,6 +20,8 @@ export interface DraftAuthoringClient {
createFromCapability(input: CreateFromCapabilityInput): Promise<DraftWorkspace>;
addCapabilityStep(input: AddCapabilityStepInput): Promise<DraftWorkspace>;
updateCapabilityStep(input: UpdateCapabilityStepInput): Promise<DraftWorkspace>;
setStepInputBindings(input: SetStepInputBindingsInput): Promise<DraftWorkspace>;
setStepOutputBindings(input: SetStepOutputBindingsInput): Promise<DraftWorkspace>;
setRoute(input: SetDraftRouteInput): Promise<DraftWorkspace>;
validate(workspaceId: string): Promise<DraftWorkspace>;
}
@@ -51,9 +56,9 @@ const ifDefined = <T>(
if (value !== undefined) target[key] = value;
};
const copyBindings = (
bindings: ReadonlyArray<InputBinding> | null | undefined,
): InputBinding[] | null | undefined =>
const copyBindings = <T>(
bindings: ReadonlyArray<T> | null | undefined,
): T[] | null | undefined =>
bindings === undefined || bindings === null ? bindings : [...bindings];
export const createDraftAuthoringClient = (
@@ -147,6 +152,34 @@ export const createDraftAuthoringClient = (
);
},
setStepInputBindings: async (input) => {
const operation = "workflow.draft_workspaces.set_step_input_bindings";
return executor.run(
operation,
{
workspace_id: requireIdentifier(operation, input.workspaceId, "workspace id"),
revision: input.revision,
step_id: requireIdentifier(operation, input.stepId, "step id"),
bindings: copyBindings<InputBinding>(input.bindings),
},
decodeDraftWorkspace,
);
},
setStepOutputBindings: async (input) => {
const operation = "workflow.draft_workspaces.set_step_output_bindings";
return executor.run(
operation,
{
workspace_id: requireIdentifier(operation, input.workspaceId, "workspace id"),
revision: input.revision,
step_id: requireIdentifier(operation, input.stepId, "step id"),
bindings: copyBindings<OutputBinding>(input.bindings),
},
decodeDraftWorkspace,
);
},
setRoute: async (input) => {
const operation = "workflow.draft_workspaces.set_route";
return executor.run(
@@ -95,6 +95,8 @@ const authoringClient: DraftAuthoringClient = {
createFromCapability: vi.fn(),
addCapabilityStep: vi.fn(),
updateCapabilityStep: vi.fn(),
setStepInputBindings: vi.fn(),
setStepOutputBindings: vi.fn(),
setRoute: vi.fn(),
validate: vi.fn(),
};
@@ -67,6 +67,8 @@ const authoringClient: DraftAuthoringClient = {
createFromCapability: vi.fn(),
addCapabilityStep: vi.fn(),
updateCapabilityStep: vi.fn(),
setStepInputBindings: vi.fn(),
setStepOutputBindings: vi.fn(),
setRoute: vi.fn(),
validate: vi.fn(),
};