fix: harden authoring schema and contract projections

This commit is contained in:
lda
2026-08-29 21:08:05 +07:00 Verified
parent 8b16c6403d
commit 0570ef8a78
14 changed files with 243 additions and 359 deletions
@@ -531,7 +531,9 @@ export const authoredRpcSchemas = {
payload: Schema.Struct({
workspace_id: Schema.String.pipe(Schema.minLength(1)),
revision: PositiveIntegerSchema,
selected_step_id: Schema.optional(Schema.NullOr(Schema.String)),
selected_step_id: Schema.optional(
Schema.NullOr(Schema.String.pipe(Schema.minLength(1))),
),
}),
success: Schema.Union(AuthoringContractInventorySchema, DraftWorkspaceSchema),
},
@@ -548,7 +548,11 @@ const parityCases: ReadonlyArray<ParityCase> = [
revision: 7,
selected_step_id: "render",
},
invalidPayload: { workspace_id: "console.demo", revision: 0 },
invalidPayload: {
workspace_id: "console.demo",
revision: 7,
selected_step_id: "",
},
validSuccess: authoringContractInventory,
invalidSuccess: {
...authoringContractInventory,
@@ -61,6 +61,35 @@ describe("run operation registry", () => {
});
});
it("preserves explicit null and empty contract replacement semantics", async () => {
const { getOperationMeta } = await import("./method-registry.js");
const operation = getOperationMeta("workflow.draft_workspaces.set_contract");
if (operation === undefined) throw new Error("missing set contract operation");
const cli = operation.equivalentCli({
workspace_id: "console.demo",
revision: 7,
input_schema: null,
outcomes: [],
});
expect(cli).toContain("input_schema=null (no equivalent CLI clear flag)");
expect(cli).toContain("outcomes=[] (no equivalent CLI clear flag)");
expect(cli).not.toContain("--outcome");
});
it("renders non-empty contract outcomes as repeated CLI flags", async () => {
const { getOperationMeta } = await import("./method-registry.js");
const operation = getOperationMeta("workflow.draft_workspaces.set_contract");
if (operation === undefined) throw new Error("missing set contract operation");
expect(operation.equivalentCli({
workspace_id: "console.demo",
revision: 7,
outcomes: ["ok", "cancelled"],
})).toContain("--outcome ok --outcome cancelled");
});
it("decodes start results with the start success schema", async () => {
vi.doMock("./rpcs.js", async () => {
const actual = await vi.importActual<typeof import("./rpcs.js")>(
+25 -5
View File
@@ -839,11 +839,31 @@ const operationEntries = defineOperationEntries([
String(p.revision),
];
const unavailable: string[] = [];
if (p.input_schema != null) unavailable.push("input_schema (use --input-schema-file)");
if (p.state_schema != null) unavailable.push("state_schema (use --state-schema-file)");
if (p.output_schema != null) unavailable.push("output_schema (use --output-schema-file)");
for (const outcome of p.outcomes ?? []) {
parts.push("--outcome", shellArg(outcome));
if (p.input_schema !== undefined) {
unavailable.push(p.input_schema === null
? "input_schema=null (no equivalent CLI clear flag)"
: "input_schema (use --input-schema-file)");
}
if (p.state_schema !== undefined) {
unavailable.push(p.state_schema === null
? "state_schema=null (no equivalent CLI clear flag)"
: "state_schema (use --state-schema-file)");
}
if (p.output_schema !== undefined) {
unavailable.push(p.output_schema === null
? "output_schema=null (no equivalent CLI clear flag)"
: "output_schema (use --output-schema-file)");
}
if (p.outcomes !== undefined) {
if (p.outcomes === null) {
unavailable.push("outcomes=null (no equivalent CLI clear flag)");
} else if (p.outcomes.length === 0) {
unavailable.push("outcomes=[] (no equivalent CLI clear flag)");
} else {
for (const outcome of p.outcomes) {
parts.push("--outcome", shellArg(outcome));
}
}
}
return nonEquivalentCli(parts.join(" "), unavailable);
},