408 lines
12 KiB
TypeScript
408 lines
12 KiB
TypeScript
import { Either, Schema } from "effect";
|
|
import { describe, expect, it } from "vitest";
|
|
import type {
|
|
WorkflowOperationParams,
|
|
WorkflowOperationResult,
|
|
} from "../generated/workflow-contract.js";
|
|
import { workflowRuntimeContract } from "../generated/workflow-contract.js";
|
|
import { runtimeSchemasFor } from "./runtime-schema.js";
|
|
|
|
const accepts = (schema: Schema.Schema.AnyNoContext, value: unknown): boolean =>
|
|
Either.isRight(
|
|
Schema.decodeUnknownEither(schema)(value, { onExcessProperty: "error" }),
|
|
);
|
|
|
|
describe("runtimeSchemasFor", () => {
|
|
it("contains exactly the authored RPC cohort", () => {
|
|
expect(Object.keys(workflowRuntimeContract.operations)).toEqual([
|
|
"workflow.artifacts.inspect",
|
|
"workflow.artifacts.list",
|
|
"workflow.capabilities.call",
|
|
"workflow.capabilities.inspect",
|
|
"workflow.capabilities.list",
|
|
"workflow.deployments.inspect",
|
|
"workflow.deployments.list",
|
|
"workflow.deployments.validate",
|
|
"workflow.draft_workspaces.add_step_from_capability",
|
|
"workflow.draft_workspaces.create_empty",
|
|
"workflow.draft_workspaces.create_from_capability",
|
|
"workflow.draft_workspaces.get",
|
|
"workflow.draft_workspaces.list",
|
|
"workflow.draft_workspaces.set_route",
|
|
"workflow.draft_workspaces.set_step_input_bindings",
|
|
"workflow.draft_workspaces.set_step_output_bindings",
|
|
"workflow.draft_workspaces.update_capability_step",
|
|
"workflow.draft_workspaces.validate",
|
|
"workflow.health",
|
|
"workflow.runs.inspect",
|
|
"workflow.runs.list",
|
|
"workflow.runs.resume",
|
|
"workflow.runs.start",
|
|
"workflow.runs.trace",
|
|
"workflow.sources.list",
|
|
]);
|
|
});
|
|
|
|
it("exposes the six draft authoring runtime schemas without generic replacement", () => {
|
|
expect(
|
|
runtimeSchemasFor("workflow.draft_workspaces.create_empty"),
|
|
).toBeDefined();
|
|
expect(
|
|
runtimeSchemasFor("workflow.draft_workspaces.create_from_capability"),
|
|
).toBeDefined();
|
|
expect(
|
|
runtimeSchemasFor("workflow.draft_workspaces.add_step_from_capability"),
|
|
).toBeDefined();
|
|
expect(
|
|
runtimeSchemasFor("workflow.draft_workspaces.update_capability_step"),
|
|
).toBeDefined();
|
|
expect(runtimeSchemasFor("workflow.draft_workspaces.set_route")).toBeDefined();
|
|
expect(runtimeSchemasFor("workflow.draft_workspaces.validate")).toBeDefined();
|
|
expect(
|
|
Object.hasOwn(workflowRuntimeContract.operations, "workflow.draft_workspaces.replace_document"),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("accepts every JSON literal category in focused input bindings", () => {
|
|
const schemas = runtimeSchemasFor(
|
|
"workflow.draft_workspaces.set_step_input_bindings",
|
|
);
|
|
const values = [
|
|
"markdown",
|
|
3.14,
|
|
true,
|
|
null,
|
|
["markdown", false],
|
|
{ format: "markdown", options: { strict: true } },
|
|
];
|
|
|
|
for (const value of values) {
|
|
expect(
|
|
accepts(schemas.payload, {
|
|
workspace_id: "console.demo",
|
|
revision: 3,
|
|
step_id: "render",
|
|
bindings: [{ target: "literal", value }],
|
|
}),
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("rejects an input expression over the canonical 1024-node budget", () => {
|
|
const schemas = runtimeSchemasFor(
|
|
"workflow.draft_workspaces.set_step_input_bindings",
|
|
);
|
|
const expression = {
|
|
kind: "array",
|
|
items: Array.from({ length: 1025 }, () => ({
|
|
kind: "literal",
|
|
value: "child",
|
|
})),
|
|
};
|
|
|
|
expect(
|
|
accepts(schemas.payload, {
|
|
workspace_id: "console.demo",
|
|
revision: 3,
|
|
step_id: "render",
|
|
bindings: [{ target: "request", expression }],
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("accepts a large ordinary JSON value shaped like a literal in a simple binding", () => {
|
|
const schemas = runtimeSchemasFor(
|
|
"workflow.draft_workspaces.set_step_input_bindings",
|
|
);
|
|
const ordinaryValue = {
|
|
kind: "literal",
|
|
value: Array.from({ length: 1025 }, () => ({})),
|
|
};
|
|
|
|
expect(
|
|
accepts(schemas.payload, {
|
|
workspace_id: "console.demo",
|
|
revision: 3,
|
|
step_id: "render",
|
|
bindings: [{ target: "request", value: ordinaryValue }],
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("accepts a large ordinary JSON value shaped like a literal as workflow input", () => {
|
|
const schemas = runtimeSchemasFor("workflow.runs.start");
|
|
const ordinaryValue = {
|
|
kind: "literal",
|
|
value: Array.from({ length: 1025 }, () => ({})),
|
|
};
|
|
|
|
expect(
|
|
accepts(schemas.payload, {
|
|
deployment_id: "report.default",
|
|
workflow_input: { request: ordinaryValue },
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("counts nested literal array and object containers in the input budget", () => {
|
|
const schemas = runtimeSchemasFor(
|
|
"workflow.draft_workspaces.set_step_input_bindings",
|
|
);
|
|
const expression = {
|
|
kind: "literal",
|
|
value: Array.from({ length: 1023 }, () => ({})),
|
|
};
|
|
|
|
expect(
|
|
accepts(schemas.payload, {
|
|
workspace_id: "console.demo",
|
|
revision: 3,
|
|
step_id: "render",
|
|
bindings: [{ target: "request", expression }],
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("returns typed payload and result schemas for a generated operation", () => {
|
|
const schemas = runtimeSchemasFor("workflow.health");
|
|
const payload: WorkflowOperationParams<"workflow.health"> =
|
|
Schema.decodeUnknownSync(schemas.payload)({});
|
|
const result: WorkflowOperationResult<"workflow.health"> =
|
|
Schema.decodeUnknownSync(schemas.success)({
|
|
status: "ok",
|
|
store_root: "C:/store",
|
|
});
|
|
|
|
expect(payload).toEqual({});
|
|
expect(result.status).toBe("ok");
|
|
expect(accepts(schemas.success, { status: "bad", store_root: "C:/store" })).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("keeps generated payload constraints at runtime", () => {
|
|
const schemas = runtimeSchemasFor("workflow.sources.list");
|
|
|
|
expect(accepts(schemas.payload, { limit: 100 })).toBe(true);
|
|
expect(accepts(schemas.payload, { limit: 101 })).toBe(false);
|
|
expect(accepts(schemas.payload, { extra: true })).toBe(false);
|
|
});
|
|
|
|
it("rejects adversarial nesting before recursive JSON decoding", () => {
|
|
const schemas = runtimeSchemasFor("workflow.artifacts.inspect");
|
|
let plan: unknown = "leaf";
|
|
for (let depth = 0; depth < 70; depth += 1) plan = { nested: plan };
|
|
|
|
expect(
|
|
accepts(schemas.success, {
|
|
id: "report",
|
|
version: 1,
|
|
title: "Report",
|
|
kind: "workflow",
|
|
description: null,
|
|
outcomes: [],
|
|
input_schema: {},
|
|
output_schema: {},
|
|
plan,
|
|
required_capabilities: [],
|
|
workflow_dependencies: {},
|
|
created_from_catalog_version: null,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("accepts shared acyclic runtime values", () => {
|
|
const schemas = runtimeSchemasFor("workflow.artifacts.inspect");
|
|
const sharedSchema = {};
|
|
const result = {
|
|
id: "report",
|
|
version: 1,
|
|
title: "Report",
|
|
kind: "workflow",
|
|
description: null,
|
|
outcomes: [],
|
|
input_schema: sharedSchema,
|
|
output_schema: sharedSchema,
|
|
plan: {},
|
|
required_capabilities: [],
|
|
workflow_dependencies: {},
|
|
created_from_catalog_version: null,
|
|
};
|
|
|
|
expect(accepts(schemas.success, result)).toBe(true);
|
|
});
|
|
|
|
it("rejects cyclic runtime values", () => {
|
|
const schemas = runtimeSchemasFor("workflow.artifacts.inspect");
|
|
const cyclic: Record<string, unknown> = {};
|
|
cyclic.self = cyclic;
|
|
|
|
const result = {
|
|
id: "report",
|
|
version: 1,
|
|
title: "Report",
|
|
kind: "workflow",
|
|
description: null,
|
|
outcomes: [],
|
|
input_schema: {},
|
|
output_schema: {},
|
|
plan: cyclic,
|
|
required_capabilities: [],
|
|
workflow_dependencies: {},
|
|
created_from_catalog_version: null,
|
|
};
|
|
|
|
expect(accepts(schemas.success, result)).toBe(false);
|
|
});
|
|
|
|
it("accepts the maximum permitted container depth", () => {
|
|
const schemas = runtimeSchemasFor("workflow.artifacts.inspect");
|
|
let plan: unknown = "leaf";
|
|
for (let depth = 0; depth < 63; depth += 1) plan = { nested: plan };
|
|
|
|
expect(
|
|
accepts(schemas.success, {
|
|
id: "report",
|
|
version: 1,
|
|
title: "Report",
|
|
kind: "workflow",
|
|
description: null,
|
|
outcomes: [],
|
|
input_schema: {},
|
|
output_schema: {},
|
|
plan,
|
|
required_capabilities: [],
|
|
workflow_dependencies: {},
|
|
created_from_catalog_version: null,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("requires the canonical persisted interrupt contract", () => {
|
|
const schemas = runtimeSchemasFor("workflow.runs.inspect");
|
|
const baseRun = {
|
|
run_id: "run_1",
|
|
deployment_id: "report.default",
|
|
artifact_id: "report",
|
|
artifact_version: 1,
|
|
status: "interrupted",
|
|
resume_readiness: "ready",
|
|
outcome: null,
|
|
error: null,
|
|
output: null,
|
|
diagnostics: [],
|
|
trace_count: 1,
|
|
next_actions: {
|
|
can_continue: true,
|
|
can_save_now: null,
|
|
recommended_next_tool: "wf.workflow.resume_run",
|
|
reason: "run is interrupted",
|
|
patch_examples: [],
|
|
warnings: [],
|
|
},
|
|
};
|
|
const completeInterrupt = {
|
|
id: "interrupt_1",
|
|
frame_id: "frame_1",
|
|
node_id: "review_issues",
|
|
kind: "issue_review",
|
|
payload: {},
|
|
resumable: true,
|
|
route: null,
|
|
outcomes: ["submitted", "cancelled"],
|
|
request_schema: { type: "object" },
|
|
resume_schema: { type: "object" },
|
|
typed: true,
|
|
};
|
|
|
|
expect(accepts(schemas.success, { ...baseRun, interrupt: completeInterrupt })).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
accepts(schemas.success, {
|
|
...baseRun,
|
|
interrupt: { kind: "issue_review", payload: {}, outcomes: [] },
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
accepts(schemas.success, {
|
|
...baseRun,
|
|
run_id: null,
|
|
status: "failed",
|
|
resume_readiness: null,
|
|
interrupt: null,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("keeps canonical run input defaults optional", () => {
|
|
const start = runtimeSchemasFor("workflow.runs.start");
|
|
const resume = runtimeSchemasFor("workflow.runs.resume");
|
|
const trace = runtimeSchemasFor("workflow.runs.trace");
|
|
|
|
expect(accepts(start.payload, { deployment_id: "report.default" })).toBe(true);
|
|
expect(accepts(resume.payload, { run_id: "run_1" })).toBe(true);
|
|
expect(accepts(trace.payload, { run_id: "run_1", trace_range: {} })).toBe(true);
|
|
expect(
|
|
accepts(trace.payload, {
|
|
run_id: "run_1",
|
|
trace_range: { start: 0, limit: 101 },
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("requires the full run envelope and canonical trace frame identifiers", () => {
|
|
const schemas = runtimeSchemasFor("workflow.runs.trace");
|
|
const trace = [
|
|
{
|
|
frame_id: "frame_1",
|
|
node_id: "review_issues",
|
|
step_type: "interrupt",
|
|
resolved_input: {},
|
|
outcome: "submitted",
|
|
next_node_id: "create_issues",
|
|
output: {},
|
|
state_changes: {},
|
|
},
|
|
];
|
|
const result = {
|
|
run_id: "run_1",
|
|
deployment_id: "report.default",
|
|
artifact_id: "report",
|
|
artifact_version: 1,
|
|
status: "completed",
|
|
resume_readiness: "not_applicable",
|
|
interrupt: null,
|
|
outcome: "completed",
|
|
error: null,
|
|
output: {},
|
|
diagnostics: [],
|
|
trace_count: 1,
|
|
next_actions: {
|
|
can_continue: false,
|
|
can_save_now: null,
|
|
recommended_next_tool: null,
|
|
reason: "run completed",
|
|
patch_examples: [],
|
|
warnings: [],
|
|
},
|
|
trace,
|
|
trace_start: 0,
|
|
trace_limit: 50,
|
|
trace_truncated: false,
|
|
};
|
|
|
|
expect(accepts(schemas.success, result)).toBe(true);
|
|
expect(
|
|
accepts(schemas.success, {
|
|
run_id: "run_1",
|
|
status: "completed",
|
|
trace,
|
|
trace_start: 0,
|
|
trace_limit: 50,
|
|
trace_truncated: false,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
});
|