Viewing files at commit ada5e175e5bfCommit detailsView latest

Browse files
tests/test_workflow_publication_qualification.py 152 lines · 9.1 KB · python
1
import copy
2
from pathlib import Path
3
import sys
4
import unittest
5
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
6
from oakbench.public_output import workflow_evidence_failures, workflow_qualification_failures
7
from oakbench.populations import comparison_identity, comparison_conflicts
8
from oakbench.workflow_oracles import VERSION
9
10
11
def rows():
12
    return [dict(subject=subject, scenario='bugfix_test_loop', operation='workflow.total', run=run,
13
        bench_id='study', returncode=0, workflow_integrity_passed=True, oracle_version=VERSION,
14
        workflow_integrity_evidence_class='independent_committed_payload', workflow_integrity_source='git_committed_payload',
15
        git_head_instrument_sha256='a'*64, workflow_subject_binary_sha256='b'*64,
16
        env_isolation_version='3', randomized_subject_order=True, subject_order=['git','oak'], benchmark_track='agent-default')
17
        for subject in ('git','oak') for run in range(30)]
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 WorkflowPublicationQualificationTest(unittest.TestCase):
40
    def test_qualified_observations_are_allowed(self):
41
        self.assertEqual(workflow_qualification_failures(rows()), [])
42
        self.assertEqual(workflow_evidence_failures(rows()), [])
43
44
    def test_thin_duplicate_missing_and_mixed_environment_block(self):
45
        for data in (rows()[:1], rows()+[rows()[0]], [{**row,'env_isolation_version': None} for row in rows()],
46
                     [{**row,'randomized_subject_order':False} for row in rows()],
47
                     [rows()[0], {**rows()[1], 'env_isolation_version':'2'}]):
48
            self.assertTrue(workflow_qualification_failures(data))
49
50
    def test_different_conditions_cannot_disappear_into_singletons(self):
51
        for changes in ({'benchmark_track':'core-equivalent'}, {'instruction_level':'zero-shot'},
52
                        {'agent':{'name':'x','model':'different'}},
53
                        {'workflow_integrity_evidence_class':'subject_produced_committed_payload'}):
54
            data=rows()
55
            for row in data:
56
                if row['subject']=='oak': row.update(changes)
57
            self.assertTrue(workflow_evidence_failures(data),changes)
58
59
    def test_comparison_identity_includes_correctness_class(self):
60
        left=rows()[0]
61
        right={**left,'workflow_integrity_evidence_class':'subject_produced_committed_payload'}
62
        self.assertIn('workflow_integrity_evidence_class',comparison_conflicts(comparison_identity(left),comparison_identity(right)))
63
64
    def test_failed_agent_trials_keep_failure_denominator(self):
65
        data=rows()
66
        for row in data:
67
            row.update(operation='agent.workflow', agent_launched=True,outcome_attribution='agent',success=True,outcome='pass')
68
        data[-1].update(workflow_integrity_passed=False,workflow_integrity_evidence_class='worktree_payload',
69
                        success=False,outcome='fail',returncode=1)
70
        self.assertEqual(workflow_evidence_failures(data),[])
71
        self.assertEqual(workflow_qualification_failures(data),[])
72
        data[-1]['outcome_attribution']='harness'
73
        self.assertTrue(workflow_evidence_failures(data))
74
75
class RealMixedOutcomePublicationTest(unittest.TestCase):
76
    def test_real_producers_preserve_failed_arm_in_publication_and_pair(self):
77
        import contextlib
78
        import io
79
        import json
80
        import os
81
        import shutil
82
        import subprocess
83
        import tempfile
84
        from unittest import mock
85
        import agent_workflow as aw
86
        import workflow_ab
87
        import publish_gate
88
        from oakbench.campaign_report import report_campaign
89
        if not os.environ.get('OAK_BENCH_OAK_READER_PYTHON'):
90
            self.skipTest('configure independent reader Python to exercise real decoder')
91
        with tempfile.TemporaryDirectory() as tmp:
92
            root=Path(tmp); fixture=root/'fixture'
93
            workflow_ab.make_bugfix_fixture(fixture)
94
            agent=aw.AgentConfig('mock','deterministic_mock','local',None,None,'1')
95
            original=aw.run_mock_agent
96
            def wrong_agent(subject, workflow, repo, *args, **kwargs):
97
                result=original(subject, workflow, repo, *args, **kwargs)
98
                (repo/'app/pricing.py').write_text('def apply_discount(cents, percent):\n    return cents\n')
