Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
package:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.13"]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: ${{ matrix.python-version }}
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d
with:
version: "0.12.3"
- name: Install package and test dependencies
run: uv pip install --system -e ".[dev]"
- name: Check package
run: |
ruff check src tests
Comment thread
wolfiesch marked this conversation as resolved.
Resolved
mypy src
pytest -q
uv build
80 changes: 51 additions & 29 deletions src/spreadsheet_guard/guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@
from pathlib import Path
from typing import Any, cast

EXPECTED_GUARD_CONTRACT_VERSION = "wolfxl-guard-contract-v1"


class GuardError(RuntimeError):
"""Base error for the public Guard surface."""


class GuardUnavailableError(GuardError):
"""Raised when the WolfXL Guard runtime is not installed."""
"""Raised when a compatible WolfXL Guard runtime is not installed."""


class GuardExecutionError(GuardError):
"""Raised when WolfXL does not produce a valid Guard report."""
"""Raised when WolfXL does not produce a valid Guard result."""


@dataclass(frozen=True)
Expand All @@ -30,22 +32,31 @@ class GuardOutcome:
report: Mapping[str, Any]


GuardFunction = Callable[..., dict[str, Any]]
GuardFunction = Callable[..., Mapping[str, object]]


def _load_wolfxl_guard() -> GuardFunction:
"""Resolve only WolfXL's versioned public contract, never an internal path."""

try:
module = import_module("wolfxl.operations")
module = import_module("wolfxl.guard_contract")
except ImportError as exc:
raise GuardUnavailableError(
"WolfXL Guard requires the WolfXL Commercial runtime. "
"WolfXL Community does not include the operations SDK. "
"Request evaluation access at https://wolfxl.com"
"Spreadsheet Guard requires a WolfXL runtime exposing "
f"{EXPECTED_GUARD_CONTRACT_VERSION}"
) from exc
run_guard = getattr(module, "run_guard", None)
if not callable(run_guard):
raise GuardUnavailableError("the installed WolfXL runtime does not expose Guard")
return cast(GuardFunction, run_guard)
actual_version = getattr(module, "GUARD_CONTRACT_VERSION", None)
if actual_version != EXPECTED_GUARD_CONTRACT_VERSION:
raise GuardUnavailableError(
"installed WolfXL Guard contract is incompatible; "
f"expected {EXPECTED_GUARD_CONTRACT_VERSION}, got {actual_version!r}"
)
runner = getattr(module, "run_guard_contract", None)
if not callable(runner):
raise GuardUnavailableError(
"installed WolfXL runtime does not expose run_guard_contract"
)
return cast(GuardFunction, runner)


