fix: deeply copy selected-step bindings

This commit is contained in:
lda
2026-08-09 23:06:37 +07:00 Verified
parent 96298161d2
commit 15a2b61a17
2 changed files with 148 additions and 8 deletions
@@ -8,6 +8,7 @@ import {
type InputBinding,
type InputPathBinding,
type InputValueBinding,
type OutputBinding,
type SetDraftRouteInput,
type SetStepInputBindingsInput,
type SetStepOutputBindingsInput,
@@ -391,4 +392,88 @@ describe("DraftAuthoringClient", () => {
});
expect(run).not.toHaveBeenCalled();
});
it("deeply copies input binding records, paths, and JSON values", async () => {
const { executor: writeExecutor, run } = createExecutor();
const client = createDraftAuthoringClient(writeExecutor);
const pathBinding = {
path: { root: "context", parts: ["request", "text"] },
target: { root: "local", parts: ["text"] },
} satisfies InputPathBinding;
const valueBinding = {
target: { root: "local", parts: ["optional"] },
value: { nested: ["keep"] },
} satisfies InputValueBinding;
const bindings = [pathBinding, valueBinding] satisfies SetStepInputBindingsInput["bindings"];
await client.setStepInputBindings({
workspaceId: "report",
revision: 7,
stepId: "render",
bindings,
});
if (typeof pathBinding.path !== "string") pathBinding.path.parts[0] = "changed";
if (typeof pathBinding.target !== "string") pathBinding.target.parts[0] = "changed";
pathBinding.path = { root: "context", parts: ["changed.path"] };
pathBinding.target = { root: "local", parts: ["changed.target"] };
valueBinding.target.parts[0] = "changed";
valueBinding.value.nested[0] = "changed";
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: { nested: ["keep"] },
},
],
},
decodeDraftWorkspace,
);
});
it("deeply copies output binding records and structural paths", async () => {
const { executor: writeExecutor, run } = createExecutor();
const client = createDraftAuthoringClient(writeExecutor);
const outputBinding = {
source: { root: "local", parts: ["text"] },
target: { root: "state", parts: ["report", "latest"] },
} satisfies OutputBinding;
const bindings = [outputBinding] satisfies SetStepOutputBindingsInput["bindings"];
await client.setStepOutputBindings({
workspaceId: "report",
revision: 7,
stepId: "render",
bindings,
});
if (typeof outputBinding.source !== "string") outputBinding.source.parts[0] = "changed";
if (typeof outputBinding.target !== "string") outputBinding.target.parts[0] = "changed";
outputBinding.source = { root: "local", parts: ["changed.source"] };
outputBinding.target = { root: "state", parts: ["changed.target"] };
expect(run).toHaveBeenCalledWith(
"workflow.draft_workspaces.set_step_output_bindings",
{
workspace_id: "report",
revision: 7,
step_id: "render",
bindings: [
{
source: { root: "local", parts: ["text"] },
target: { root: "state", parts: ["report", "latest"] },
},
],
},
decodeDraftWorkspace,
);
});
});
@@ -56,10 +56,65 @@ const ifDefined = <T>(
if (value !== undefined) target[key] = value;
};
const copyBindings = <T>(
bindings: ReadonlyArray<T> | null | undefined,
): T[] | null | undefined =>
bindings === undefined || bindings === null ? bindings : [...bindings];
const copyJsonValue = (value: unknown): unknown => {
if (Array.isArray(value)) return value.map(copyJsonValue);
if (value !== null && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, nestedValue]) => [
key,
copyJsonValue(nestedValue),
]),
);
}
return value;
};
const copyInputBinding = (binding: InputBinding): InputBinding => {
if ("path" in binding) {
return {
path:
typeof binding.path === "string"
? binding.path
: { root: binding.path.root, parts: [...binding.path.parts] },
target:
typeof binding.target === "string"
? binding.target
: { root: binding.target.root, parts: [...binding.target.parts] },
};
}
return {
target:
typeof binding.target === "string"
? binding.target
: { root: binding.target.root, parts: [...binding.target.parts] },
value: copyJsonValue(binding.value),
};
};
const copyOutputBinding = (binding: OutputBinding): OutputBinding => ({
source:
typeof binding.source === "string"
? binding.source
: { root: binding.source.root, parts: [...binding.source.parts] },
target:
typeof binding.target === "string"
? binding.target
: { root: binding.target.root, parts: [...binding.target.parts] },
});
const copyInputBindings = (
bindings: ReadonlyArray<InputBinding> | null | undefined,
): InputBinding[] | null | undefined =>
bindings === undefined || bindings === null
? bindings
: bindings.map(copyInputBinding);
const copyOutputBindings = (
bindings: ReadonlyArray<OutputBinding> | null | undefined,
): OutputBinding[] | null | undefined =>
bindings === undefined || bindings === null
? bindings
: bindings.map(copyOutputBinding);
export const createDraftAuthoringClient = (
executor: ConsoleWriteExecutor,
@@ -125,7 +180,7 @@ export const createDraftAuthoringClient = (
);
ifDefined(params, "routes", input.routes);
ifDefined(params, "input_map", input.inputMap);
ifDefined(params, "input_bindings", copyBindings(input.inputBindings));
ifDefined(params, "input_bindings", copyInputBindings(input.inputBindings));
ifDefined(params, "bind_outputs", input.bindOutputs);
ifDefined(params, "desc", input.description);
ifDefined(params, "retry", input.retry);
@@ -137,7 +192,7 @@ export const createDraftAuthoringClient = (
const operation = "workflow.draft_workspaces.update_capability_step";
const update: Record<string, unknown> = {};
ifDefined(update, "desc", input.update.description);
ifDefined(update, "input", copyBindings(input.update.input));
ifDefined(update, "input", copyInputBindings(input.update.input));
ifDefined(update, "retry", input.update.retry);
ifDefined(update, "timeout_seconds", input.update.timeoutSeconds);
return executor.run(
@@ -160,7 +215,7 @@ export const createDraftAuthoringClient = (
workspace_id: requireIdentifier(operation, input.workspaceId, "workspace id"),
revision: input.revision,
step_id: requireIdentifier(operation, input.stepId, "step id"),
bindings: copyBindings<InputBinding>(input.bindings),
bindings: copyInputBindings(input.bindings),
},
decodeDraftWorkspace,
);
@@ -174,7 +229,7 @@ export const createDraftAuthoringClient = (
workspace_id: requireIdentifier(operation, input.workspaceId, "workspace id"),
revision: input.revision,
step_id: requireIdentifier(operation, input.stepId, "step id"),
bindings: copyBindings<OutputBinding>(input.bindings),
bindings: copyOutputBindings(input.bindings),
},
decodeDraftWorkspace,
);