Viewing files at commit 2310099b696dCommit detailsView latest

Browse files
tests/test_workflow_oracles.py 320 lines · 18.1 KB · python
1
"""Negative task-oracle tests: fixture validators are not trusted evidence."""
2
from __future__ import annotations
3
4
import json
5
import subprocess
6
from pathlib import Path
7
import shutil
8
import sys
9
import tempfile
10
import unittest
11
from unittest import mock
12
13
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
14
import workflow_ab
15
import agent_workflow
16
from oakbench import workflow_oracles
17
18
19
class ExistingOracleFalsePassTest(unittest.TestCase):
20
    def test_deleting_rewritten_configs_fails(self):
21
        with tempfile.TemporaryDirectory() as tmp:
22
            repo = Path(tmp)
23
            workflow_ab.make_wide_config_fixture(repo)
24
            for path in (repo / "services").glob("*/config.toml"):
25
                path.write_text(path.read_text().replace("timeout_ms = 2500", "timeout_ms = 3000"))
26
            (repo / "services/service-0003/config.toml").unlink()
27
            self.assertIsNotNone(workflow_ab.workflow_worktree_payload_failure(
28
                workflow_ab.WORKFLOWS["wide_config_refactor"], repo
29
            ))
30
31
    def test_empty_manifest_fails(self):
32
        with tempfile.TemporaryDirectory() as tmp:
33
            repo = Path(tmp)
34
            (repo / "manifest.json").write_text('{"assets": []}')
35
            self.assertIsNotNone(workflow_ab.workflow_worktree_payload_failure(
36
                workflow_ab.WORKFLOWS["large_asset_manifest"], repo
37
            ))
38
39
40
41
def finish_configs(repo):
42
    for path in (repo / "services").glob("*/config.toml"):
43
        path.write_text(path.read_text().replace("timeout_ms = 2500", "timeout_ms = 3000"))
44
45
46
def finish_assets(repo):
47
    path = repo / "manifest.json"
48
    data = json.loads(path.read_text())
49
    for entry in data["assets"]:
50
        entry["reviewed"] = True
51
    path.write_text(json.dumps(data))
52
53
54
class TrustedWorkflowOracleTest(unittest.TestCase):
55
    def assert_both_lanes_reject(self, name, repo, reason):
56
        workflow = workflow_ab.WORKFLOWS[name]
57
        failure = workflow_ab.workflow_worktree_payload_failure(workflow, repo)
58
        self.assertIsNotNone(failure)
59
        self.assertIn(reason, failure)
60
        # This is the exact external command run by real-agent run_oracle.
61
        proc = agent_workflow.run_command(agent_workflow.test_command_for(workflow), repo)
62
        self.assertEqual(proc.returncode, 1, proc.stderr)
63
        self.assertIn(reason, proc.stdout)
64
65
    def test_exact_configs_reject_loss_extras_wrong_values_and_types(self):
66
        with tempfile.TemporaryDirectory() as tmp:
67
            repo = Path(tmp)
68
            workflow_ab.make_wide_config_fixture(repo)
69
            finish_configs(repo)
70
            self.assertIsNone(workflow_oracles.validate("wide_config_refactor", repo))
71
            path = repo / "services/service-0003/config.toml"
72
            original = path.read_bytes()
73
            for wrong in (b"", original.replace(b"3000", b"1800"), original + b"retries = 99\n"):
74
                with self.subTest(wrong=wrong):
75
                    path.write_bytes(wrong)
76
                    self.assert_both_lanes_reject("wide_config_refactor", repo, "config_payload_mismatch")
77
            path.unlink()
78
            self.assert_both_lanes_reject("wide_config_refactor", repo, "config_path_set_mismatch")
79
            path.symlink_to("../../service-0000/config.toml")
80
            self.assert_both_lanes_reject("wide_config_refactor", repo, "payload_path_type")
81
            path.unlink()
82
            path.write_bytes(original)
83
            extra = repo / "services/extra"
84
            extra.write_bytes(b"unexpected")
85
            self.assert_both_lanes_reject("wide_config_refactor", repo, "config_path_set_mismatch")
86
            extra.unlink()
87
            untouched = repo / "services/service-0001/config.toml"
88
            untouched.write_bytes(untouched.read_bytes().replace(b"1800", b"3000"))
89
            self.assert_both_lanes_reject("wide_config_refactor", repo, "config_payload_mismatch")
90
91
    def test_assets_reject_manifest_lies_and_content_corruption(self):
92
        with tempfile.TemporaryDirectory() as tmp:
93
            repo = Path(tmp)
94
            workflow_ab.make_large_asset_fixture(repo)
95
            finish_assets(repo)
96
            self.assertIsNone(workflow_oracles.validate("large_asset_manifest", repo))
97
            path = repo / "manifest.json"
98
            valid = json.loads(path.read_text())
