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()