Viewing files at commit 62758990fe31Commit detailsView latest

Browse files
tests/test_workflow_oracles.py 483 lines · 28.6 KB · python
1
"""Negative task-oracle tests: fixture validators are not trusted evidence."""
2
from __future__ import annotations
3
4
import json
5
import os
6
import subprocess
7
from pathlib import Path
8
import shutil
9
import sys
10
import tempfile
11
import unittest
12
from unittest import mock
13
14
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
15
import workflow_ab
16
import agent_workflow
17
from oakbench import workflow_oracles
18
19
20
21
# These tests assert the DEFAULT evidence path (subject Oak export, PATH Git).
22
# Isolate them from an operator-configured independent reader/instrument so
23
# the full suite is green with and without the optional reader (QA P1 A5).
24
_ISOLATED_ENV = ("OAK_BENCH_OAK_READER_PYTHON", "OAK_BENCH_GIT_INSTRUMENT")
25
_SAVED_ENV = {}
26
27
28
def setUpModule():
29
    import os as _os
30
    for _name in _ISOLATED_ENV:
31
        if _name in _os.environ:
32
            _SAVED_ENV[_name] = _os.environ.pop(_name)
33
34
35
def tearDownModule():
36
    import os as _os
37
    _os.environ.update(_SAVED_ENV)
38
39
class ExistingOracleFalsePassTest(unittest.TestCase):
40
    def test_oracle_version_is_pinned(self):
41
        self.assertEqual(workflow_oracles.VERSION, "2026-09-28.2")
42
43
    def test_deleting_rewritten_configs_fails(self):
44
        with tempfile.TemporaryDirectory() as tmp:
45
            repo = Path(tmp)
46
            workflow_ab.make_wide_config_fixture(repo)
47
            for path in (repo / "services").glob("*/config.toml"):
48
                path.write_text(path.read_text().replace("timeout_ms = 2500", "timeout_ms = 3000"))
49
            (repo / "services/service-0003/config.toml").unlink()
50
            self.assertIsNotNone(workflow_ab.workflow_worktree_payload_failure(
51
                workflow_ab.WORKFLOWS["wide_config_refactor"], repo
52
            ))
53
54
    def test_empty_manifest_fails(self):
55
        with tempfile.TemporaryDirectory() as tmp:
56
            repo = Path(tmp)
57
            (repo / "manifest.json").write_text('{"assets": []}')
58
            self.assertIsNotNone(workflow_ab.workflow_worktree_payload_failure(
59
                workflow_ab.WORKFLOWS["large_asset_manifest"], repo
60
            ))
61
62
63
64
def finish_configs(repo):
65
    for path in (repo / "services").glob("*/config.toml"):
66
        path.write_text(path.read_text().replace("timeout_ms = 2500", "timeout_ms = 3000"))
67
68
69
def finish_assets(repo):
70
    path = repo / "manifest.json"
71
    data = json.loads(path.read_text())
72
    for entry in data["assets"]:
73
        entry["reviewed"] = True
74
    path.write_text(json.dumps(data))
75
76
77
class TrustedWorkflowOracleTest(unittest.TestCase):
78
    def assert_both_lanes_reject(self, name, repo, reason):
79
        workflow = workflow_ab.WORKFLOWS[name]
80
        failure = workflow_ab.workflow_worktree_payload_failure(workflow, repo)
81
        self.assertIsNotNone(failure)
82
        self.assertIn(reason, failure)
83
        # This is the exact external command run by real-agent run_oracle.
84
        proc = agent_workflow.run_command(agent_workflow.test_command_for(workflow), repo)
85
        self.assertEqual(proc.returncode, 1, proc.stderr)
86
        self.assertIn(reason, proc.stdout)
87
88
    def test_exact_configs_reject_loss_extras_wrong_values_and_types(self):
89
        with tempfile.TemporaryDirectory() as tmp:
90
            repo = Path(tmp)
91
            workflow_ab.make_wide_config_fixture(repo)
92
            finish_configs(repo)
93
            self.assertIsNone(workflow_oracles.validate("wide_config_refactor", repo))
94
            path = repo / "services/service-0003/config.toml"
95
            original = path.read_bytes()
96
            for wrong in (b"", original.replace(b"3000", b"1800"), original + b"retries = 99\n"):
97
                with self.subTest(wrong=wrong):
98
                    path.write_bytes(wrong)