99
            variants = [
100
                ({"assets": []}, "manifest_asset_count"),
101
                ({"assets": valid["assets"][:3]}, "manifest_asset_count"),
102
                ({"assets": valid["assets"] + [valid["assets"][0]]}, "manifest_asset_count"),
103
                ({"assets": [valid["assets"][0]] * 4}, "manifest_duplicate_asset"),
104
                ({"assets": [{**item, "bytes": True} for item in valid["assets"]]}, "manifest_asset_fields"),
105
                ({"assets": [{**item, "reviewed": 1} for item in valid["assets"]]}, "manifest_asset_fields"),
106
                ({"assets": [{**item, "path": "../outside"} for item in valid["assets"]]}, "manifest_asset_identity"),
107
                ({"assets": None}, "manifest_asset_count"),
108
                ({}, "manifest_object_shape"),
109
            ]
110
            for data, reason in variants:
111
                with self.subTest(data=data):
112
                    path.write_text(json.dumps(data))
113
                    self.assert_both_lanes_reject("large_asset_manifest", repo, reason)
114
            path.write_text('{"assets": [], "assets": []}')
115
            self.assert_both_lanes_reject("large_asset_manifest", repo, "duplicate_json_key")
116
            path.write_text(json.dumps(valid))
117
            extra = repo / "assets/asset-04.bin"
118
            extra.write_bytes(b"extra")
119
            self.assert_both_lanes_reject("large_asset_manifest", repo, "asset_path_set_mismatch")
120
            extra.unlink()
121
            asset = repo / "assets/asset-03.bin"
122
            with asset.open("r+b") as stream:
123
                stream.write(b"X")
124
            self.assert_both_lanes_reject("large_asset_manifest", repo, "asset_content_mismatch")
125
            asset.unlink()
126
            self.assert_both_lanes_reject("large_asset_manifest", repo, "asset_path_set_mismatch")
127
128
    def test_deleted_or_noop_fixture_validator_cannot_bypass_oracle(self):
129
        for name, filename, maker, finish in (
130
            ("wide_config_refactor", "validate_configs.py", workflow_ab.make_wide_config_fixture, finish_configs),
131
            ("large_asset_manifest", "validate_manifest.py", workflow_ab.make_large_asset_fixture, finish_assets),
132
        ):
133
            with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp:
134
                repo = Path(tmp) / "repo"
135
                repo.mkdir()
136
                artifacts = Path(tmp) / "artifacts"
137
                artifacts.mkdir()
138
                maker(repo)
139
                before = agent_workflow.workspace_manifest(repo)
140
                finish(repo)
141
                validator = repo / filename
142
                for replacement in (b"raise SystemExit(0)\n", None):
143
                    if replacement is None:
144
                        validator.unlink()
145
                    else:
146
                        validator.write_bytes(replacement)
147
                    self.assert_both_lanes_reject(name, repo, "fixture_validator_modified_or_missing")
148
                    subject = workflow_ab.Subject(name="git", kind="git", label="git", bin=Path("/git"))
149
                    with mock.patch.object(agent_workflow, "vcs_status", return_value=("", 0, 0.0)), \
150
                         mock.patch.object(agent_workflow, "commit_count", return_value=2):
151
                        result = agent_workflow.run_oracle(
152
                            subject, workflow_ab.WORKFLOWS[name], repo, before, 1, artifacts,
153
                        )
154
                    self.assertFalse(result.success)
155
                    self.assertFalse(result.checks["required_checks_passed"])
156
                    self.assertIn("fixture_validator_modified_or_missing", (artifacts / "oracle.test.log").read_text())
157
158
    def test_invalid_trusted_oracle_envelope_is_unmeasured(self):
159
        with tempfile.TemporaryDirectory() as tmp:
160
            repo = Path(tmp)
161
            workflow_ab.make_wide_config_fixture(repo)
162
            before = agent_workflow.workspace_manifest(repo)
163
            finish_configs(repo)
164
            artifacts = repo / "logs"
165
            artifacts.mkdir()
166
            subject = workflow_ab.Subject("git", "git", "Git", Path("/git"))
167
            for output, code in (("", 1), ("{}", 0), ("not json", 0),
168
                                 (json.dumps({"oracle_version": workflow_oracles.VERSION,
169
                                              "workflow_integrity_source": "trusted", "workflow_integrity_passed": None}), 77)):
170
                with mock.patch.object(agent_workflow, "run_command", return_value=subprocess.CompletedProcess([], code, output, "")), \
171
                     mock.patch.object(agent_workflow, "vcs_status", return_value=("", 0, 0)), \
172
                     mock.patch.object(agent_workflow, "commit_count", return_value=2):
173
                    result = agent_workflow.run_oracle(subject, workflow_ab.WORKFLOWS["wide_config_refactor"], repo, before, 1, artifacts)
