Viewing files at commit 5717cc9fc7edCommit detailsView latest

Browse files
tests/test_command_semantics.py 131 lines · 6.4 KB · python
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()