99
                    self.assert_both_lanes_reject("wide_config_refactor", repo, "config_payload_mismatch")
100
            path.unlink()
101
            self.assert_both_lanes_reject("wide_config_refactor", repo, "config_path_set_mismatch")
102
            path.symlink_to("../../service-0000/config.toml")
103
            self.assert_both_lanes_reject("wide_config_refactor", repo, "payload_path_type")
104
            path.unlink()
105
            path.write_bytes(original)
106
            extra = repo / "services/extra"
107
            extra.write_bytes(b"unexpected")
108
            self.assert_both_lanes_reject("wide_config_refactor", repo, "config_path_set_mismatch")
109
            extra.unlink()
110
            untouched = repo / "services/service-0001/config.toml"
111
            untouched.write_bytes(untouched.read_bytes().replace(b"1800", b"3000"))
112
            self.assert_both_lanes_reject("wide_config_refactor", repo, "config_payload_mismatch")
113
114
    def test_assets_reject_manifest_lies_and_content_corruption(self):
115
        with tempfile.TemporaryDirectory() as tmp:
116
            repo = Path(tmp)
117
            workflow_ab.make_large_asset_fixture(repo)
118
            finish_assets(repo)
119
            self.assertIsNone(workflow_oracles.validate("large_asset_manifest", repo))
120
            path = repo / "manifest.json"
121
            valid = json.loads(path.read_text())
122
            variants = [
123
                ({"assets": []}, "manifest_asset_count"),
124
                ({"assets": valid["assets"][:3]}, "manifest_asset_count"),
125
                ({"assets": valid["assets"] + [valid["assets"][0]]}, "manifest_asset_count"),
126
                ({"assets": [valid["assets"][0]] * 4}, "manifest_duplicate_asset"),
127
                ({"assets": [{**item, "bytes": True} for item in valid["assets"]]}, "manifest_asset_fields"),
128
                ({"assets": [{**item, "reviewed": 1} for item in valid["assets"]]}, "manifest_asset_fields"),
129
                ({"assets": [{**item, "path": "../outside"} for item in valid["assets"]]}, "manifest_asset_identity"),
130
                ({"assets": None}, "manifest_asset_count"),
131
                ({}, "manifest_object_shape"),
132
            ]
133
            for data, reason in variants:
134
                with self.subTest(data=data):
135
                    path.write_text(json.dumps(data))
136
                    self.assert_both_lanes_reject("large_asset_manifest", repo, reason)
137
            path.write_text('{"assets": [], "assets": []}')
138
            self.assert_both_lanes_reject("large_asset_manifest", repo, "duplicate_json_key")
139
            path.write_text(json.dumps(valid))
140
            extra = repo / "assets/asset-04.bin"
141
            extra.write_bytes(b"extra")
142
            self.assert_both_lanes_reject("large_asset_manifest", repo, "asset_path_set_mismatch")
143
            extra.unlink()
144
            asset = repo / "assets/asset-03.bin"
145
            with asset.open("r+b") as stream:
146
                stream.write(b"X")
147
            self.assert_both_lanes_reject("large_asset_manifest", repo, "asset_content_mismatch")
148
            asset.unlink()
149
            self.assert_both_lanes_reject("large_asset_manifest", repo, "asset_path_set_mismatch")
150
151
    def test_deleted_or_noop_fixture_validator_cannot_bypass_oracle(self):
152
        for name, filename, maker, finish in (
153
            ("wide_config_refactor", "validate_configs.py", workflow_ab.make_wide_config_fixture, finish_configs),
154
            ("large_asset_manifest", "validate_manifest.py", workflow_ab.make_large_asset_fixture, finish_assets),
155
        ):
156
            with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp:
157
                repo = Path(tmp) / "repo"
158
                repo.mkdir()
159
                artifacts = Path(tmp) / "artifacts"
160
                artifacts.mkdir()
161
                maker(repo)
162
                before = agent_workflow.workspace_manifest(repo)
163
                finish(repo)
164
                validator = repo / filename
165
                for replacement in (b"raise SystemExit(0)\n", None):
166
                    if replacement is None:
167
                        validator.unlink()
168
                    else:
169
                        validator.write_bytes(replacement)
170
                    self.assert_both_lanes_reject(name, repo, "fixture_validator_modified_or_missing")
171
                    subject = workflow_ab.Subject(name="git", kind="git", label="git", bin=Path("/git"))
