Files
Stirling-PDF/engine/tests/test_tool_io_compat.py
James BruntonandAnthony Stirling cd199c8659 Define tool inputs & outputs in a structured way (#7204)
# Description of Changes
Change tool APIs to use structured definitions for input/output/type
info because we need that info to be able to validate whether policies
can actually successfully work based on whether one tool accepts the
output of another. There were various bugs in the previous string
definitions because of either misspellings or just incorrect
definitions, so I've gone through and fixed all that I can find.

<img width="729" height="271" alt="image"
src="https://github.com/user-attachments/assets/08357e96-6fbb-4b9c-ba4d-8995420c7b86"
/>

<img width="749" height="264" alt="image"
src="https://github.com/user-attachments/assets/76f46284-1866-4b64-b1ed-2480e01866e9"
/>

<img width="402" height="636" alt="image"
src="https://github.com/user-attachments/assets/8f7a36ca-2845-4f14-a2df-ec9c772e66f6"
/>

<img width="393" height="317" alt="image"
src="https://github.com/user-attachments/assets/46d8b891-9820-4ce3-8109-a8b782277037"
/>

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-08-03 14:13:43 +00:00

60 lines
2.2 KiB
Python

"""The shared cases in ``testing/tool-io-cases.json``, which all three implementations run."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import pytest
from stirling.models.tool_io import ToolFormat, ToolIOSpec
from stirling.models.tool_models import ToolEndpoint
from stirling.services.tool_io_compat import (
ToolChainStep,
ToolDiagnostic,
validate_tool_chain,
)
def _cases_file() -> Path:
"""Shared with the backend and frontend, so it lives at the repo root."""
for parent in Path(__file__).resolve().parents:
candidate = parent / "testing" / "tool-io-cases.json"
if candidate.exists():
return candidate
raise RuntimeError("testing/tool-io-cases.json not found above the test directory")
_DATA: dict[str, Any] = json.loads(_cases_file().read_text())
_SPECS: dict[str, ToolIOSpec] = {name: ToolIOSpec.model_validate(spec) for name, spec in _DATA["specs"].items()}
_ENDPOINTS: list[ToolEndpoint] = list(ToolEndpoint)
def _summarise(diagnostics: list[ToolDiagnostic]) -> list[str]:
"""Messages are free text, so compare only the contractual parts."""
return [f"{d.step_index}:{d.severity}:{d.code}" for d in diagnostics]
@pytest.mark.parametrize("case", _DATA["cases"], ids=lambda c: c["name"])
def test_shared_cases(case: dict[str, Any]) -> None:
table: dict[ToolEndpoint, ToolIOSpec] = {}
steps: list[ToolChainStep] = []
for index, step in enumerate(case["steps"]):
# Distinct endpoints so the same spec can appear twice in a chain. Which ones is
# irrelevant: the case supplies its own table, these are just keys.
operation = _ENDPOINTS[index]
if step.get("spec") is not None:
table[operation] = _SPECS[step["spec"]]
steps.append(ToolChainStep(operation=operation, parameters=step.get("parameters") or {}))
source = case.get("sourceFormat")
actual = validate_tool_chain(
steps,
source_format=ToolFormat(source) if source else None,
tool_io=table,
)
expected = [f"{e['stepIndex']}:{e['severity']}:{e['code']}" for e in case["expected"]]
assert _summarise(actual) == expected, [d.message for d in actual]