98 lines
5.2 KiB
Python
98 lines
5.2 KiB
Python
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'<root/>')
|
|
first = capture_sources(root)
|
|
self.assertEqual(first['xml']['sha256'], hashlib.sha256(b'<root/>').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'<changed/>')
|
|
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()
|