172
                    with mock.patch.object(agent_workflow, "vcs_status", return_value=("", 0, 0.0)), \
173
                         mock.patch.object(agent_workflow, "commit_count", return_value=2):
174
                        result = agent_workflow.run_oracle(
175
                            subject, workflow_ab.WORKFLOWS[name], repo, before, 1, artifacts,
176
                        )
177
                    self.assertFalse(result.success)
178
                    self.assertFalse(result.checks["required_checks_passed"])
179
                    self.assertIn("fixture_validator_modified_or_missing", (artifacts / "oracle.test.log").read_text())
180
181
    def test_invalid_trusted_oracle_envelope_is_unmeasured(self):
182
        with tempfile.TemporaryDirectory() as tmp:
183
            repo = Path(tmp)
184
            workflow_ab.make_wide_config_fixture(repo)
185
            before = agent_workflow.workspace_manifest(repo)
186
            finish_configs(repo)
187
            artifacts = repo / "logs"
188
            artifacts.mkdir()
189
            subject = workflow_ab.Subject("git", "git", "Git", Path("/git"))
190
            for output, code in (("", 1), ("{}", 0), ("not json", 0),
191
                                 (json.dumps({"oracle_version": workflow_oracles.VERSION,
192
                                              "workflow_integrity_source": "trusted", "workflow_integrity_passed": None}), 77)):
193
                with mock.patch.object(agent_workflow, "run_command", return_value=subprocess.CompletedProcess([], code, output, "")), \
194
                     mock.patch.object(agent_workflow, "vcs_status", return_value=("", 0, 0)), \
195
                     mock.patch.object(agent_workflow, "commit_count", return_value=2):
196
                    result = agent_workflow.run_oracle(subject, workflow_ab.WORKFLOWS["wide_config_refactor"], repo, before, 1, artifacts)
197
                self.assertIsNone(result.evidence["workflow_integrity_passed"])
198
                self.assertEqual(result.evidence["workflow_integrity_unmeasured_reason"], "trusted_oracle_evidence_invalid_or_missing")
199
200
    def test_unavailable_required_check_does_not_hide_measured_failure(self):
201
        with tempfile.TemporaryDirectory() as tmp:
202
            repo = Path(tmp)
203
            workflow_ab.make_wide_config_fixture(repo)
204
            before = agent_workflow.workspace_manifest(repo)
205
            finish_configs(repo)
206
            evidence = {"oracle_version": workflow_oracles.VERSION, "workflow_integrity_source": "trusted",
207
                "workflow_integrity_passed": None, "workflow_integrity_unmeasured_reason": "missing"}
208
            with mock.patch.object(agent_workflow, "run_command", return_value=subprocess.CompletedProcess([], 77, json.dumps(evidence), "")), \
209
                 mock.patch.object(agent_workflow, "vcs_status", return_value=("dirty", 1, 0)), \
210
                 mock.patch.object(agent_workflow, "expected_change_ok", return_value=True), \
211
                 mock.patch.object(agent_workflow, "commit_count", return_value=2):
212
                result = agent_workflow.run_oracle(workflow_ab.Subject("git", "git", "Git", Path("/git")),
213
                    workflow_ab.WORKFLOWS["wide_config_refactor"], repo, before, 1, repo)
214
            self.assertIsNone(result.checks["required_checks_passed"])
215
            self.assertEqual(result.failure_reason, "final_vcs_clean")
216
217
    def _bugfix_oracle(self, repo, behavior):
218
        trusted = {"oracle_version": workflow_oracles.VERSION, "workflow_integrity_source": "trusted",
219
                   "workflow_integrity_evidence_class": "independent_committed_payload",
220
                   "workflow_integrity_passed": True, "workflow_integrity_failure_reason": None,
221
                   "workflow_integrity_unmeasured_reason": None}
222
        real_run = agent_workflow.run_command
223
        calls = []
224
        def fake(command, cwd, *args, **kwargs):
225
            calls.append(command)
226
            if len(calls) == 1:
227
                return subprocess.CompletedProcess(command, 0, json.dumps(trusted), "")
228
            return behavior(real_run, command, cwd, *args, **kwargs)
229
        with mock.patch.object(agent_workflow, "run_command", side_effect=fake), \
230
             mock.patch.object(agent_workflow, "vcs_status", return_value=("", 0, 0)), \
231
             mock.patch.object(agent_workflow, "expected_change_ok", return_value=True), \
232
             mock.patch.object(agent_workflow, "commit_count", return_value=2):
