feat(experement): define reference experiment contract without tightening

This commit is contained in:
Даниил Грабарь
2026-11-08 13:14:23 +10:00
parent cdae71e2bc
commit a59a3f75b2
8 changed files with 237 additions and 87 deletions
@@ -24,16 +24,21 @@ def test_package_declares_its_ros_dependencies():
assert f'<exec_depend>{dependency}</exec_depend>' in manifest
def test_versioned_specifications_match_the_approved_copies():
def test_package_documentation_points_at_the_reference_specification():
root = PACKAGE.parents[1]
pairs = (
('docs/superpowers/specs/2026-11-08-grant-experiments.md', 'doc/experements.md'),
('docs/superpowers/specs/2026-11-08-grant-scenes.md', 'doc/experiment-scenes.md'),
reference = (
root
/ 'docs'
/ 'superpowers'
/ 'specs'
/ '2026-11-08-grant-reference-control-design.md'
)
for source, installed in pairs:
assert (PACKAGE / installed).read_text(encoding='utf-8') == (
root / source
).read_text(encoding='utf-8')
assert reference.is_file()
for relative in ('doc/experements.md', 'doc/experiment-scenes.md'):
text = (PACKAGE / relative).read_text(encoding='utf-8')
assert '2026-11-08-grant-reference-control-design.md' in text
assert 'tighten' not in text.lower() or 'не затяг' in text.lower()
assert 'seat_screw' not in text
def test_expected_resources_are_installed():
+73 -10
View File
@@ -88,18 +88,52 @@ def test_screw_experiments_have_installed_hatch_and_ten_screws():
assert spec.cap_state == 'installed'
assert spec.tray_pose is not None
assert len(spec.screws) == 10
assert spec.tool_names[0] == 'magnetic_tool'
assert set(scenario(14).tool_names) == {'magnetic_tool', 'torque_tool'}
assert set(scenario(15).tool_names) == {'magnetic_tool', 'torque_tool'}
assert spec.tool_names == ('magnetic_tool',)
assert 'torque_tool' not in spec.tool_names
assert len(spec.screw_holes) == {
11: 1, 12: 2, 13: 3, 14: 4, 15: 4,
}[experiment_id]
def test_full_assembly_experiments_have_loose_hatch_tray_and_all_tools():
def test_full_assembly_experiments_have_loose_hatch_tray_and_no_torque_tool():
for experiment_id in range(16, 21):
spec = scenario(experiment_id)
assert spec.cap_state == 'free'
assert spec.tray_pose is not None
assert len(spec.screws) == 10
assert set(spec.tool_names) == STANDARD_TOOLS
assert set(spec.tool_names) == {'vacuum', 'magnetic_tool', 'sealant_tool'}
assert 'torque_tool' not in spec.tool_names
assert len(spec.screw_holes) == {16: 1, 17: 2, 18: 4, 19: 4, 20: 4}[experiment_id]
def test_sealant_coverage_matches_each_experiment():
assert scenario(6).sealant_sides == ('side_1',)
assert scenario(7).sealant_sides == ('side_1', 'side_2')
for experiment_id in (8, 9, 10, 17, 18, 19, 20):
assert scenario(experiment_id).sealant_sides == (
'side_1', 'side_2', 'side_3', 'side_4',
)
assert scenario(16).sealant_sides == ('side_1',)
for experiment_id in (1, 2, 3, 4, 5, 11, 12, 13, 14, 15):
assert scenario(experiment_id).sealant_sides == ()
def test_screw_holes_are_unique_bounded_and_ordered():
for experiment_id in range(11, 21):
holes = scenario(experiment_id).screw_holes
assert len(holes) == len(set(holes))
assert set(holes) <= {'hole_1', 'hole_2', 'hole_3', 'hole_4'}
assert scenario(14).screw_holes == ('hole_1', 'hole_3', 'hole_2', 'hole_4')
assert scenario(15).screw_holes == ('hole_1', 'hole_3', 'hole_2', 'hole_4')
def test_reference_experiments_never_request_tightening():
for experiment_id in range(1, 21):
spec = scenario(experiment_id)
assert all(
failure.skill not in ('tighten', 'seat_screw')
for failure in spec.failures
)
def test_standard_screw_layout_matches_the_specification():
@@ -162,13 +196,12 @@ def test_failure_metadata_for_experiments_four_nine_fifteen_twenty():
fifteen = [(f.skill, f.outcome, f.target) for f in scenario(15).failures]
assert fifteen == [
('grab', 'grasp_failed', 'third_selected_screw'),
('tighten', 'torque_not_reached', 'hole_4'),
]
twenty = [(f.skill, f.outcome) for f in scenario(20).failures]
twenty = [(f.skill, f.outcome, f.target) for f in scenario(20).failures]
assert twenty == [
('dispense', 'interrupted'),
('grab', 'grasp_failed'),
('verify', 'not_ok'),
('dispense', 'interrupted', 'side_3'),
('grab', 'grasp_failed', 'first_selected_screw'),
('verify', 'not_ok', 'cap'),
]
@@ -211,6 +244,8 @@ def test_incorrect_screw_count_is_rejected(tmp_path, count):
def mutate(data):
data['experiments']['1']['tray'] = True
data['experiments']['1']['tools'] = ['magnetic_tool']
data['experiments']['1']['screw_holes'] = ['hole_1']
data['experiments']['1']['screws'] = entries
with pytest.raises(ScenarioError, match='exactly 10 screws'):
@@ -225,6 +260,8 @@ def test_duplicate_screw_name_is_rejected(tmp_path):
def mutate(data):
data['experiments']['1']['tray'] = True
data['experiments']['1']['tools'] = ['magnetic_tool']
data['experiments']['1']['screw_holes'] = ['hole_1']
data['experiments']['1']['screws'] = entries
with pytest.raises(ScenarioError, match='duplicate screw name'):
@@ -240,6 +277,8 @@ def test_screw_outside_the_tray_is_rejected(tmp_path):
def mutate(data):
data['experiments']['1']['tray'] = True
data['experiments']['1']['tools'] = ['magnetic_tool']
data['experiments']['1']['screw_holes'] = ['hole_1']
data['experiments']['1']['screws'] = entries
with pytest.raises(ScenarioError, match='outside the tray'):
@@ -279,3 +318,27 @@ def test_tray_without_screws_and_screws_without_tray_are_rejected(tmp_path):
def test_missing_manifest_is_reported(tmp_path):
with pytest.raises(FileNotFoundError):
load_scenario(1, tmp_path / 'missing.yaml')
def test_invalid_sealant_side_is_rejected(tmp_path):
def mutate(data):
data['experiments']['6']['sealant'] = ['side_5']
with pytest.raises(ScenarioError, match='sealant'):
load_scenario(6, variant(tmp_path, mutate))
def test_invalid_screw_hole_is_rejected(tmp_path):
def mutate(data):
data['experiments']['11']['screw_holes'] = ['hole_5']
with pytest.raises(ScenarioError, match='screw_holes'):
load_scenario(11, variant(tmp_path, mutate))
def test_tray_without_screw_holes_is_rejected(tmp_path):
def mutate(data):
data['experiments']['11'].pop('screw_holes')
with pytest.raises(ScenarioError, match='requires screw_holes'):
load_scenario(11, variant(tmp_path, mutate))
@@ -74,13 +74,13 @@ def test_scenarios_one_six_eleven_sixteen_nineteen_are_complete():
16: [
'grant_fixture', 'grant_body', 'grant_rack',
'grant_vacuum', 'grant_magnetic_tool', 'grant_sealant_tool',
'grant_torque_tool', 'grant_tray',
'grant_tray',
*[f'grant_screw_{i:02d}' for i in range(1, 11)], 'grant_cap',
],
19: [
'grant_fixture', 'grant_body', 'grant_rack',
'grant_vacuum', 'grant_magnetic_tool', 'grant_sealant_tool',
'grant_torque_tool', 'grant_tray',
'grant_tray',
*[f'grant_screw_{i:02d}' for i in range(1, 11)], 'grant_cap',
],
}
@@ -149,9 +149,10 @@ def test_working_tools_are_in_their_own_sockets():
(tool.pose.x, tool.pose.y, tool.pose.z)
)
positions = {
translation(parsed[TOOL_NODE_NAMES[name]]) for name in TOOL_NAMES
translation(parsed[TOOL_NODE_NAMES[name]]) for name in scenario.tool_names
}
assert len(positions) == 4
assert len(positions) == len(scenario.tool_names) == 3
assert 'grant_torque_tool' not in parsed
def test_experiment_nineteen_shifts_the_tray_and_all_ten_screws():