99
                for command in ([str(subject.bin),'add','.'],[str(subject.bin),'commit','-m','incorrect answer']):
100
                    subprocess.run(command,cwd=repo,env=workflow_ab.base_env(),check=True,capture_output=True)
101
                return result
102
            observations=[]
103
            for kind in ('git','oak'):
104
                subject=workflow_ab.Subject(kind,kind,kind,Path(shutil.which(kind)))
105
                with mock.patch.object(aw,'run_mock_agent',side_effect=wrong_agent if kind=='git' else original):
106
                    row=aw.run_one(agent,subject,workflow_ab.WORKFLOWS['bugfix_test_loop'],fixture,0,
107
                        root/'work',root/'artifacts',60,'agent-default','minimal','full-docs','study','2026-09-18T00:00:00Z',
108
                        {'oak_hash':'test-binary-provenance','oak_status':'clean'},
109
                        {'randomized_subject_order':True,'subject_order':['git','oak']})
110
                observations.append(row)
111
            self.assertEqual([r['success'] for r in observations],[False,True])
112
            self.assertEqual(workflow_evidence_failures(observations),[])
113
            plan={'schema_version':1,'campaign_id':'study','plan_sha256':'bound','manifest':{},'schedule':[]}
114
            events=[]
115
            for index,row in enumerate(observations):
116
                trial_id=str(index); row['task_id']=trial_id
117
                plan['schedule'].append(dict(trial_id=trial_id,block_id='pair',ordinal=index,repetition=0,
118
                    workflow='bugfix_test_loop',instruction_level='full-docs',agent='mock',model=None,subject=row['subject']))
119
                events.extend([dict(event='started',trial_id=trial_id,plan_sha256='bound'),
120
                               dict(event='finished',trial_id=trial_id,plan_sha256='bound',outcome=row['outcome'],row=row)])
121
            report=report_campaign(plan,events,bootstrap_samples=20)
122
            comparison=report['groups'][0]['paired_comparisons'][0]
123
            self.assertEqual(comparison['complete_pairs'],1)
124
            self.assertEqual(comparison['success_delta_percentage_points'],100.0)
125
            # Repeated IDs below test the gate's mechanics only: not 30 measured trials.
126
            gate_rows=[{**row,'run':rep,'task_id':f'{rep}-{row["subject"]}'} for rep in range(30) for row in observations]
127
            path=root/'rows.jsonl';path.write_text('\n'.join(map(json.dumps,gate_rows)))
128
            output=io.StringIO()
129
            with contextlib.redirect_stdout(output): result=publish_gate.main([str(path)])
130
            self.assertEqual(result,0,output.getvalue())
131
            candidate=Path('/Applications/Xcode.app/Contents/Developer/usr/bin/git')
132
            instrument=Path('/Library/Developer/CommandLineTools/usr/bin/git')
133
            if candidate.exists() and instrument.exists():
134
                # Distinct installed vendor builds test configured identity plumbing,
135
                # not independent Git implementation or statistical power.
136
                with mock.patch.dict(os.environ, {'OAK_BENCH_GIT_INSTRUMENT': str(instrument)}):
137
                    passed_git=aw.run_one(agent,workflow_ab.Subject('git','git','Git candidate',candidate),
138
                        workflow_ab.WORKFLOWS['bugfix_test_loop'],fixture,0,root/'success-work',root/'success-artifacts',
139
                        60,'agent-default','minimal','full-docs','study','2026-09-18T00:00:00Z',
140
                        {'oak_hash':'test-binary-provenance','oak_status':'clean'},
141
                        {'randomized_subject_order':True,'subject_order':['git','oak']})
142
                self.assertTrue(passed_git['success'],passed_git.get('failure_reason'))
143
                self.assertEqual(passed_git['workflow_integrity_evidence_class'],'independent_committed_payload')
144
                self.assertNotEqual(passed_git['workflow_subject_binary_sha256'],passed_git['git_head_instrument_sha256'])
145
                successful_pair=[passed_git, observations[1]]
146
                self.assertEqual(workflow_evidence_failures(successful_pair),[])
147
                path.write_text('\n'.join(json.dumps({**row,'run':rep,'task_id':f'{rep}-{row["subject"]}'})
148
                                          for rep in range(30) for row in successful_pair))
149
                output=io.StringIO()
150
                with contextlib.redirect_stdout(output): result=publish_gate.main([str(path)])
151
                self.assertEqual(result,0,output.getvalue())
152