233
            return agent_workflow.run_oracle(workflow_ab.Subject("git", "git", "Git", Path("/git")),
234
                workflow_ab.WORKFLOWS["bugfix_test_loop"], repo, {}, 1, repo)
235
236
    def test_subject_controlled_exit_77_is_a_measured_failure_not_unmeasured(self):
237
        # Real producer: agent-edited test module exits 77 at import time.
238
        with tempfile.TemporaryDirectory() as tmp:
239
            repo = Path(tmp)
240
            (repo / "tests").mkdir()
241
            (repo / "tests" / "test_sentinel.py").write_text("import os\nos._exit(77)\n")
242
            result = self._bugfix_oracle(repo, lambda real, command, cwd, *a, **k: real(command, cwd, *a, **k))
243
            self.assertIs(result.checks["behavior_tests_passed"], False)
244
            self.assertEqual(result.failure_reason, "behavior_tests_passed")
245
            self.assertIs(result.evidence["workflow_integrity_passed"], True)
246
            self.assertIsNone(result.evidence["workflow_integrity_unmeasured_reason"])
247
248
    def test_behavior_interpreter_launch_failure_is_harness_unmeasured(self):
249
        def missing(real, command, cwd, *a, **k):
250
            raise FileNotFoundError("python3")
251
        with tempfile.TemporaryDirectory() as tmp:
252
            result = self._bugfix_oracle(Path(tmp), missing)
253
            self.assertIsNone(result.checks["behavior_tests_passed"])
254
            self.assertIsNone(result.evidence["workflow_integrity_passed"])
255
            self.assertEqual(result.evidence["workflow_integrity_unmeasured_reason"], "behavior_test_instrument_unavailable")
256
257
    def test_fixture_module_shadowing_is_not_executed(self):
258
        with tempfile.TemporaryDirectory() as tmp:
259
            repo = Path(tmp)
260
            workflow_ab.make_wide_config_fixture(repo)
261
            finish_configs(repo)
262
            (repo / "json.py").write_text("raise RuntimeError('untrusted import executed')")
263
            proc = agent_workflow.run_command(
264
                agent_workflow.test_command_for(workflow_ab.WORKFLOWS["wide_config_refactor"]), repo,
265
            )
266
            self.assertEqual(proc.returncode, 0, proc.stderr)
267
268
    def test_scripted_unavailable_instrument_row_persists(self):
269
        from oakbench.rows import validate_rows
270
        from oakbench.results import ResultsStore
271
        with tempfile.TemporaryDirectory() as tmp:
272
            repo = Path(tmp)
273
            workflow_ab.make_wide_config_fixture(repo)
274
            finish_configs(repo)
275
            subject = workflow_ab.Subject("git", "git", "Git", Path("/subject/not-reader"))
276
            with mock.patch.object(workflow_oracles, "resolve_git_instrument", return_value=(None, {})):
277
                evidence = workflow_ab.workflow_integrity_fields(subject, workflow_ab.WORKFLOWS["wide_config_refactor"], repo, [])
278
            row = workflow_ab.sum_rows([], {"bench_id": "test", "profile": "test"}, subject,
279
                workflow_ab.WORKFLOWS["wide_config_refactor"], 0, "workflow", "workflow.total", evidence)
280
            self.assertEqual(row["returncode"], 77)
281
            self.assertEqual(validate_rows([row], "workflow"), [])
282
            ResultsStore(repo / "results", "workflow").write("test", [row])
283
284
    def test_workflow_summary_fails_when_trusted_payload_fails(self):
285
        with tempfile.TemporaryDirectory() as tmp:
286
            repo = Path(tmp)
287
            workflow_ab.make_wide_config_fixture(repo)
288
            finish_configs(repo)
289
            (repo / "services/service-0003/config.toml").unlink()
290
            subject = workflow_ab.Subject(name="oak", kind="oak", label="oak", bin=Path("/oak"))
291
            fields = workflow_ab.workflow_integrity_fields(
292
                subject, workflow_ab.WORKFLOWS["wide_config_refactor"], repo, [],
293
            )
294
            self.assertFalse(fields["correctness_passed"])
295
            self.assertIn("config_path_set_mismatch", fields["correctness_failure_reason"])
296
            self.assertEqual(fields["oracle_version"], workflow_oracles.VERSION)
