import hashlib import json from pathlib import Path import sys import tempfile import subprocess import unittest sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'coordinator')) from robot_bt_coordinator.provenance import capture_sources, execution_versions import test_coordinator as fixture class ProvenanceTest(unittest.TestCase): def test_git_commit_and_dirty_are_measured(self): with tempfile.TemporaryDirectory() as tmp: subprocess.run(['git', 'init', '-q', tmp], check=True) subprocess.run(['git', '-C', tmp, '-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', 'commit', '--allow-empty', '-qm', 'fixture'], check=True) clean = capture_sources(tmp)['code'] self.assertEqual(clean['status'], 'captured') self.assertEqual(len(clean['commit']), 40) self.assertFalse(clean['dirty']) Path(tmp, 'changed.py').write_text('changed') dirty = capture_sources(tmp)['code'] self.assertTrue(dirty['dirty']) self.assertEqual(clean['commit'], dirty['commit']) def test_actual_planner_prompt_is_hashed_in_linked_record(self): sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'robobrain')) from robot_robobrain.service import BrainService, PLANNER_RULES from robot_robobrain.backends import FixtureBackend from robot_bt_coordinator.plan import canonical from test_robobrain import RoboBrainTests requests = [] def infer(request): requests.append(request) return '{"missing_information":["which destination?"]}' with tempfile.TemporaryDirectory() as tmp: result = BrainService(FixtureBackend(infer), tmp).plan(RoboBrainTests().goal()) record = json.loads(Path(result['record_ref']).read_text()) self.assertEqual(record['prompt_provenance']['sha256'], hashlib.sha256(requests[0]['prompt'].encode()).hexdigest()) self.assertEqual(record['prompt_provenance']['template_sha256'], hashlib.sha256(PLANNER_RULES.encode()).hexdigest()) self.assertEqual(requests[0]['prompt'], PLANNER_RULES + '\nINPUT: ' + canonical(record['input'])) def test_source_hash_changes_and_missing_is_explicit(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) xml = root / 'ros2/bt_executor/trees/fixed_workflow.xml' xml.parent.mkdir(parents=True) xml.write_bytes(b'') first = capture_sources(root) self.assertEqual(first['xml']['sha256'], hashlib.sha256(b'').hexdigest()) self.assertEqual(first['xml']['scope'], 'source_only') self.assertEqual(first['idl']['status'], 'missing') self.assertEqual(first['code']['status'], 'missing') source = root / 'coordinator/example.py' source.parent.mkdir() source.write_text('version_one = True') before = capture_sources(root)['code_content'] source.write_text('version_two = True') self.assertNotEqual(before['sha256'], capture_sources(root)['code_content']['sha256']) xml.write_bytes(b'') self.assertNotEqual(first['xml'], capture_sources(root)['xml']) def test_actual_configuration_hash_is_order_independent_and_sensitive(self): task = {'context': {'a': 1, 'b': 2}, 'execution_plan': {'schema_version': 1}, 'request': {'instruction': 'move water'}, 'planning_record_ref': '/record/1'} first = execution_versions({}, task, {'timeout': 5, 'recovery_token': 'secret'}) task['context'] = {'b': 2, 'a': 1} self.assertEqual(first, execution_versions({}, task, {'recovery_token': 'secret', 'timeout': 5})) task['context']['a'] = 3 self.assertNotEqual(first['runtime_config'], execution_versions({}, task, {})['runtime_config']) self.assertNotIn('secret', json.dumps(first)) self.assertEqual(first['deployed_executor']['status'], 'missing') self.assertEqual(first['instruction_template']['status'], 'missing') self.assertEqual(first['planning_record_ref'], '/record/1') class DispatchProvenanceTest(unittest.TestCase): setUp = fixture.CoordinatorTest.setUp tearDown = fixture.CoordinatorTest.tearDown plan = fixture.CoordinatorTest.plan def test_dispatch_identity_survives_database_reopen(self): tid = self.c.submit(fixture.REQ)['task_id'] self.plan(tid) events = self.c.store.events(tid, 0, 100) versions = next(e['versions'] for e in events if e['kind'] == 'execution_dispatched') self.assertEqual(versions['runtime_config']['status'], 'captured') self.assertEqual(len(versions['sources']['xml']['sha256']), 64) self.assertIn(versions['sources']['code']['status'], ('captured', 'missing')) import sqlite3 with sqlite3.connect(self.db) as connection: persisted = json.loads(connection.execute("SELECT data FROM events WHERE kind='execution_dispatched'").fetchone()[0]) self.assertEqual(versions, persisted['versions']) if __name__ == '__main__': unittest.main()