"""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