# -*- coding: utf-8 -*- """The handling stress limit, from the window down to the job file. `handling.tension_limit_sqrt_fc` multiplies sqrt(f'c) to give the tensile allowable in psi, and there are two defensible answers: 5.0 -- the modulus of rupture over a 1.5 safety factor, "no discernible cracking" -- and 7.5, the rupture modulus itself, first cracking with no margin. They are a factor of 1.5 apart on EVERY stress verdict, which is the largest single source of disagreement between this tool and a commercial check of the same panel. The owner would not pick one for everybody, 2026-08-30: "应力极限写入 projectconfig 里面的 lifting&handling 板块里面去,让用户自己 填写。I can see it both ways." So it moved OUT of `configform.NOT_SHOWN` and INTO the lifting & handling module as a number box. What is tested here is the half that makes that honest: the key a person types into is the key `liftexport` reads, and the value reaches every stage of the job file panellift is handed. A field that writes a key nothing reads is the drift `tools/knowledge_drift.py` exists to catch, and it has caught it before. """ from vibe import configform, configmodel, knowledge, liftexport from test_liftexport import FACE_AND_EDGE_LIFTERS, make_job, make_know KEY = 'handling.tension_limit_sqrt_fc' # ------------------------------------------------------- it is in the window def test_the_stress_limit_is_a_field_in_lifting_and_handling(): """The owner's ruling, as a placement. It used to be in NOT_SHOWN with the reason 'a stress limit is argued for once and then left alone' -- which was an argument for having a doctrine, and he declined to have one.""" assert configform.MODULE_OF[KEY] == configform.LIFTING assert KEY not in configform.NOT_SHOWN def test_it_is_a_box_you_type_a_number_into(): field = [f for f in configform.settings_fields() if f['key'] == KEY][0] assert field['kind'] == 'number' ok, value, _why = configform.parse(field, '7.5') assert ok and value == 7.5 def test_the_row_note_puts_both_reference_values_in_front_of_the_typist(): """A box that says 'stress limit' and nothing else asks a person to remember which two numbers are argued about. The whole reason this is a field rather than a doctrine is that the choice is the engineer's, so the window has to show what is being chosen between.""" field = [f for f in configform.settings_fields() if f['key'] == KEY][0] note = field['note'] assert '5.0' in note and '7.5' in note assert "sqrt(f'c)" in note # what the number multiplies assert 'psi' in note assert 'cracking' in note # and what each end of it means def test_the_full_view_says_where_it_is_edited_now(): """`configpage` reads `placement()`, so the two halves of the answer come from one table. Before the move this key was absent from it and the page said 'the file'.""" assert configform.placement()[KEY] == 'Lifting and handling' def test_nothing_fell_through_the_move(): """The guard that makes the not-shown pool honest: every editable setting is in a module or has a written reason for not being.""" assert configform.unmapped() == [] assert len(configform.not_shown_rows()) >= 8 assert KEY not in [key for _label, key, _why in configform.not_shown_rows()] # ------------------------------------------------------------ what it refuses def test_a_zero_or_negative_limit_is_refused(): """The only refusal, and it is not an opinion: a zero allowable fails every piece at any stress, and a negative one inverts the comparison so that every piece passes.""" assert configmodel.validate(KEY, 0) assert configmodel.validate(KEY, -5.0) assert 'greater than zero' in ' '.join(configmodel.validate(KEY, 0)) def test_both_schools_are_accepted_without_argument(): """5.0 and 7.5 are each somebody's defensible position. The validator does not get to have a doctrine the owner declined to have.""" assert configmodel.validate(KEY, 5.0) == [] assert configmodel.validate(KEY, 7.5) == [] def test_the_extremes_are_warned_about_and_still_allowed(): """Above 7.5 and below 3 are worth saying something about; neither is refused, because a refusal here would be the tool taking the position the owner left to the engineer.""" assert configmodel.validate(KEY, 9.0) == [] assert configmodel.validate(KEY, 2.0) == [] high = ' '.join(configmodel.warnings(KEY, 9.0)) low = ' '.join(configmodel.warnings(KEY, 2.0)) assert 'rupture' in high and high assert 'conservative' in low and low def test_seven_point_five_says_what_it_is(): said = ' '.join(configmodel.warnings(KEY, 7.5)) assert 'first cracking' in said and 'no margin' in said def test_the_ordinary_values_say_nothing(): """A warning on every value is a warning nobody reads.""" assert configmodel.warnings(KEY, 5.0) == [] assert configmodel.warnings(KEY, 6.0) == [] assert configmodel.warnings(KEY, 3.5) == [] # ------------------------------------------------- it reaches the calculation def test_the_default_is_still_five(): """Byte-identity when nobody sets it. Every job configured today has no override, and this is the number they have all been running.""" assert knowledge.get(knowledge.DEFAULTS, KEY) == 5.0 def test_the_job_layer_value_lands_on_every_stage_of_the_job_file(): """The chain, as one assertion: knowledge -> default_stages -> the stages panellift is handed. This is the link that makes the field real rather than a knob nothing turns.""" know = make_know() know['handling']['tension_limit_sqrt_fc'] = 7.5 stages, _rows, _problems = liftexport.default_stages( make_job(FACE_AND_EDGE_LIFTERS), know) assert stages assert all(s['tension_limit_sqrt_fc'] == 7.5 for s in stages) def test_an_unset_limit_writes_the_five_it_always_wrote(): stages, _rows, _problems = liftexport.default_stages( make_job(FACE_AND_EDGE_LIFTERS), make_know()) assert all(s['tension_limit_sqrt_fc'] == 5.0 for s in stages) assert all(s['tension_limit_source'] == 'job' for s in stages) def test_a_stage_that_states_its_own_limit_is_marked_as_the_override(): """A `handling.stages` entry beats the job-wide field. The engineer who typed 5.0 into Project Config and reads 7.5 on the report has to be able to see why on the page rather than by opening the YAML.""" know = make_know() know['handling']['tension_limit_sqrt_fc'] = 5.0 know['handling']['stages'][0]['tension_limit_sqrt_fc'] = 7.5 stages, _rows, _problems = liftexport.default_stages( make_job(FACE_AND_EDGE_LIFTERS), know) assert stages[0]['tension_limit_sqrt_fc'] == 7.5 assert stages[0]['tension_limit_source'] == 'stage' assert stages[1]['tension_limit_sqrt_fc'] == 5.0 assert stages[1]['tension_limit_source'] == 'job' def test_the_provenance_words_are_the_ones_panellift_reads(): """Written on one side, refused on the other if they disagree. panellift raises on an unknown word rather than printing it, so the two lists have to be the same list.""" stages, _rows, _problems = liftexport.default_stages( make_job(FACE_AND_EDGE_LIFTERS), make_know()) assert set(s['tension_limit_source'] for s in stages) <= \ set(['job', 'stage', 'default']) # ------------------------------------------------- and the warning is spoken def _button_source(): import io from test_configform import SCRIPT return io.open(SCRIPT, encoding='utf-8').read() def test_the_window_actually_calls_warnings(): """`configmodel.warnings()` had no caller outside the test suite -- every sentence in it was written and never shown. This setting is the reason it needs one: 5.0 and 7.5 are both defensible so neither may be refused, and the only thing left to do is say what the number means before it is written.""" source = _button_source() assert 'configmodel.warnings(' in source def test_a_warning_is_not_dressed_as_a_refusal(): """The two read the same on screen and mean opposite things. The window paints the line so it gets read and words it so it is not mistaken for a rejection, and it survives the window closing into the summary.""" source = _button_source() assert 'ALLOWED, WITH A NOTE' in source assert 'Allowed, with a note' in source assert 'self.cautioned' in source