297
298
299
@unittest.skipIf(shutil.which("git") is None, "git not installed")
300
class CommittedWorkflowOracleTest(unittest.TestCase):
301
    def test_subject_binary_never_becomes_committed_tree_reader(self):
302
        with tempfile.TemporaryDirectory() as tmp:
303
            repo = Path(tmp)
304
            workflow_ab.make_wide_config_fixture(repo)
305
            finish_configs(repo)
306
            subject = workflow_ab.Subject("git", "git", "Git candidate", Path("/candidate/git"))
307
            with mock.patch.object(workflow_oracles, "resolve_git_instrument", return_value=(None, {})) as resolve:
308
                workflow_ab.workflow_integrity_fields(subject, workflow_ab.WORKFLOWS["wide_config_refactor"], repo, [])
309
                resolve.assert_called_once_with(None)
310
            command = agent_workflow.test_command_for(workflow_ab.WORKFLOWS["wide_config_refactor"], subject)
311
            self.assertEqual(command[command.index("--binary") + 1], "/candidate/git")
312
            self.assertNotIn("--git-instrument", command)
313
314
    def test_nontrusted_git_records_actual_scoped_head_evidence(self):
315
        with tempfile.TemporaryDirectory() as tmp:
316
            repo = Path(tmp)
317
            workflow_ab.make_bugfix_fixture(repo)
318
            path = repo / "app/pricing.py"
319
            path.write_text(path.read_text().replace("return cents - percent", "return cents - ((cents * percent) // 100)"))
320
            self.git(repo, "init"); self.git(repo, "add", "."); self.git(repo, "commit", "-m", "fixed")
321
            fields = workflow_ab.workflow_integrity_fields(workflow_ab.Subject("git", "git", "Git", Path(shutil.which("git"))),
322
                workflow_ab.WORKFLOWS["bugfix_test_loop"], repo, [])
323
            self.assertTrue(fields["workflow_integrity_passed"])
324
            self.assertEqual(fields["workflow_integrity_evidence_class"], "subject_binary_committed_payload")
325
            self.assertEqual(fields["workflow_subject_binary_sha256"], fields["git_head_instrument_sha256"])
326
327
    def test_task_evidence_resolves_instrument_once(self):
328
        with tempfile.TemporaryDirectory() as tmp:
329
            repo = Path(tmp)
330
            workflow_ab.make_wide_config_fixture(repo)
331
            finish_configs(repo)
332
            actual = workflow_oracles.resolve_git_instrument
333
            with mock.patch.object(workflow_oracles, "resolve_git_instrument", wraps=actual) as resolver:
334
                evidence = workflow_oracles.task_evidence("wide_config_refactor", repo, "git", shutil.which("git"))
335
            self.assertEqual(resolver.call_count, 1)
336
            self.assertFalse(evidence["workflow_integrity_passed"])
337
338
    def test_bound_subject_hashes_are_reused_without_binary_reads(self):
339
        for kind, metadata, expected in (
340
            ("oak", {"oak_export_instrument_sha256": "a" * 64}, "subject_produced_committed_tree"),
341
            ("git", {"workflow_subject_binary_sha256": "a" * 64, "git_head_instrument_sha256": "a" * 64}, "subject_binary_committed_tree"),
342
        ):
343
            with mock.patch.object(Path, "read_bytes", side_effect=AssertionError("redundant binary read")), \
344
                 mock.patch.object(workflow_oracles.shutil, "which", side_effect=AssertionError("redundant subject lookup")):
345
                result = workflow_oracles.reader_evidence(kind, "/subject", metadata)
346
            self.assertEqual(result["workflow_integrity_evidence_class"], expected)
347
348
    def test_killed_git_instrument_is_unmeasured(self):
349
        with mock.patch.object(workflow_oracles, "resolve_git_instrument", return_value=("/git", {})), \
350
             mock.patch.object(workflow_oracles.subprocess, "run", return_value=subprocess.CompletedProcess([], -9, b"", b"")):
351
            self.assertEqual(workflow_oracles.validate_git_head("wide_config_refactor", Path("/tmp")), "git_head_oracle_unavailable")
352
            self.assertEqual(workflow_oracles.git_head_text(Path("/tmp"), "x"), (None, "git_head_oracle_unavailable"))
353
354
    def test_path_instrument_and_missing_instrument_are_not_subject_failures(self):
355
        real_git = shutil.which("git")
356
        with tempfile.TemporaryDirectory() as tmp:
357
            repo = Path(tmp)
