mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
* refactor: carve out extraction panel * refactor: create spec versions for node types * refactor: create a GenericNode and remove custom nodes * feat: add python and typescript sdk * add dograh sdk * fix: fetch draft workflow definition over published one * fix: fix routes of SDKs to use code gen * chore: remove doclink dependency to reduce image size * chore: format files * chore: bump pipecat * feat: let mcp fetch archived workflows on demand * chore: fix tests * feat: add sdk documentation * chore: change banner and add badge
99 lines
3.2 KiB
Python
99 lines
3.2 KiB
Python
"""Drift guard: committed SDK typed files must match what codegen
|
|
produces from the current `node_specs/` registry.
|
|
|
|
Fails loudly if a spec was edited without running
|
|
`./scripts/generate_sdk.sh`. CI also runs the full script and asserts
|
|
an empty `git diff` as the authoritative cross-language check; this
|
|
test is the fast local feedback loop inside pytest.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# Ensure the Python SDK package is importable without requiring a
|
|
# `pip install -e sdk/python`. The codegen lives there because it ships
|
|
# with the SDK wheel, but tests need to reach it directly.
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
SDK_PY_SRC = REPO_ROOT / "sdk" / "python" / "src"
|
|
if str(SDK_PY_SRC) not in sys.path:
|
|
sys.path.insert(0, str(SDK_PY_SRC))
|
|
|
|
from dograh_sdk.codegen import generate_all # noqa: E402
|
|
|
|
from api.services.workflow.node_specs import SPEC_VERSION, all_specs # noqa: E402
|
|
|
|
PY_OUT = REPO_ROOT / "sdk" / "python" / "src" / "dograh_sdk" / "typed"
|
|
TS_OUT = REPO_ROOT / "sdk" / "typescript" / "src" / "typed"
|
|
TS_CODEGEN = REPO_ROOT / "sdk" / "typescript" / "scripts" / "codegen.mts"
|
|
REGEN_HINT = "Run ./scripts/generate_sdk.sh to regenerate."
|
|
|
|
|
|
def _specs_payload() -> dict:
|
|
return {
|
|
"spec_version": SPEC_VERSION,
|
|
"node_types": [s.model_dump(mode="json") for s in all_specs()],
|
|
}
|
|
|
|
|
|
def _compare_trees(expected_dir: Path, actual_dir: Path, *, skip: set[str]) -> None:
|
|
def tree(d: Path) -> dict[str, str]:
|
|
return {
|
|
p.name: p.read_text()
|
|
for p in d.iterdir()
|
|
if p.is_file() and p.name not in skip
|
|
}
|
|
|
|
expected = tree(expected_dir)
|
|
actual = tree(actual_dir)
|
|
|
|
if expected.keys() != actual.keys():
|
|
pytest.fail(
|
|
f"File set differs in {expected_dir.name}/.\n"
|
|
f" committed: {sorted(expected)}\n"
|
|
f" generated: {sorted(actual)}\n"
|
|
f"{REGEN_HINT}"
|
|
)
|
|
for name in sorted(expected):
|
|
if expected[name] != actual[name]:
|
|
pytest.fail(
|
|
f"{expected_dir.name}/{name} is out of sync with node_specs. "
|
|
f"{REGEN_HINT}"
|
|
)
|
|
|
|
|
|
def test_python_sdk_typed_in_sync(tmp_path: Path) -> None:
|
|
specs = _specs_payload()["node_types"]
|
|
generate_all(specs, tmp_path)
|
|
# _base.py is hand-written and lives alongside generated files.
|
|
_compare_trees(PY_OUT, tmp_path, skip={"_base.py", "__pycache__"})
|
|
|
|
|
|
@pytest.mark.skipif(shutil.which("node") is None, reason="node binary not available")
|
|
def test_typescript_sdk_typed_in_sync(tmp_path: Path) -> None:
|
|
specs_file = tmp_path / "specs.json"
|
|
specs_file.write_text(json.dumps(_specs_payload()))
|
|
out = tmp_path / "ts_out"
|
|
|
|
result = subprocess.run(
|
|
[
|
|
"node",
|
|
str(TS_CODEGEN),
|
|
"--input",
|
|
str(specs_file),
|
|
"--out",
|
|
str(out),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert result.returncode == 0, (
|
|
f"TS codegen failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"
|
|
)
|
|
_compare_trees(TS_OUT, out, skip=set())
|