aps-agent/tests/golden/test_contract_generation.py

103 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 契约生成自动化黄金测试(plan.md §12 / 矩阵 117 行)
# ============================================================
# 生成器以 schema 为权威源;手写 types.ts 关键面(字段/必填/枚举)
# 必须与 schema 一致(兼容豁免 interfaceVersion 等可选字段)。
from __future__ import annotations
import json
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SCHEMAS = ROOT / "shared" / "schemas"
TYPESCRIPT_TYPES = ROOT / "apps" / "web" / "src" / "api" / "types.ts"
GENERATOR = ROOT / "scripts" / "generate_contract_types.py"
def _schema(name: str) -> dict:
return json.loads((SCHEMAS / f"{name}.schema.json").read_text(encoding="utf-8"))
def _run_generator(name: str | None = None) -> str:
cmd = [sys.executable, str(GENERATOR)]
if name:
cmd += ["--name", name]
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", timeout=60)
assert result.returncode == 0, result.stderr
return result.stdout
def _typescript_source() -> str:
return TYPESCRIPT_TYPES.read_text(encoding="utf-8")
def test_generator_runs_for_all_schemas():
"""生成器可对全部 7 个 schema 无异常执行,且输出含 interface。"""
out = _run_generator()
assert "export interface" in out
for path in SCHEMAS.glob("*.schema.json"):
schema = json.loads(path.read_text(encoding="utf-8"))
title = str(schema.get("title") or path.stem)
assert f"export interface {title}" in out, f"{title} 缺失"
def test_generator_emits_required_fields():
"""生成 interface 的必填字段与 schema required 一致。"""
schema = _schema("viewport_command")
out = _run_generator("viewport_command")
required = set(schema["required"])
for key in required:
assert re.search(rf"^ {key}: ", out, re.M), f"必填字段 {key} 未生成"
# 非必填字段带 ?
for key in set(schema["properties"]) - required:
assert re.search(rf"^ {key}\?: ", out, re.M), f"可选字段 {key} 未生成"
def test_generator_emits_enum_unions_matching_schema():
"""枚举 union 与 schema 枚举一致。"""
schema = _schema("ui_block")
out = _run_generator("ui_block")
enum = set(schema["properties"]["type"]["enum"])
# 生成 union 形式:'a' | 'b' ...
for value in enum:
assert f"'{value}'" in out, f"枚举 {value} 缺失"
# 手写 types.ts 的 UIBlockType 必须含 schema 全部枚举
ts = _typescript_source()
match = re.search(r"export type UIBlockType\s*=\s*(.*?);", ts, re.DOTALL)
assert match is not None
ts_enum = set(re.findall(r"'([^']+)'", match.group(1)))
assert enum <= ts_enum, "手写 UIBlockType 与 schema 枚举漂移"
def test_handwritten_types_cover_schema_required_fields():
"""手写 types.ts 关键面 ⊆ schema:每个 schema 必填字段出现在对应 TS 接口。"""
ts = _typescript_source()
for path in SCHEMAS.glob("*.schema.json"):
schema = json.loads(path.read_text(encoding="utf-8"))
title = str(schema.get("title") or path.stem)
# 手写接口名与 schema title 相同的契约
interface_match = re.search(
rf"export interface {re.escape(title)}\s*\{{(.*?)\n\}}", ts, re.DOTALL)
if not interface_match:
continue # 该契约 TS 端以其他形式承载(如 PlanNode 在 plan 端),跳过
body = interface_match.group(1)
for key in schema.get("required") or []:
assert re.search(rf"^\s*{re.escape(key)}\??:", body, re.M), \
f"{title} 手写缺必填字段 {key}"
def test_generator_maps_null_type_correctly():
"""nullable 类型:"null" 显式映射为 TS null(如 plan_node.parentId)。"""
out = _run_generator("plan_node")
assert "parentId: string | null;" in out
assert "| unknown;" not in out # null 不再坍缩为 unknown
def test_generator_deterministic():
"""生成器确定性:同输入两次输出一致。"""
a = _run_generator()
b = _run_generator()
assert a == b