| 1 | """Tests for the command-semantics contract (the fairness guarantee)."""
|
| 2 |
|
| 3 | from __future__ import annotations
|
| 4 |
|
| 5 | import sys
|
| 6 | from pathlib import Path
|
| 7 |
|
| 8 | sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
| 9 |
|
| 10 | import unittest
|
| 11 | import json
|
| 12 | import tempfile
|
| 13 |
|
| 14 | from oakbench.command_semantics import CommandSemantics, semantics
|
| 15 |
|
| 16 | ROOT = Path(__file__).resolve().parents[1]
|
| 17 |
|
| 18 |
|
| 19 | class ContractTest(unittest.TestCase):
|
| 20 | def setUp(self) -> None:
|
| 21 | self.sem = semantics()
|
| 22 |
|
| 23 | def test_version_is_recorded_shape(self) -> None:
|
| 24 | self.assertRegex(self.sem.version, r"^\d{4}-\d{2}-\d{2}\.\d+$")
|
| 25 |
|
| 26 | def test_snapshot_call_accounting(self) -> None:
|
| 27 | # The suite-wide convention: git pays stage+commit as two tool calls,
|
| 28 | # oak snapshots in one. Agents pay every recorded call.
|
| 29 | git_commands = self.sem.snapshot_commands("git", "git", "agent task")
|
| 30 | oak_commands = self.sem.snapshot_commands("oak", "oak", "agent task")
|
| 31 | self.assertEqual(len(git_commands), 2)
|
| 32 | self.assertEqual(len(oak_commands), 1)
|
| 33 | self.assertEqual(git_commands[0], ["git", "add", "."])
|
| 34 | self.assertEqual(git_commands[1], ["git", "commit", "-m", "agent task"])
|
| 35 | self.assertEqual(oak_commands[0], ["oak", "commit", "--no-verify"])
|
| 36 |
|
| 37 | def test_message_substitution(self) -> None:
|
| 38 | self.assertEqual(self.sem.commit_args("git", "initial"), ["commit", "-m", "initial"])
|
| 39 | # Oak commits carry no message; the contract must not leak the placeholder.
|
| 40 | self.assertNotIn("{message}", " ".join(self.sem.commit_args("oak", "anything")))
|
| 41 |
|
| 42 | def test_workflow_tracks_keep_oak_snapshots_local_only(self) -> None:
|
| 43 | self.assertEqual(self.sem.status_args("git", "agent-default"), ["status", "--short"])
|
| 44 | self.assertEqual(self.sem.status_args("git", "core-equivalent"), ["status", "--porcelain=v1"])
|
| 45 | self.assertEqual(self.sem.diff_args("git", "agent-default"), ["diff", "--stat"])
|
| 46 | self.assertEqual(self.sem.diff_args("git", "core-equivalent"), ["diff", "--no-ext-diff"])
|
| 47 | # Workflow snapshots are local checkpoints. Remote workflow setup and
|
| 48 | # publish phases must add explicit `oak push` or `oak commit --push`.
|
| 49 | self.assertEqual(self.sem.commit_args("oak", "agent task"), ["commit", "--no-verify"])
|
| 50 | for track in ("agent-default", "core-equivalent"):
|
| 51 | self.assertEqual(self.sem.status_args("oak", track), ["status"])
|
| 52 | # Agents type plain `oak diff` (summary); the core-equivalent full diff
|
| 53 | # is `oak diff --print` (unified hunks + binary notices, oak >= 0.96).
|
| 54 | self.assertEqual(self.sem.diff_args("oak", "agent-default"), ["diff"])
|
| 55 | self.assertEqual(self.sem.diff_args("oak", "core-equivalent"), ["diff", "--print"])
|
| 56 |
|
| 57 | def test_published_operation_contracts_use_agent_native_oak_commands(self) -> None:
|
| 58 | data = json.loads((ROOT / "config" / "command_semantics.json").read_text())
|
| 59 | operations = data["operations"]
|
| 60 |
|
| 61 | self.assertEqual(operations["status.short"]["oak"], ["oak", "status", "--porcelain"])
|
| 62 | self.assertTrue(operations["status.short"]["semantic_contract_match"])
|
| 63 | self.assertEqual(operations["diff.name_only"]["oak"], ["oak", "diff", "--name-only"])
|
| 64 | self.assertTrue(operations["diff.name_only"]["semantic_contract_match"])
|
| 65 | self.assertEqual(
|
| 66 | operations["commit.quiet"]["oak"],
|
| 67 | ["oak", "commit", "--json", "--quiet", "--no-verify"],
|
| 68 | )
|
| 69 | self.assertTrue(operations["commit.quiet"]["semantic_contract_match"])
|
| 70 |
|
| 71 | def test_displays_match_args(self) -> None:
|
| 72 | self.assertEqual(self.sem.status_display("git", "agent-default"), "git status --short")
|
| 73 | self.assertEqual(self.sem.diff_display("oak", "agent-default"), "oak diff")
|
| 74 |
|
| 75 | def test_oak_agent_native_json_probes_are_configured(self) -> None:
|
| 76 | self.assertEqual(self.sem.agent_native_json_probes("git", "agent-task"), {})
|
| 77 | probes = self.sem.agent_native_json_probes("oak", "agent-task")
|
| 78 | self.assertEqual(
|
| 79 | probes["oak.agent.state.json"]["args"],
|
| 80 | ["agent", "state", "--json"],
|
| 81 | )
|
| 82 | self.assertEqual(
|
| 83 | probes["oak.agent.state.compact.json"]["args"],
|
| 84 | ["agent", "state", "--json", "--compact"],
|
| 85 | )
|
| 86 | self.assertNotIn("changes", probes["oak.agent.state.compact.json"]["required_fields"])
|
| 87 | self.assertIn("finish_eligible", probes["oak.agent.state.compact.json"]["required_fields"])
|
| 88 | self.assertIn("recommended_action", probes["oak.agent.state.compact.json"]["required_fields"])
|
| 89 | self.assertIn("recommended_action", probes["oak.agent.state.json"]["required_fields"])
|
| 90 | self.assertEqual(probes["oak.agent.state.json"]["minimum_oak_version"], "0.98.0")
|
| 91 | self.assertEqual(probes["oak.agent.state.compact.json"]["minimum_oak_version"], "0.98.0")
|
| 92 | self.assertEqual(
|
| 93 | probes["oak.status.json"]["args"],
|
| 94 | ["status", "--json"],
|
| 95 | )
|
| 96 | self.assertEqual(
|
| 97 | probes["oak.branch.review.json"]["args"],
|
| 98 | ["branch", "review", "--json", "agent-task"],
|
| 99 | )
|
| 100 | self.assertEqual(
|
| 101 | probes["oak.branch.diff.json"]["args"],
|
| 102 | ["branch", "diff", "--json", "agent-task"],
|
| 103 | )
|
| 104 | for spec in probes.values():
|
| 105 | self.assertIn("--json", spec["args"])
|
| 106 | self.assertIn("schema_version", spec["required_fields"])
|
| 107 |
|
| 108 | def test_unknown_kind_rejected(self) -> None:
|
| 109 | with self.assertRaises(ValueError):
|
| 110 | self.sem.init_args("svn")
|
| 111 |
|
| 112 | def test_loads_fresh_from_config(self) -> None:
|
| 113 | fresh = CommandSemantics()
|
| 114 | self.assertEqual(fresh.version, self.sem.version)
|
| 115 |
|
| 116 | def test_missing_output_format_has_actionable_config_error(self) -> None:
|
| 117 | data = json.loads((ROOT / "config/command_semantics.json").read_text())
|
| 118 | for formats in (None, {}, {"git": {}}, {"git": {"diff": {}}}):
|
| 119 | with self.subTest(formats=formats), tempfile.TemporaryDirectory() as tmp:
|
| 120 | if formats is None:
|
| 121 | data.pop("output_formats", None)
|
| 122 | else:
|
| 123 | data["output_formats"] = formats
|
| 124 | path = Path(tmp) / "semantics.json"
|
| 125 | path.write_text(json.dumps(data))
|
| 126 | with self.assertRaisesRegex(ValueError, "output_formats.*git.*diff.*core-equivalent"):
|
| 127 | CommandSemantics(path).output_format("git", "diff.full", "agent-default")
|
| 128 |
|
| 129 |
|
| 130 | if __name__ == "__main__":
|
| 131 | unittest.main()
|