358
            workflow_ab.make_wide_config_fixture(repo)
359
            finish_configs(repo)
360
            with mock.patch.object(workflow_oracles.shutil, "which", return_value=None) as lookup:
361
                evidence = workflow_oracles.task_evidence("wide_config_refactor", repo, "git")
362
                lookup.assert_called_with("git")
363
                self.assertIsNone(evidence["workflow_integrity_passed"])
364
                self.assertEqual(evidence["workflow_integrity_unmeasured_reason"], "git_head_oracle_unavailable")
365
                self.assertIsNone(evidence["git_head_instrument_sha256"])
366
                self.assertEqual(evidence["workflow_integrity_evidence_class"], "worktree_payload")
367
            binary, metadata = workflow_oracles.resolve_git_instrument(real_git)
368
            self.assertTrue(Path(binary).is_absolute())
369
            self.assertEqual(len(metadata["git_head_instrument_sha256"]), 64)
370
            self.assertTrue(metadata["git_head_instrument_version"])
371
372
    @unittest.skipUnless(shutil.which("oak"), "Oak installed binary required")
373
    def test_correct_oak_worktree_cannot_hide_partial_checkpoint(self):
374
        binary = shutil.which("oak")
375
        with tempfile.TemporaryDirectory() as tmp:
376
            repo = Path(tmp)
377
            workflow_ab.make_wide_config_fixture(repo)
378
            def oak(*args):
379
                process = subprocess.run([binary, *args], cwd=repo, env=workflow_ab.base_env(), capture_output=True, timeout=60)
380
                self.assertEqual(process.returncode, 0, process.stderr)
381
            oak("init"); oak("commit", "--no-verify")
382
            finish_configs(repo)
383
            oak("commit", "--no-verify", "services/service-0000/config.toml")
384
            self.assertIsNone(workflow_oracles.validate("wide_config_refactor", repo))
385
            # Default Oak arm: the independent stdlib reader (no Oak binary).
386
            independent = workflow_oracles.task_evidence("wide_config_refactor", repo, "oak", binary)
387
            self.assertIs(independent["workflow_integrity_passed"], False, independent)
388
            self.assertEqual(independent["workflow_integrity_evidence_class"], "independent_committed_read_attempt")
389
            self.assertIn("config_payload_mismatch", independent["workflow_integrity_failure_reason"])
390
            # Explicit opt-in: the subject-produced export arm still works.
391
            with mock.patch.dict(os.environ, {"OAK_BENCH_OAK_COMMITTED_READER": "export"}):
392
                evidence = workflow_oracles.task_evidence("wide_config_refactor", repo, "oak", binary)
393
            self.assertIs(evidence["workflow_integrity_passed"], False, evidence)
394
            self.assertEqual(evidence["workflow_integrity_evidence_class"], "subject_produced_committed_tree_attempt")
395
            self.assertIn("git_head_config_payload_mismatch", evidence["workflow_integrity_failure_reason"])
396
            oak("commit", "--no-verify")
397
            with mock.patch.dict(os.environ, {"OAK_BENCH_OAK_COMMITTED_READER": "export"}):
398
                verified = workflow_oracles.task_evidence("wide_config_refactor", repo, "oak", binary)
399
            self.assertIs(verified["workflow_integrity_passed"], True, verified)
400
            self.assertNotEqual(evidence["oak_export_head"], verified["oak_export_head"])
401
            passed = workflow_oracles.task_evidence("wide_config_refactor", repo, "oak", binary)
402
            self.assertIs(passed["workflow_integrity_passed"], True, passed)
403
            self.assertEqual(passed["workflow_integrity_evidence_class"], "independent_committed_tree")
404
            self.assertEqual(passed["oak_reader_head"], verified["oak_export_head"])
405
406
    def git(self, repo, *args):
407
        result = subprocess.run(
408
            [shutil.which("git"), *args], cwd=repo, env=workflow_ab.base_env(),
409
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
410
        )
411
        self.assertEqual(result.returncode, 0, result.stderr)
412
413
    def test_correct_worktree_cannot_hide_partial_commit(self):
414
        for name, maker, finish, staged in (
415
            ("wide_config_refactor", workflow_ab.make_wide_config_fixture, finish_configs,
416
             "services/service-0000/config.toml"),
417
            ("large_asset_manifest", workflow_ab.make_large_asset_fixture, finish_assets, "manifest.json"),
418
        ):
