fix: retain planning feedback and add acceptance probes
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user