fix: deeply copy selected-step bindings
This commit is contained in:
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user