personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_schema_strict_portability.py
4.7 kB 141 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Offline CI gate for strict structured-output schema portability.""" 5 6from __future__ import annotations 7 8import json 9from pathlib import Path 10from typing import Any 11 12import pytest 13 14from solstone.apps.timeline.rollup import build_rollup_schema 15from solstone.think.schema_prep import ( 16 prepare_provider_schema, 17 unsupported_keyword_hits, 18) 19from solstone.think.talent import hydrate_runtime_enums 20 21REPO_ROOT = Path(__file__).resolve().parents[1] 22STRICT_PROVIDERS = ("openai", "anthropic", "google") 23 24 25def _discover_schemas() -> tuple[tuple[str, dict[str, Any]], ...]: 26 discovered: list[tuple[str, dict[str, Any]]] = [] 27 for path in sorted((REPO_ROOT / "solstone").glob("**/*.schema.json")): 28 schema_id = path.relative_to(REPO_ROOT).as_posix() 29 schema = json.loads(path.read_text(encoding="utf-8")) 30 if isinstance(schema.get("x-journal-contract"), dict): 31 continue 32 discovered.append((schema_id, schema)) 33 discovered.append(("build_rollup_schema(3)", build_rollup_schema(3))) 34 return tuple(discovered) 35 36 37SCHEMAS = _discover_schemas() 38 39 40def violations(schema: dict[str, Any]) -> list[str]: 41 found: list[str] = [] 42 43 root_is_object = schema.get("type") == "object" or ( 44 "properties" in schema and "type" not in schema 45 ) 46 if not root_is_object: 47 found.append("$: root schema must be an object") 48 49 def walk(node: Any, path: str) -> None: 50 if isinstance(node, dict): 51 for key in node: 52 if key == "oneOf": 53 found.append(f"{path}: banned key 'oneOf'") 54 55 if node.get("type") == "object" or "properties" in node: 56 if node.get("additionalProperties") is not False: 57 found.append(f"{path}: object missing additionalProperties:false") 58 properties = node.get("properties") or {} 59 required = node.get("required") or [] 60 missing = sorted(set(properties) - set(required)) 61 if missing: 62 found.append(f"{path}: properties not required {missing!r}") 63 64 for key, value in node.items(): 65 walk(value, f"{path}/{key}") 66 elif isinstance(node, list): 67 for index, value in enumerate(node): 68 walk(value, f"{path}[{index}]") 69 70 walk(schema, "$") 71 return found 72 73 74@pytest.mark.parametrize( 75 ("schema_id", "schema"), 76 [pytest.param(schema_id, schema, id=schema_id) for schema_id, schema in SCHEMAS], 77) 78def test_all_discovered_schemas_are_strict_portable( 79 schema_id: str, schema: dict[str, Any] 80) -> None: 81 schema_violations = violations(schema) 82 assert schema_violations == [], f"{schema_id}: {schema_violations}" 83 84 85@pytest.mark.parametrize("provider", STRICT_PROVIDERS) 86@pytest.mark.parametrize( 87 ("schema_id", "schema"), 88 [pytest.param(schema_id, schema, id=schema_id) for schema_id, schema in SCHEMAS], 89) 90def test_prepared_schemas_have_no_provider_unsupported_keywords( 91 schema_id: str, schema: dict[str, Any], provider: str 92) -> None: 93 prepared = prepare_provider_schema(schema, provider) 94 95 assert prepared is not None 96 assert unsupported_keyword_hits(prepared, provider) == [], schema_id 97 98 99@pytest.mark.parametrize( 100 "schema_path", 101 [ 102 Path("solstone/talent/schedule.schema.json"), 103 Path("solstone/talent/sense.schema.json"), 104 ], 105) 106def test_zero_facet_runtime_hydration_of_shipped_schemas_has_no_banned_keys( 107 schema_path: Path, monkeypatch 108) -> None: 109 monkeypatch.setattr("solstone.think.talent._valid_runtime_facets", lambda: []) 110 schema = json.loads((REPO_ROOT / schema_path).read_text(encoding="utf-8")) 111 112 hydrated = hydrate_runtime_enums(schema) 113 114 for provider in STRICT_PROVIDERS: 115 prepared = prepare_provider_schema(hydrated, provider) 116 assert prepared is not None 117 assert unsupported_keyword_hits(prepared, provider) == [] 118 119 120@pytest.mark.parametrize( 121 "schema", 122 [ 123 { 124 "type": "object", 125 "properties": { 126 "a": {"type": "array"}, 127 "b": {"type": "string"}, 128 "c": {"oneOf": [{"type": "string"}, {"type": "integer"}]}, 129 }, 130 "required": ["a"], 131 } 132 ], 133) 134def test_strict_portability_guard_rejects_bad_schema(schema: dict[str, Any]) -> None: 135 schema_violations = violations(schema) 136 137 assert any( 138 "object missing additionalProperties:false" in v for v in schema_violations 139 ) 140 assert any("properties not required" in v for v in schema_violations) 141 assert any("banned key 'oneOf'" in v for v in schema_violations)