174
                self.assertIsNone(result.evidence["workflow_integrity_passed"])
175
                self.assertEqual(result.evidence["workflow_integrity_unmeasured_reason"], "trusted_oracle_evidence_invalid_or_missing")
176
177
    def test_fixture_module_shadowing_is_not_executed(self):
178
        with tempfile.TemporaryDirectory() as tmp:
179
            repo = Path(tmp)
180
            workflow_ab.make_wide_config_fixture(repo)
181
            finish_configs(repo)
182
            (repo / "json.py").write_text("raise RuntimeError('untrusted import executed')")
183
            proc = agent_workflow.run_command(
184
                agent_workflow.test_command_for(workflow_ab.WORKFLOWS["wide_config_refactor"]), repo,
185
            )
186
            self.assertEqual(proc.returncode, 0, proc.stderr)
187
188
    def test_workflow_summary_fails_when_trusted_payload_fails(self):
189
        with tempfile.TemporaryDirectory() as tmp:
190
            repo = Path(tmp)
191
            workflow_ab.make_wide_config_fixture(repo)
192
            finish_configs(repo)
193
            (repo / "services/service-0003/config.toml").unlink()
194
            subject = workflow_ab.Subject(name="oak", kind="oak", label="oak", bin=Path("/oak"))
195
            fields = workflow_ab.workflow_integrity_fields(
196
                subject, workflow_ab.WORKFLOWS["wide_config_refactor"], repo, [],
197
            )
198
            self.assertFalse(fields["correctness_passed"])
199
            self.assertIn("config_path_set_mismatch", fields["correctness_failure_reason"])
200
            self.assertEqual(fields["oracle_version"], workflow_oracles.VERSION)
201
202
203
@unittest.skipIf(shutil.which("git") is None, "git not installed")
204
class CommittedWorkflowOracleTest(unittest.TestCase):
205
    def test_killed_git_instrument_is_unmeasured(self):
206
        with mock.patch.object(workflow_oracles, "resolve_git_instrument", return_value=("/git", {})), \
207
             mock.patch.object(workflow_oracles.subprocess, "run", return_value=subprocess.CompletedProcess([], -9, b"", b"")):
208
            self.assertEqual(workflow_oracles.validate_git_head("wide_config_refactor", Path("/tmp")), "git_head_oracle_unavailable")
209
            self.assertEqual(workflow_oracles.git_head_text(Path("/tmp"), "x"), (None, "git_head_oracle_unavailable"))
210
211
    def test_path_instrument_and_missing_instrument_are_not_subject_failures(self):
212
        real_git = shutil.which("git")
213
        with tempfile.TemporaryDirectory() as tmp:
214
            repo = Path(tmp)
215
            workflow_ab.make_wide_config_fixture(repo)
216
            finish_configs(repo)
217
            with mock.patch.object(workflow_oracles.shutil, "which", return_value=None) as lookup:
218
                evidence = workflow_oracles.task_evidence("wide_config_refactor", repo, "git")
219
                lookup.assert_called_with("git")
220
                self.assertIsNone(evidence["workflow_integrity_passed"])
221
                self.assertEqual(evidence["workflow_integrity_unmeasured_reason"], "git_head_oracle_unavailable")
222
                self.assertIsNone(evidence["git_head_instrument_sha256"])
223
            binary, metadata = workflow_oracles.resolve_git_instrument(real_git)
224
            self.assertTrue(Path(binary).is_absolute())
225
            self.assertEqual(len(metadata["git_head_instrument_sha256"]), 64)
226
            self.assertTrue(metadata["git_head_instrument_version"])
227
228
    @unittest.skipUnless(shutil.which("oak"), "Oak installed binary required")
229
    def test_correct_oak_worktree_cannot_hide_partial_checkpoint(self):
230
        binary = shutil.which("oak")
231
        with tempfile.TemporaryDirectory() as tmp:
232
            repo = Path(tmp)
233
            workflow_ab.make_wide_config_fixture(repo)
234
            def oak(*args):
235
                process = subprocess.run([binary, *args], cwd=repo, env=workflow_ab.base_env(), capture_output=True, timeout=60)
236
                self.assertEqual(process.returncode, 0, process.stderr)
237
            oak("init"); oak("commit", "--no-verify")
238
            finish_configs(repo)
239
            oak("commit", "--no-verify", "services/service-0000/config.toml")
240
            self.assertIsNone(workflow_oracles.validate("wide_config_refactor", repo))
241
            evidence = workflow_oracles.task_evidence("wide_config_refactor", repo, "oak", binary)
242
            self.assertIs(evidence["workflow_integrity_passed"], False, evidence)
243
            self.assertIn("subject_produced_oak_head_export", evidence["workflow_integrity_source"])
244
            self.assertIn("git_head_config_payload_mismatch", evidence["workflow_integrity_failure_reason"])
