fix: close Task 4 contract graph review findings
This commit is contained in:
@@ -132,6 +132,31 @@ def _schema(value: object, path: str) -> JsonSchema:
|
|||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _is_named_schema_reference(value: object) -> bool:
|
||||||
|
return (
|
||||||
|
isinstance(value, Mapping)
|
||||||
|
and set(value) == {"$ref"}
|
||||||
|
and isinstance(value.get("$ref"), str)
|
||||||
|
and value["$ref"].startswith("#/components/schemas/")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_composed_named_result_schema(value: object) -> bool:
|
||||||
|
if not isinstance(value, Mapping):
|
||||||
|
return False
|
||||||
|
|
||||||
|
composition_keys = [key for key in ("allOf", "anyOf", "oneOf") if key in value]
|
||||||
|
if len(composition_keys) != 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
branches = value[composition_keys[0]]
|
||||||
|
return (
|
||||||
|
isinstance(branches, list)
|
||||||
|
and bool(branches)
|
||||||
|
and all(_is_named_schema_reference(branch) for branch in branches)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _error_component(value: object, path: str) -> JsonValue:
|
def _error_component(value: object, path: str) -> JsonValue:
|
||||||
"""Normalize an OpenRPC error object, whose ``data`` member is a schema."""
|
"""Normalize an OpenRPC error object, whose ``data`` member is a schema."""
|
||||||
if not isinstance(value, Mapping):
|
if not isinstance(value, Mapping):
|
||||||
@@ -269,22 +294,9 @@ def manifest_from_openrpc(document: Mapping[str, object]) -> ContractManifest:
|
|||||||
f"{result_path}.schema", "missing success result schema"
|
f"{result_path}.schema", "missing success result schema"
|
||||||
)
|
)
|
||||||
raw_result_schema = result["schema"]
|
raw_result_schema = result["schema"]
|
||||||
if not isinstance(raw_result_schema, Mapping):
|
if not _is_named_schema_reference(
|
||||||
raise ManifestError(
|
raw_result_schema
|
||||||
f"{result_path}.schema",
|
) and not _is_composed_named_result_schema(raw_result_schema):
|
||||||
"success result must be a schema object",
|
|
||||||
)
|
|
||||||
named_result = (
|
|
||||||
set(raw_result_schema) == {"$ref"}
|
|
||||||
and isinstance(raw_result_schema.get("$ref"), str)
|
|
||||||
and raw_result_schema["$ref"].startswith("#/components/schemas/")
|
|
||||||
)
|
|
||||||
# This inspection RPC returns either its inventory or the standard
|
|
||||||
# revision-conflict workspace payload, so its OpenRPC result is a union.
|
|
||||||
if (
|
|
||||||
not named_result
|
|
||||||
and method_name != "workflow.draft_workspaces.inspect_authoring_contract"
|
|
||||||
):
|
|
||||||
raise ManifestError(
|
raise ManifestError(
|
||||||
f"{result_path}.schema",
|
f"{result_path}.schema",
|
||||||
"success result must reference a named schema component",
|
"success result must reference a named schema component",
|
||||||
|
|||||||
@@ -240,6 +240,43 @@ def test_rejects_invalid_result_schema_shape() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_accepts_composed_result_with_named_component_branches() -> None:
|
||||||
|
document = synthetic_openrpc_document()
|
||||||
|
document["methods"][1]["result"]["schema"] = {
|
||||||
|
"anyOf": [
|
||||||
|
{"$ref": "#/components/schemas/AlphaResult"},
|
||||||
|
{"$ref": "#/components/schemas/ZetaResult"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest = manifest_from_openrpc(document)
|
||||||
|
|
||||||
|
assert manifest["operations"][0]["result"] == {
|
||||||
|
"schema": {
|
||||||
|
"anyOf": [
|
||||||
|
{"$ref": "#/components/schemas/AlphaResult"},
|
||||||
|
{"$ref": "#/components/schemas/ZetaResult"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_rejects_composed_result_with_an_inline_branch() -> None:
|
||||||
|
document = synthetic_openrpc_document()
|
||||||
|
document["methods"][1]["result"]["schema"] = {
|
||||||
|
"oneOf": [
|
||||||
|
{"$ref": "#/components/schemas/AlphaResult"},
|
||||||
|
{"type": "object"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_manifest_error(
|
||||||
|
document,
|
||||||
|
"$.methods[1].result.schema",
|
||||||
|
"success result must reference a named schema component",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_reports_a_missing_success_result_schema() -> None:
|
def test_reports_a_missing_success_result_schema() -> None:
|
||||||
document = synthetic_openrpc_document()
|
document = synthetic_openrpc_document()
|
||||||
del document["methods"][1]["result"]["schema"]
|
del document["methods"][1]["result"]["schema"]
|
||||||
|
|||||||
@@ -221,11 +221,22 @@ const authoringPathOption = {
|
|||||||
uses: ["step_input"],
|
uses: ["step_input"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const conditionalRuntimeContextOption = {
|
||||||
|
...authoringPathOption,
|
||||||
|
path: "context.viewer_id",
|
||||||
|
label: "Viewer ID",
|
||||||
|
origin: "runtime_context",
|
||||||
|
required: false,
|
||||||
|
availability: "conditional",
|
||||||
|
reason: "available when the selected capability accepts runtime context",
|
||||||
|
uses: ["step_input"],
|
||||||
|
};
|
||||||
|
|
||||||
const authoringContractInventory = {
|
const authoringContractInventory = {
|
||||||
workspace_id: "console.demo",
|
workspace_id: "console.demo",
|
||||||
revision: 7,
|
revision: 7,
|
||||||
selected_step_id: "render",
|
selected_step_id: "render",
|
||||||
readable_sources: [authoringPathOption],
|
readable_sources: [authoringPathOption, conditionalRuntimeContextOption],
|
||||||
step_input_targets: [
|
step_input_targets: [
|
||||||
{
|
{
|
||||||
...authoringPathOption,
|
...authoringPathOption,
|
||||||
@@ -1113,6 +1124,16 @@ const parityReport = (): ParityReport => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("authored RPC and manifest schema parity", () => {
|
describe("authored RPC and manifest schema parity", () => {
|
||||||
|
it("covers conditional runtime context sources with a reason", () => {
|
||||||
|
expect(authoringContractInventory.readable_sources).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
origin: "runtime_context",
|
||||||
|
availability: "conditional",
|
||||||
|
reason: "available when the selected capability accepts runtime context",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects empty segments in authored structural binding paths", () => {
|
it("rejects empty segments in authored structural binding paths", () => {
|
||||||
const cases: ReadonlyArray<{
|
const cases: ReadonlyArray<{
|
||||||
readonly method: keyof typeof authoredRpcSchemas;
|
readonly method: keyof typeof authoredRpcSchemas;
|
||||||
|
|||||||
Reference in New Issue
Block a user