def guard_workbooks(
Expand All @@ -55,36 +66,47 @@ def guard_workbooks(
*,
policy: Mapping[str, Any] | None = None,
) -> GuardOutcome:
"""Compare two workbooks without modifying either input.
"""Compare two workbooks through WolfXL's pinned Guard contract.

WolfXL owns workbook inspection and policy semantics. This package owns only
the stable public invocation and fail-closed result contract.
This package owns the stable command/package identity. WolfXL owns policy,
inspection, report validation, and report-artifact binding.
"""

runner = _load_wolfxl_guard()
arguments: dict[str, Any] = {
"before": before,
"after": after,
"output": output,
}
if policy is not None:
arguments["policy"] = policy

try:
report = runner(**arguments)
result = runner(before=before, after=after, output=output, policy=policy)
except Exception as exc:
if isinstance(exc, GuardError):
raise
raise GuardExecutionError(
f"WolfXL Guard failed ({type(exc).__name__}): {exc}"
) from exc

if not isinstance(report, Mapping):
raise GuardExecutionError("WolfXL Guard returned a non-object report")
status = report.get("status")
if status not in {"passed", "failed", "unassessed"}:
if not isinstance(result, Mapping):
raise GuardExecutionError("WolfXL Guard contract returned a non-object result")
if result.get("contract_version") != EXPECTED_GUARD_CONTRACT_VERSION:
raise GuardExecutionError("WolfXL Guard result contract version drifted")
status = result.get("status")
if not isinstance(status, str) or status not in {
"passed",
"failed",
"unassessed",
}:
raise GuardExecutionError(f"WolfXL Guard returned invalid status {status!r}")
if not output.is_file():
report = result.get("report")
if not isinstance(report, Mapping) or report.get("status") != status:
raise GuardExecutionError("WolfXL Guard returned an inconsistent report")
output_path_value = result.get("output_path")
if not isinstance(output_path_value, str):
raise GuardExecutionError("WolfXL Guard did not return its report path")
output_path = Path(output_path_value).resolve(strict=False)
if output_path != output.expanduser().resolve(strict=False):
raise GuardExecutionError("WolfXL Guard returned an unbound report path")
if not output_path.is_file():
raise GuardExecutionError("WolfXL Guard did not write the requested report")

return GuardOutcome(status=status, output_path=output, report=dict(report))
return GuardOutcome(
status=str(status),
output_path=output_path,
report=dict(report),
)
95 changes: 79 additions & 16 deletions tests/test_guard_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,111 @@

import json
from pathlib import Path
from types import ModuleType
from typing import Any

import pytest

import spreadsheet_guard.cli as cli_module
import spreadsheet_guard.guard as guard_module
from spreadsheet_guard.guard import GuardExecutionError, GuardOutcome, guard_workbooks
from spreadsheet_guard.guard import (
GuardExecutionError,
GuardOutcome,
GuardUnavailableError,
guard_workbooks,
)


def test_guard_workbooks_delegates_read_only_comparison(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
def test_guard_workbooks_delegates_to_versioned_contract(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
before = tmp_path / "before.xlsx"
after = tmp_path / "after.xlsx"
output = tmp_path / "guard.json"
before.write_bytes(b"before")
after.write_bytes(b"after")
received: dict[str, Any] = {}

def fake_guard(**kwargs: Any) -> dict[str, Any]:
def fake_guard(**kwargs: Any) -> dict[str, object]:
received.update(kwargs)
report = {"schema_version": 1, "status": "passed"}
kwargs["output"].write_text(json.dumps(report))
return report
return {
"contract_version": "wolfxl-guard-contract-v1",
"status": "passed",
"output_path": str(kwargs["output"].resolve()),
"report": report,
}

monkeypatch.setattr(guard_module, "_load_wolfxl_guard", lambda: fake_guard)

outcome = guard_workbooks(before, after, output)

assert outcome.status == "passed"
assert outcome.output_path == output
assert received == {"before": before, "after": after, "output": output}
assert outcome.output_path == output.resolve()
assert received == {
"before": before,
"after": after,
"output": output,
"policy": None,
}
assert before.read_bytes() == b"before"
assert after.read_bytes() == b"after"


def test_guard_workbooks_fails_closed_on_invalid_report(
tmp_path: Path,
@pytest.mark.parametrize(
("version", "has_runner"),
[
(None, True),
("wolfxl-guard-contract-v0", True),
("wolfxl-guard-contract-v1", False),
],
)
def test_loader_fails_closed_on_contract_drift(
version: object,
has_runner: bool,
monkeypatch: pytest.MonkeyPatch,
) -> None:
output = tmp_path / "guard.json"
module = ModuleType("wolfxl.guard_contract")
if version is not None:
module.GUARD_CONTRACT_VERSION = version
if has_runner:
module.run_guard_contract = lambda **kwargs: kwargs
monkeypatch.setattr(guard_module, "import_module", lambda name: module)

def fake_guard(**kwargs: Any) -> dict[str, Any]:
kwargs["output"].write_text("{}")
return {"status": "unknown"}
with pytest.raises(GuardUnavailableError):
guard_module._load_wolfxl_guard()

monkeypatch.setattr(guard_module, "_load_wolfxl_guard", lambda: fake_guard)

@pytest.mark.parametrize("invalid_status", ["unknown", [], {}])
def test_guard_workbooks_fails_closed_on_invalid_contract_result(
invalid_status: object,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
output = tmp_path / "guard.json"
output.write_text("{}")

monkeypatch.setattr(
guard_module,
"_load_wolfxl_guard",
lambda: lambda **kwargs: {
"contract_version": "wolfxl-guard-contract-v1",
"status": invalid_status,
"output_path": str(output),
"report": {},
},
)

with pytest.raises(GuardExecutionError, match="invalid status"):
guard_workbooks(tmp_path / "before.xlsx", tmp_path / "after.xlsx", output)


@pytest.mark.parametrize(("status", "expected_code"), [("passed", 0), ("failed", 1), ("unassessed", 1)])
@pytest.mark.parametrize(
("status", "expected_code"),
[("passed", 0), ("failed", 1), ("unassessed", 1)],
)
def test_guard_cli_exit_code_tracks_guard_status(
status: str,
expected_code: int,
Expand All @@ -73,12 +127,21 @@ def fake_guard_workbooks(
assert after == tmp_path / "after.xlsx"
assert requested_output == output
assert policy is None
return GuardOutcome(status=status, output_path=output, report={"status": status})
return GuardOutcome(
status=status,
output_path=output,
report={"status": status},
)

monkeypatch.setattr(cli_module, "guard_workbooks", fake_guard_workbooks)

code = cli_module.main(
[str(tmp_path / "before.xlsx"), str(tmp_path / "after.xlsx"), "--output", str(output)]
[
str(tmp_path / "before.xlsx"),
str(tmp_path / "after.xlsx"),
"--output",
str(output),
]
)

assert code == expected_code
Expand Down
Loading