CmdForge/tests/test_integrity.py

122 lines
4.1 KiB
Python

"""Tests for M9.4 transitive integrity verification."""
from unittest.mock import patch
from cmdforge.integrity import (
IntegrityNode,
IntegrityChain,
compute_tool_hash,
build_integrity_chain,
verify_integrity,
)
from cmdforge.tool import Tool, PromptStep, ToolStep
class TestComputeToolHash:
def test_same_tool_same_hash(self):
tool = Tool(name="test", version="1.0.0")
assert compute_tool_hash(tool) == compute_tool_hash(tool)
def test_different_tools_different_hash(self):
a = Tool(name="a", version="1.0.0")
b = Tool(name="b", version="1.0.0")
assert compute_tool_hash(a) != compute_tool_hash(b)
def test_version_change_changes_hash(self):
v1 = Tool(name="test", version="1.0.0")
v2 = Tool(name="test", version="2.0.0")
assert compute_tool_hash(v1) != compute_tool_hash(v2)
class TestIntegrityChain:
def test_single_tool_chain(self):
tool = Tool(name="solo", version="1.0.0")
chain = build_integrity_chain(tool)
assert chain.root.name == "solo"
assert chain.is_valid
def test_chain_with_unresolved_dep(self):
tool = Tool(
name="parent",
version="1.0.0",
steps=[ToolStep(tool="missing-dep", output_var="x")],
output="{x}",
)
chain = build_integrity_chain(tool)
assert "missing-dep" not in chain.nodes
assert not chain.is_valid
assert chain.errors == ["Unresolved dependency: missing-dep"]
def test_chain_valid_with_resolved_dep(self, tmp_path):
with (
patch("cmdforge.tool.TOOLS_DIR", tmp_path / ".cmdforge"),
patch("cmdforge.tool.BIN_DIR", tmp_path / "bin"),
):
from cmdforge.tool import save_tool
child = Tool(name="child", version="1.0.0")
save_tool(child)
parent = Tool(
name="parent",
version="1.0.0",
steps=[ToolStep(tool="child", output_var="x")],
output="{x}",
)
save_tool(parent)
chain = build_integrity_chain(parent)
assert "child" in chain.nodes
assert chain.is_valid
def test_child_change_changes_root_identity(self, tmp_path):
with (
patch("cmdforge.tool.TOOLS_DIR", tmp_path / ".cmdforge"),
patch("cmdforge.tool.BIN_DIR", tmp_path / "bin"),
):
from cmdforge.tool import save_tool
child = Tool(name="child", version="1.0.0", output="one")
save_tool(child)
parent = Tool(
name="parent", version="1.0.0",
steps=[ToolStep(tool="child", output_var="x")], output="{x}",
)
first = build_integrity_chain(parent).root.content_hash
child.output = "two"
save_tool(child)
second = build_integrity_chain(parent).root.content_hash
assert first != second
class TestVerifyIntegrity:
def test_valid_chain(self):
from cmdforge.integrity import compute_content_identity
definition_hash = "sha256:" + "a" * 64
content_hash = compute_content_identity(definition_hash, {})
node = IntegrityNode(
name="root", definition_hash=definition_hash,
content_hash=content_hash,
)
chain = IntegrityChain(root=node, nodes={"root": node})
assert verify_integrity(chain)
def test_tampered_chain(self):
chain = IntegrityChain(
root=IntegrityNode(
name="root",
content_hash="abc",
dependencies=["dep"],
dependency_hashes={"dep": "expected_hash"},
),
nodes={
"root": IntegrityNode(
name="root",
content_hash="abc",
dependencies=["dep"],
dependency_hashes={"dep": "expected_hash"},
),
"dep": IntegrityNode(name="dep", content_hash="different_hash"),
},
)
assert not chain.is_valid