"""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 # Chain is still valid because the dependency hash is "unresolved" # and there's no node to compare against assert chain.is_valid def test_chain_valid_with_resolved_dep(self, tmp_path): with patch("cmdforge.tool.TOOLS_DIR", tmp_path / ".cmdforge"): 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 class TestVerifyIntegrity: def test_valid_chain(self): chain = IntegrityChain( root=IntegrityNode(name="root", content_hash="abc"), nodes={"root": IntegrityNode(name="root", content_hash="abc")}, ) 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