419
            with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp:
420
                repo = Path(tmp)
421
                maker(repo)
422
                self.git(repo, "init")
423
                self.git(repo, "add", ".")
424
                if name == "large_asset_manifest":
425
                    self.git(repo, "rm", "--cached", "assets/asset-03.bin")
426
                self.git(repo, "commit", "-m", "initial")
427
                finish(repo)
428
                self.git(repo, "add", staged)
429
                self.git(repo, "commit", "-m", "partial task")
430
                self.assertIsNone(workflow_oracles.validate(name, repo))
431
                self.assertIsNotNone(workflow_oracles.validate_git_head(name, repo))
432
                subject = workflow_ab.Subject(name="git", kind="git", label="Git", bin=Path(shutil.which("git")))
433
                fields = workflow_ab.workflow_integrity_fields(subject, workflow_ab.WORKFLOWS[name], repo, [])
434
                self.assertFalse(fields["correctness_passed"])
435
                proc = agent_workflow.run_command(agent_workflow.test_command_for(workflow_ab.WORKFLOWS[name], subject), repo)
436
                self.assertEqual(proc.returncode, 1, proc.stderr)
437
                self.assertIn("git_head_", proc.stdout)
438
                self.git(repo, "add", ".")
439
                self.git(repo, "commit", "-m", "complete task")
440
                self.assertIsNone(workflow_oracles.validate_git_head(name, repo))
441
                proc = agent_workflow.run_command(agent_workflow.test_command_for(workflow_ab.WORKFLOWS[name], subject), repo)
442
                self.assertEqual(proc.returncode, 0, proc.stderr)
443
                observed = json.loads(proc.stdout)
444
                self.assertEqual(observed["workflow_integrity_evidence_class"], "subject_binary_committed_tree")
445
                self.assertEqual(observed["workflow_subject_binary_sha256"], observed["git_head_instrument_sha256"])
446
447
448
class RealSubjectWorkflowOracleTest(unittest.TestCase):
449
    def test_mock_agent_config_and_asset_tasks_on_real_vcs(self):
450
        for kind in ("git", "oak"):
451
            binary = shutil.which(kind)
452
            if binary is None:
453
                with self.subTest(kind=kind):
454
                    self.skipTest(f"{kind} not installed")
455
                continue
456
            for name in ("wide_config_refactor", "large_asset_manifest"):
457
                with self.subTest(kind=kind, name=name), tempfile.TemporaryDirectory() as tmp:
458
                    root = Path(tmp)
459
                    subject = workflow_ab.Subject(name=kind, kind=kind, label=kind, bin=Path(binary))
460
                    workflow = workflow_ab.WORKFLOWS[name]
461
                    fixture = workflow_ab.prepare_fixture(root, workflow)
462
                    row = agent_workflow.run_one(
463
                        agent=agent_workflow.AgentConfig(name="mock", adapter="deterministic_mock", provider="local",
464
                                                        bin=None, model=None, version="test"),
465
                        subject=subject, workflow=workflow, fixture=fixture, run_index=0,
466
                        run_root=root / "runs", artifacts_root=root / "artifacts", timeout_seconds=300,
467
                        track="agent-default", agent_environment="minimal", instruction_level="full-docs",
468
                        bench_id="oracle-integration", timestamp_utc="2026-09-10T00:00:00Z", source={}, host_meta={},
469
                    )
470
                    self.assertTrue(row["success"], row.get("failure_reason"))
471
                    self.assertEqual(agent_workflow.validate_row(row), [])
472
                    self.assertIn(workflow_oracles.VERSION, row["scenario_version"])
473
                    self.assertEqual(row["oracle_version"], workflow_oracles.VERSION)
474
                    self.assertTrue(row["workflow_integrity_passed"])
475
                    if kind == "git":
476
                        self.assertIn("git_head_instrument_sha256", row)
477
                    else:  # default Oak arm: independent stdlib reader, not the subject's export
478
                        self.assertEqual(row["workflow_integrity_evidence_class"], "independent_committed_tree")
479
                        self.assertRegex(row["oak_reader_head"], "^[0-9a-f]{64}$")
480
                    from oakbench.populations import comparison_identity
481
                    identity = comparison_identity(row)
482
                    self.assertEqual(identity["oracle_version"], workflow_oracles.VERSION)
483
                    self.assertEqual(identity["env_isolation_version"], agent_workflow.ENV_ISOLATION_VERSION)