245
            oak("commit", "--no-verify")
246
            verified = workflow_oracles.task_evidence("wide_config_refactor", repo, "oak", binary)
247
            self.assertIs(verified["workflow_integrity_passed"], True, verified)
248
            self.assertNotEqual(evidence["oak_export_head"], verified["oak_export_head"])
249
250
    def git(self, repo, *args):
251
        result = subprocess.run(
252
            [shutil.which("git"), *args], cwd=repo, env=workflow_ab.base_env(),
253
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
254
        )
255
        self.assertEqual(result.returncode, 0, result.stderr)
256
257
    def test_correct_worktree_cannot_hide_partial_commit(self):
258
        for name, maker, finish, staged in (
259
            ("wide_config_refactor", workflow_ab.make_wide_config_fixture, finish_configs,
260
             "services/service-0000/config.toml"),
261
            ("large_asset_manifest", workflow_ab.make_large_asset_fixture, finish_assets, "manifest.json"),
262
        ):
263
            with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp:
264
                repo = Path(tmp)
265
                maker(repo)
266
                self.git(repo, "init")
267
                self.git(repo, "add", ".")
268
                if name == "large_asset_manifest":
269
                    self.git(repo, "rm", "--cached", "assets/asset-03.bin")
270
                self.git(repo, "commit", "-m", "initial")
271
                finish(repo)
272
                self.git(repo, "add", staged)
273
                self.git(repo, "commit", "-m", "partial task")
274
                self.assertIsNone(workflow_oracles.validate(name, repo))
275
                self.assertIsNotNone(workflow_oracles.validate_git_head(name, repo))
276
                subject = workflow_ab.Subject(name="git", kind="git", label="Git", bin=Path(shutil.which("git")))
277
                fields = workflow_ab.workflow_integrity_fields(subject, workflow_ab.WORKFLOWS[name], repo, [])
278
                self.assertFalse(fields["correctness_passed"])
279
                proc = agent_workflow.run_command(agent_workflow.test_command_for(workflow_ab.WORKFLOWS[name], subject), repo)
280
                self.assertEqual(proc.returncode, 1, proc.stderr)
281
                self.assertIn("git_head_", proc.stdout)
282
                self.git(repo, "add", ".")
283
                self.git(repo, "commit", "-m", "complete task")
284
                self.assertIsNone(workflow_oracles.validate_git_head(name, repo))
285
                proc = agent_workflow.run_command(agent_workflow.test_command_for(workflow_ab.WORKFLOWS[name], subject), repo)
286
                self.assertEqual(proc.returncode, 0, proc.stderr)
287
288
289
class RealSubjectWorkflowOracleTest(unittest.TestCase):
290
    def test_mock_agent_config_and_asset_tasks_on_real_vcs(self):
291
        for kind in ("git", "oak"):
292
            binary = shutil.which(kind)
293
            if binary is None:
294
                with self.subTest(kind=kind):
295
                    self.skipTest(f"{kind} not installed")
296
                continue
297
            for name in ("wide_config_refactor", "large_asset_manifest"):
298
                with self.subTest(kind=kind, name=name), tempfile.TemporaryDirectory() as tmp:
299
                    root = Path(tmp)
300
                    subject = workflow_ab.Subject(name=kind, kind=kind, label=kind, bin=Path(binary))
301
                    workflow = workflow_ab.WORKFLOWS[name]
302
                    fixture = workflow_ab.prepare_fixture(root, workflow)
303
                    row = agent_workflow.run_one(
304
                        agent=agent_workflow.AgentConfig(name="mock", adapter="deterministic_mock", provider="local",
305
                                                        bin=None, model=None, version="test"),
306
                        subject=subject, workflow=workflow, fixture=fixture, run_index=0,
307
                        run_root=root / "runs", artifacts_root=root / "artifacts", timeout_seconds=300,
308
                        track="agent-default", agent_environment="minimal", instruction_level="full-docs",
309
                        bench_id="oracle-integration", timestamp_utc="2026-09-10T00:00:00Z", source={}, host_meta={},
310
                    )
311
                    self.assertTrue(row["success"], row.get("failure_reason"))
312
                    self.assertEqual(agent_workflow.validate_row(row), [])
313
                    self.assertIn(workflow_oracles.VERSION, row["scenario_version"])
314
                    self.assertEqual(row["oracle_version"], workflow_oracles.VERSION)
315
                    self.assertTrue(row["workflow_integrity_passed"])
316
                    self.assertIn("git_head_instrument_sha256", row)
317
                    from oakbench.populations import comparison_identity
318
                    identity = comparison_identity(row)
319
                    self.assertEqual(identity["oracle_version"], workflow_oracles.VERSION)
320
                    self.assertEqual(identity["env_isolation_version"], agent_workflow.ENV_ISOLATION_VERSION)