Files
willtools-demo/WillTools.extension/tests/test_ticketvision.py
T
will 52d94303bc
ci / test (push) Has been cancelled
WillTools: precast shop-ticket QC and panel handling tools
Public demo snapshot of an in-house pyRevit extension for precast concrete
shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel
handling solver (PCI Chapter 8).

- No client, job, plant or product data: knowledge layers ship as
  structural templates only.
- Ticket QC's model-read escalation targets a LAN model server endpoint
  (configurable), not a cloud provider.
- Built from the private development repo via tools/make_dist.py.
2026-09-03 16:33:47 -05:00

1069 lines
48 KiB
Python

# -*- coding: utf-8 -*-
"""The vision escalation, tested where testing is honest.
WHAT IS NOT TESTED HERE, AND WHY IT CANNOT BE
----------------------------------------------
**The verdict.** The same sheet, rendered the same way, asked the same
question, may come back `clear` on one run and `cannot_tell` on the next.
That is not a bug to be pinned down; it is what a model reading a picture is.
Nothing in this file asserts that a particular drawing gets a particular
answer, and nothing ever should -- a test that pinned one would be asserting
that a coin lands heads.
What IS testable is everything around it, and all of it matters more than the
verdict does:
* a refusal renders as NOT RUN, never as a pass
* failing to reach the model at all renders as NOT RUN, never as a pass
* every outcome, including the good one, carries its provenance
* a model-read finding cannot occupy or flip a computed checklist row
* the two kinds of finding never end up in one list, one table or one key
* the resolution arithmetic refuses a render nobody could read
Those are the properties the honesty of the report rests on, and they are
deterministic. Which is the whole argument for building it this way.
"""
import importlib.util
import json
import os
import pytest
from vibe import ticketvision as TV
from vibe import ticketpage as TP
from vibe import configmodel
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))))
def _vision_read():
"""`tools/vision_read.py` is CPython 3.12 and lives outside the package,
so it is loaded by path -- the same boundary the extension crosses at
runtime, and the reason neither half imports the other."""
path = os.path.join(ROOT, 'tools', 'vision_read.py')
spec = importlib.util.spec_from_file_location('vision_read', path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _result(**kw):
row = {'id': 'X|5.5|A VIEW', 'question_id': 'BARMARK-LEGIBLE',
'code': 'AI-BARMARK', 'status': 'OK', 'answer': 'clear',
'why': None, 'where': None, 'reason': None}
row.update(kw)
return row
# ------------------------------------------------------- the skill is a file
def test_the_skill_is_a_file_versioned_with_the_code():
"""Not a string in a function. A prompt buried in a function is a prompt
nobody reviews, and this one is the only thing between a small model and
a confident wrong answer about a piece of concrete."""
path = TV.skill_path()
assert os.path.isfile(path), path
with open(path, encoding='utf-8') as fh:
text = fh.read()
# The refusal has to be first-class in the text, not an afterthought.
assert 'cannot_tell' in text
assert 'is not a failure' in text
for forbidden in ('Never guess a character',
'Never paraphrase',
'Never score',
'Never infer from absence'):
assert forbidden in text, forbidden
def test_the_skill_forbids_repeating_note_content():
"""Checklist 8bis: the report may say what it could not judge and that
someone should look. It may not paraphrase the content."""
with open(TV.skill_path(), encoding='utf-8') as fh:
text = fh.read()
assert 'You may not say what it says.' in text
# ------------------------------------------------------------- resolution
def test_a_small_view_renders_well_above_the_floor():
plan = TV.render_plan(6.0, 4.0)
assert plan['ok'] is True
assert plan['pixels'] == 1500
assert plan['effective_dpi'] == pytest.approx(250.0)
def test_a_whole_sheet_is_refused_rather_than_rendered_illegibly():
"""A 34 x 22 in sheet at the largest size the model can receive is 44 dpi.
The QNA evidence says a stacked fraction is not separable there, so the
honest outcome is NOT RUN with the arithmetic in the reason -- not a
question asked of an image nobody could read."""
plan = TV.render_plan(34.0, 22.0)
assert plan['ok'] is False
assert '44 dpi' in plan['reason']
assert 'downscaled' in plan['reason']
def test_the_render_stays_under_the_ceiling_the_api_imposes():
"""Bigger than 1568 px on the long edge is discarded on arrival, so a
larger render is latency spent on pixels the model never sees."""
assert TV.LONG_EDGE_PX <= 1568
def test_an_unmeasurable_view_is_not_rendered_and_says_so():
plan = TV.render_plan(None, None)
assert plan['ok'] is False
assert 'could not be measured' in plan['reason']
# ------------------------------------------- a refusal is NOT RUN, not a pass
def test_a_model_refusal_is_never_a_pass():
row = TV.verdict_row(_result(answer='cannot_tell',
why='the callouts are four pixels tall'),
what='the bar marks', view='A VIEW')
assert row['severity'] == 'WARN'
assert 'could not tell' in row['message']
assert row['model_read']['answer'] == 'cannot_tell'
def test_failing_to_reach_the_model_is_not_run_with_the_reason_never_a_pass():
"""No key, no network, a timeout, no SDK. Every one of them is the same
outcome, and it is not silence and it is not OK."""
row = TV.verdict_row(_result(status='NOT_RUN', answer=None,
reason='the model could not be reached: '
'APIConnectionError'),
what='the bar marks')
assert row['severity'] == 'WARN'
assert row['model_read']['status'] == 'NOT_RUN'
assert 'could not' in row['message']
assert 'APIConnectionError' in row['message']
def test_a_missing_response_file_produces_nothing_that_looks_like_a_pass():
assert TV.rows_from_response(None) == []
assert TV.read_response(os.path.join(ROOT, 'no', 'such', 'file')) is None
def test_even_a_clean_read_is_a_warn_and_never_an_ok():
"""Section 8bis allows one shape: the row reads NOT RUN and a WARN says a
person still has to look. `clear` is a reason to look, not a reason to
skip looking, so there is no branch here that can emit OK or FAIL."""
for answer in ('clear', 'problem_seen', 'cannot_tell'):
row = TV.verdict_row(_result(answer=answer), what='the bar marks')
assert row['severity'] == 'WARN', answer
assert 'NOT a verdict' in row['message'], answer
def test_the_verdict_itself_is_not_pinned_only_the_plumbing_is():
"""Deliberate: the two opposite answers travel the same path and produce
the same severity and the same provenance. Nothing downstream branches on
which one came back, so nothing downstream can be broken by the model
changing its mind between runs."""
good = TV.verdict_row(_result(answer='clear'), what='x')
bad = TV.verdict_row(_result(answer='problem_seen', where='upper left'),
what='x')
assert good['severity'] == bad['severity'] == 'WARN'
assert good['source'] == bad['source'] == 'model-read'
# ------------------------------------------------------------- provenance
def test_every_model_read_row_carries_its_provenance_four_ways():
row = TV.verdict_row(_result(), what='the bar marks', view='A VIEW')
assert row['source'] == 'model-read' # 1: the field
assert row['code'].startswith('AI-') # 2: the code
assert row['message'].startswith('[read by a model') # 3: the message
assert 'model_read' in row # 4: the payload
def test_a_computed_finding_says_computed_by_default():
"""Everything that existed before this path was computed, so a finding
that does not say otherwise is computed."""
from vibe.ticketpage import weight_agreement # any Revit-free producer
assert weight_agreement(None, None, None) is None # sanity
# The Finding class itself is the thing under test, and it needs Revit
# to import; its default is asserted through the JSON shaping instead.
row = TV._json_row({'severity': 'WARN', 'code': 'WGT-TWO-WAY',
'message': 'x'}, 'computed')
assert row['source'] == 'computed'
assert row['computed'] is True
def test_the_finding_object_itself_carries_the_provenance(monkeypatch):
"""`to_findings` is the one place this module touches `vibe.ticketqc`,
lazily, because that module needs Revit to import. The stub here stands in
for the real `Finding`, whose signature it mirrors -- what is under test
is that `source` is set on the object and the payload rides along."""
import sys
import types
class _Finding(object):
def __init__(self, severity, code, message, source='computed'):
self.severity, self.code = severity, code
self.message, self.source = message, source
stub = types.ModuleType('vibe.ticketqc')
stub.Finding = _Finding
monkeypatch.setitem(sys.modules, 'vibe.ticketqc', stub)
out = TV.to_findings([TV.verdict_row(_result(), what='x', view='A VIEW')])
assert out[0].source == 'model-read'
assert out[0].severity == 'WARN'
assert out[0].code == 'AI-BARMARK'
assert out[0].model_read['view'] == 'A VIEW'
# And the JSON shaper reads a Finding as happily as it reads a row.
assert TV.qc_json([], out)['model_read'][0]['computed'] is False
def test_the_two_kinds_never_share_a_list_a_table_or_a_key():
computed = [{'severity': 'FAIL', 'code': 'DUN-OPENING', 'message': 'x'}]
read = [TV.verdict_row(_result(), what='the bar marks')]
doc = TV.qc_json(computed, read)
assert [r['code'] for r in doc['findings']] == ['DUN-OPENING']
assert [r['code'] for r in doc['model_read']] == ['AI-BARMARK']
assert all(r['computed'] is True for r in doc['findings'])
assert all(r['computed'] is False for r in doc['model_read'])
assert all(r['source'] == 'model-read' for r in doc['model_read'])
assert 'not a verdict' in doc['model_read_disclaimer']
# And the JSON survives a round trip, since that is how it leaves.
assert json.loads(json.dumps(doc)) == doc
def test_the_run_json_lands_beside_the_models_qc_data(tmp_path):
"""Next to `last-ticketqc.txt` in the QC data folder, under the name the
rest of the code promises -- not inside the vision working folder, which
a run with vision off has no business writing to."""
doc = TV.qc_json([], [TV.verdict_row(_result(), what='x')])
path = TV.write_run_json(str(tmp_path), doc)
assert os.path.dirname(path) == str(tmp_path)
assert os.path.basename(path) == 'qc-run.json'
with open(path, encoding='utf-8') as fh:
back = json.load(fh)
assert back['model_read'][0]['computed'] is False
assert back['findings'] == []
def test_the_run_json_is_written_when_no_model_was_consulted(tmp_path):
"""Vision defaults OFF, and the muted section promises every run's
findings reach this file. So it is written with an empty `model_read` --
which is itself the readable answer to "was a model asked?"."""
doc = TV.qc_json([{'severity': 'FAIL', 'code': '4.1',
'message': 'a computed fail'}], [])
path = TV.write_run_json(str(tmp_path), doc)
with open(path, encoding='utf-8') as fh:
back = json.load(fh)
assert back['model_read'] == []
assert [r['code'] for r in back['findings']] == ['4.1']
assert back['findings'][0]['computed'] is True
def test_the_vision_folder_does_not_double_the_qc_segment(tmp_path):
"""`designspec.root_dir` owns the layout and beside the model it already
ends in `_ticketqc`; joining that segment again put every request,
response and log in `_ticketqc/_ticketqc/vision`."""
root = os.path.join(str(tmp_path), '_ticketqc')
got = TV.vision_dir(root)
assert got == os.path.join(root, 'vision')
assert '_ticketqc' + os.sep + '_ticketqc' not in got
def test_the_report_puts_a_model_read_in_its_own_block_naming_what_read_it():
head, rows = TV.report_block([TV.verdict_row(_result(), what='x',
view='A VIEW')])
assert 'Read by a model, not computed' in head
assert 'not a verdict' in head
assert 'read by' in [c.lower() for c in TV.REPORT_COLUMNS]
assert rows[0][4] == 'a model looked at a picture'
assert len(rows[0]) == len(TV.REPORT_COLUMNS)
# And a ticket label rides along, so one table can cover a whole batch.
_, labelled = TV.report_block([('MARK1', TV.verdict_row(_result()))])
assert labelled[0][0] == 'MARK1'
def test_no_model_read_code_owns_a_checklist_row():
"""The structural guarantee: an `AI-` code appears in no CHECKLIST_ITEMS
owner tuple, so a model read cannot set, flip, or satisfy a computed row
however it is wired later."""
owned = set()
for item in TP.CHECKLIST_ITEMS:
for code in (item[3] or ()):
owned.add(code)
for row in TV.ESCALATIONS:
assert row['code'] not in owned, row['code']
assert not any(row['code'].startswith(o) for o in owned), row['code']
def test_annotating_a_row_marks_it_and_never_changes_its_status():
rows = [('5.5', 'Bar marks legible and unique on the drawing', 'D',
'FOR ENGINEER', 'person reads the drawing (grade D)'),
('8.2', 'No invisible characters', 'A', 'OK', None)]
out = TV.annotate_rows(rows, [TV.verdict_row(_result(), what='x')])
assert [r[3] for r in out] == ['FOR ENGINEER', 'OK']
assert 'a model also looked' in out[0][4]
assert 'not a verdict' in out[0][4]
assert out[1] == rows[1]
# ------------------------------------------------------ cost is opt-in, bounded
def test_the_escalation_is_off_by_default():
"""expensive_geometry is ON by default because it spends seconds on a
machine nobody bills. This spends money and leaves the building."""
assert configmodel.default_run_options()['vision_escalation'] is False
assert configmodel.default_run_options()['vision_escalation_max_calls'] == 25
def test_with_the_switch_off_nothing_is_sent_and_the_row_says_why():
items, blocked = TV.escalation_items({'mark': 'X'}, {}, None,
options={'vision_escalation': False})
assert items == []
assert len(blocked) == len(TV.ESCALATIONS)
assert 'vision_escalation is OFF' in blocked[0]['message']
assert blocked[0]['severity'] == 'WARN'
def test_an_unreadable_options_file_leaves_the_escalation_off():
"""Off is the safe default for a switch that spends money; a broken file
must never turn one on."""
on, cap = TV.run_settings(os.path.join(ROOT, 'no', 'such', 'folder'))
assert on is False and cap == 25
def test_no_plant_view_names_means_not_run_naming_the_key_never_a_pass():
"""Same discipline as notes.instruction_views: which views carry bar marks
is plant nomenclature, the default is empty, and an unconfigured plant
gets NOT RUN with the reason rather than a ticket nobody looked at."""
from vibe import knowledge as K
assert K.DEFAULTS['vision']['barmark_views'] == []
items, blocked = TV.escalation_items({'mark': 'X'}, {}, None,
options={'vision_escalation': True})
assert items == []
assert 'vision.barmark_views' in blocked[0]['message']
def test_a_view_that_prints_too_small_is_refused_before_the_model_is_called():
class _Pt(object):
def __init__(self, x, y):
self.X, self.Y = x, y
class _Box(object):
MinimumPoint = _Pt(0.0, 0.0)
MaximumPoint = _Pt(2.5, 1.5) # 30 in x 18 in on paper
class _VP(object):
@staticmethod
def GetBoxOutline():
return _Box()
t = {'mark': 'X', 'viewports': [{'name': 'A VIEW', 'viewport': _VP()}]}
items, blocked = TV.escalation_items(
t, {'vision': {'barmark_views': ['A VIEW']}}, None,
options={'vision_escalation': True})
assert items == []
assert 'dpi' in blocked[0]['message']
def test_over_budget_escalations_are_reported_never_dropped():
"""A dropped item reads exactly like an item that passed."""
items, blocked = TV.escalation_items(
{'mark': 'X', 'viewports': []},
{'vision': {'barmark_views': ['A VIEW']}}, None,
options={'vision_escalation': True, 'vision_escalation_max_calls': 0})
assert items == []
assert blocked # a reason, always
def test_a_ticket_with_no_such_view_says_so_rather_than_passing():
items, blocked = TV.escalation_items(
{'mark': 'X', 'viewports': [{'name': 'SOMETHING ELSE',
'viewport': None}]},
{'vision': {'barmark_views': ['A VIEW']}}, None,
options={'vision_escalation': True})
assert items == []
assert 'nothing to look at' in blocked[0]['message']
# ------------------------------------------------ the CPython half's plumbing
def test_prose_is_a_refusal_not_a_verdict():
"""A reply that is not one of the three answers is NOT coerced to the
nearest legal value. A coerced verdict is the exact hallucination this
path exists to keep out of the report."""
vr = _vision_read()
assert vr.parse_answer('Looks fine to me!', 'BARMARK-LEGIBLE') is None
assert vr.parse_answer('{"answer": "probably_fine"}',
'BARMARK-LEGIBLE') is None
assert vr.parse_answer('', 'BARMARK-LEGIBLE') is None
def test_an_answer_to_a_different_question_is_discarded():
vr = _vision_read()
assert vr.parse_answer('{"question_id": "NOTE-SENSE", "answer": "clear"}',
'BARMARK-LEGIBLE') is None
def test_a_fenced_json_answer_still_parses():
vr = _vision_read()
out = vr.parse_answer(
'```json\n{"question_id": "BARMARK-LEGIBLE", "answer": '
'"cannot_tell", "why": "too small"}\n```', 'BARMARK-LEGIBLE')
assert out['answer'] == 'cannot_tell'
assert out['why'] == 'too small'
def test_the_cpython_half_turns_every_failure_into_not_run(tmp_path):
"""No skill file, no SDK, no credential -- three different failures, one
outcome, and it is never a pass and never a dropped row."""
vr = _vision_read()
out = vr.run({'skill': str(tmp_path / 'nope.md'),
'items': [{'id': 'a', 'question_id': 'BARMARK-LEGIBLE'},
{'id': 'b', 'question_id': 'BARMARK-LEGIBLE'}]})
assert len(out['results']) == 2
for row in out['results']:
assert row['status'] == 'NOT_RUN'
assert row['reason']
assert row['answer'] is None
assert row['source'] == 'model-read'
def test_the_cpython_half_enforces_the_budget_and_reports_the_excess(tmp_path):
vr = _vision_read()
out = vr.run({'skill': str(tmp_path / 'nope.md'), 'max_calls': 1,
'items': [{'id': 'a'}, {'id': 'b'}, {'id': 'c'}]})
assert len(out['results']) == 3 # nothing silently dropped
over = [r for r in out['results'] if 'budget' in (r['reason'] or '')]
assert len(over) == 2
def test_the_two_halves_agree_on_the_image_ceiling():
"""They cannot import each other, so a drift between them would surface
as unexplained illegibility rather than an error."""
vr = _vision_read()
assert TV.LONG_EDGE_PX <= vr.IMAGE_LONG_EDGE_CEILING_PX
def test_no_credential_is_ever_written_into_a_file_this_keeps():
"""The request and the response are both written to disk beside the
model. Neither may carry a key, and the way that is guaranteed is that
nothing in either half ever reads one."""
with open(os.path.join(ROOT, 'tools', 'vision_read.py'),
encoding='utf-8') as fh:
source = fh.read()
assert 'api_key=' not in source
assert '192.168.99.151' in source # the LAN server, named in the docs
request = TV.build_request([{'id': 'a'}], max_calls=3)
assert 'key' not in json.dumps(request).lower()
# ============================================================================
# THE RUN'S DISPOSITION IN `qc-run.json` -- every row, its state, its reason
# ============================================================================
#
# The file used to hold `findings` + `model_read` and nothing else, so a
# consumer could not tell a row that PASSED from a row NOBODY ASKED: both were
# an absence of findings. Everything below holds the fix in place.
import copy # noqa: E402
class _StubFinding(object):
"""A stand-in for `ticketqc.Finding`, which needs Revit to import."""
def __init__(self, severity, code, message):
self.severity, self.code, self.message = severity, code, message
self.source = 'computed'
def _finding(severity, code, message):
return _StubFinding(severity, code, message)
def _run(findings=None, log=None, muted=None, mark='PNL101'):
"""One press's result entry for one piece, built the way the button
builds it: states, then the model-read annotation, then muting."""
rows = TP.checklist_states(list(findings or []), log or [])
kept, notes = TP.apply_muting(rows, muted or {})
return {'ticket': {'mark': mark}, 'design': 'PNL1.0',
'spec_source': 'spec', 'rows': kept, 'rows_unmuted': rows,
'run_log': list(log or []), 'muted_notes': notes}
def _rows_by_item(document, ticket=0):
return dict((r['item'], r) for r in document['tickets'][ticket]['rows'])
# ------------------------------------------------- no row can go missing
def test_every_checklist_row_reaches_the_json_walked_from_the_table_itself():
"""THE BUG, mechanically. Not a hand-written list of row numbers -- this
walks `CHECKLIST_ITEMS` and demands the file covers every one of them.
A hand-kept list is exactly how 5.3 spent a day printing the wrong state
(`ticketpage.CHECKLIST_FOR_ENGINEER` used to be one), so the assertion is
against the table and never against a literal."""
doc = TV.qc_json([], [], tickets=[_run(log=[
('check_sheet_name', 'RAN', None),
('check_jacking_force', 'SKIPPED', 'no jacking force printed'),
('check_note_views', 'FAILED', 'KeyError: notes'),
('check_wwf_designation', 'RAN-EMPTY', None),
])])
got = _rows_by_item(doc)
expected = [item[0] for item in TP.CHECKLIST_ITEMS]
assert sorted(got) == sorted(expected)
assert len(doc['tickets'][0]['rows']) == len(expected), 'no duplicates'
for item in TP.CHECKLIST_ITEMS:
row = got[item[0]]
assert row['name'] == item[1]
assert row['grade'] == item[2]
assert row['state'], '{0} reached the file with no state'.format(
item[0])
def test_the_row_list_is_derived_from_the_table_and_not_kept_by_hand():
"""The proof that the test above is mechanical rather than lucky: add a
row to the checklist table and the file grows one, with no edit here and
no edit in `ticketvision`. A hand-kept list could not do this."""
invented = ('9.9', 'An invented row', 'B', ('INVENTED',), None, None)
original = TP.CHECKLIST_ITEMS
try:
TP.CHECKLIST_ITEMS = original + (invented,)
doc = TV.qc_json([], [], tickets=[_run()])
finally:
TP.CHECKLIST_ITEMS = original
got = _rows_by_item(doc)
assert '9.9' in got
assert got['9.9']['name'] == 'An invented row'
assert got['9.9']['state'] == 'NOT RUN'
assert got['9.9']['judged'] is False
def test_a_row_the_run_dropped_lands_as_not_run_and_says_the_tool_dropped_it():
"""A reader must never have to infer a state from an absence. If the run
hands over rows with one missing -- for any reason, including one nobody
has thought of -- the file still carries it, loudly, never as a pass."""
entry = _run()
entry['rows'] = [r for r in entry['rows'] if r[0] != '6.3']
entry['rows_unmuted'] = list(entry['rows'])
doc = TV.qc_json([], [], tickets=[entry])
got = _rows_by_item(doc)
assert '6.3' in got
assert got['6.3']['state'] == 'NOT RUN'
assert got['6.3']['judged'] is False
assert got['6.3']['reported_by_run'] is False
assert 'never as a pass' in got['6.3']['reason']
# And every other row still says it did come from the run.
assert got['6.4']['reported_by_run'] is True
def test_an_empty_run_still_carries_no_claim_that_anything_passed():
"""No tickets at all: the findings list is empty and that is NOT the same
as a clean run. The file says so rather than looking like success."""
doc = TV.qc_json([], [])
assert doc['tickets'] == []
assert doc['ticket_count'] == 0
assert 'is not a row that passed' in doc['missing']['tickets']
# ------------------------------------------------- the reason survives
def test_a_not_run_row_carries_its_reason_verbatim_and_uncut():
"""The premise of the whole checklist. The reason existed only on the HTML
page and in `last-ticketqc.txt`; the JSON read as success."""
reason = ('the jacking force printed on the ticket is not a number, so '
'it could not be compared against the design -- ' + 'x' * 400)
doc = TV.qc_json([], [], tickets=[_run(log=[
('check_jacking_force', 'SKIPPED', reason)])])
row = _rows_by_item(doc)['4.3']
assert row['state'] == 'NOT RUN'
assert row['judged'] is False
assert row['reason'] == reason, 'the reason must be verbatim'
assert len(row['reason']) == len(reason), 'and uncut'
assert row['note'] == reason
def test_the_disposition_log_reaches_the_file_named_not_positional():
"""`run_all`'s (check, state, note) is the premise of every row's state.
A consumer reading a bare array has to know the tuple order to use it."""
doc = TV.qc_json([], [], tickets=[_run(log=[
('check_note_views', 'FAILED', 'KeyError: notes'),
('check_sheet_name', 'RAN', None)])])
log = doc['tickets'][0]['run_log']
assert {'check': 'check_note_views', 'disposition': 'FAILED',
'note': 'KeyError: notes'} in log
# And the row that names that check carries the disposition too.
assert _rows_by_item(doc)['6.5']['checks'] == [
{'check': 'check_note_views', 'disposition': 'FAILED',
'note': 'KeyError: notes'}]
def test_a_row_with_no_reason_at_all_says_so_rather_than_reading_as_settled():
"""Absence is not a value. A row that produced no verdict AND recorded no
reason is a hole in the tool, and it is named as one.
`missing.reason` is the LAST rung of that ladder and it is no longer the
one that catches this case: as of 2026-08-31 `checklist_states` refuses
to hand out an empty reason at all, so a code-answered row with nothing
to say arrives carrying a sentence that says exactly that (see
`ticketpage.NO_REASON_AT_ALL`). Both rungs assert the same thing -- the
row is not judged and does not read as settled -- and this pins the one
that now fires. `missing.reason` still guards a row that arrives with a
state but no note from anywhere else.
"""
doc = TV.qc_json([], [], tickets=[_run()])
row = _rows_by_item(doc)['4.1']
assert row['judged'] is False
assert row['reason'], 'a NOT RUN row may never carry an empty reason'
assert 'nothing in this run recorded why' in row['reason']
assert 'not a pass' in row['reason'] or 'NOT judged' in row['reason']
# ------------------------------------- the states are distinct in the DATA
def test_not_run_and_fail_are_distinguishable_in_data_not_just_in_colour():
"""Both are red on the page, deliberately. A file that carried only the
colour, or only a pass/fail boolean, would have merged them."""
doc = TV.qc_json([], [], tickets=[_run(
findings=[_finding('FAIL', 'DUN-OPENING',
'a block sits under an opening')],
log=[('check_dunnage_openings', 'RAN', None),
('check_jacking_force', 'SKIPPED', 'no printed force')])])
got = _rows_by_item(doc)
assert got['6.4']['state_key'] == 'fail'
assert got['4.3']['state_key'] == 'not_run'
assert got['6.4']['state_key'] != got['4.3']['state_key']
# ... even though the page paints them the same.
assert TP.state_color('FAIL') == TP.state_color('NOT RUN') == 'red'
# And only one of them is a judgement.
assert got['6.4']['judged'] is True
assert got['4.3']['judged'] is False
def test_every_state_the_table_can_print_has_its_own_key_in_the_file():
"""Seven states, seven keys, no two alike -- and the vocabulary travels in
the document so a consumer does not have to have this file to read it."""
words = [s for (s, _k, _j) in TP.STATE_VOCABULARY]
keys = [k for (_s, k, _j) in TP.STATE_VOCABULARY]
for word in ('OK', 'WARN', 'FAIL', 'NOT RUN', 'FOR ENGINEER',
TP.MUTED_STATE, TP.OUT_OF_SCOPE_STATE):
assert word in words, '{0} has no machine key'.format(word)
assert len(set(keys)) == len(keys), 'two states share a key'
assert TP.state_key('SOMETHING NOBODY DECLARED') == TP.UNKNOWN_STATE_KEY
assert TP.state_key('SOMETHING NOBODY DECLARED') != 'ok'
assert TP.state_is_judged('SOMETHING NOBODY DECLARED') is False
doc = TV.qc_json([], [], tickets=[_run()])
assert doc['checklist_states'] == [
{'state': s, 'state_key': k, 'judged': j}
for (s, k, j) in TP.STATE_VOCABULARY]
def test_every_state_word_the_page_paints_is_in_the_vocabulary():
"""The seam. A word `state_color` knows but `state_key` does not would be
a state the page can print and the file cannot spell."""
painted = (TP.GREEN_STATES + TP.YELLOW_STATES + TP.RED_STATES +
TP.GREY_STATES)
words = [s.upper() for (s, _k, _j) in TP.STATE_VOCABULARY]
for word in painted:
assert word.upper() in words, '{0} has no machine key'.format(word)
assert TP.state_key(word) != TP.UNKNOWN_STATE_KEY
# ------------------------------------------------------------- muting
def test_a_muted_row_is_distinguishable_from_an_ok_row():
"""Muting is a volume control, never an eraser. A grey row and a green row
must not read the same to a machine, and the grey one has to say what the
check REALLY reported -- as a state, not only inside an English note."""
doc = TV.qc_json([], [], tickets=[_run(
findings=[_finding('OK', 'FCI-RATIO', 'ratio in band')],
log=[('check_release_ratio', 'RAN', None),
('check_sheet_name', 'RAN', None)],
muted={'3.3': 'no piece on this job has one'})])
got = _rows_by_item(doc)
assert got['1.1']['state_key'] == 'ok'
assert got['3.3']['state_key'] == 'muted'
assert got['3.3']['state_key'] != got['1.1']['state_key']
assert got['3.3']['muted']['reported_state'] == 'OK'
assert got['3.3']['muted']['reported_state_key'] == 'ok'
assert got['3.3']['muted']['reason'] == 'no piece on this job has one'
assert got['3.3']['judged'] is True
assert 'muted' not in got['1.1']
# And the run's mute list travels beside the rows.
assert doc['tickets'][0]['muted'][0]['item'] == '3.3'
assert doc['tickets'][0]['muted'][0]['declined'] is False
def test_muting_a_row_that_never_ran_does_not_make_it_read_as_judged():
"""The one door this table left open. A job may mute a row that reported
NOT RUN, and the grey then sits over a question nobody could answer --
`judged` has to follow what the row really reported or the file hands back
the silence-is-a-pass reading through the mute."""
doc = TV.qc_json([], [], tickets=[_run(
log=[('check_release_ratio', 'SKIPPED', 'no fci in the intent')],
muted={'3.3': 'quiet on this job'})])
row = _rows_by_item(doc)['3.3']
assert row['state_key'] == 'muted'
assert row['muted']['reported_state'] == 'NOT RUN'
assert row['muted']['reported_state_key'] == 'not_run'
assert row['judged'] is False
assert 'no fci in the intent' in row['reason']
def test_a_mute_that_was_declined_says_so_in_the_file():
"""A FAIL is never softened. A consumer that read `checks.muted` off the
job file would believe the row was quiet; the run says it was not."""
doc = TV.qc_json([], [], tickets=[_run(
findings=[_finding('FAIL', 'FCI-RATIO', 'ratio out of band')],
log=[('check_release_ratio', 'RAN', None)],
muted={'3.3': 'quiet on this job'})])
row = _rows_by_item(doc)['3.3']
assert row['state_key'] == 'fail', 'muting must never soften a FAIL'
assert 'muted' not in row
entry = doc['tickets'][0]['muted'][0]
assert entry['item'] == '3.3'
assert entry['declined'] is True
assert 'FULL VOLUME' in entry['why']
def test_a_mute_with_no_reason_is_reported_and_not_quietly_accepted():
doc = TV.qc_json([], [], tickets=[_run(
log=[('check_release_ratio', 'RAN', None)], muted={'3.3': ''})])
entry = doc['tickets'][0]['muted'][0]
assert entry['reason'] is None
assert entry['reason_missing'] is True
# ------------------------------------------------------------ provenance
def test_the_envelope_is_the_one_this_codebase_already_uses():
"""Copied from `ticketqc.run_evidence`, not invented next door -- same
keys, same `missing` discipline, built by the same function."""
envelope = {'schema': 'ignored -- qc_json stamps its own',
'written_at': '2026-08-31T09:00:00',
'written_by': 'TicketQC 1.2.3',
'build': {'extension_version': '1.2.3', 'reason': None},
'doc_key': 'a-doc-key', 'model_stem': 'sample_job',
'knowledge_sources': [{'label': 'defaults', 'path': '-',
'exists': True}],
'missing': {}}
doc = TV.qc_json([], [], tickets=[_run()], envelope=envelope)
assert doc['schema'] == TV.RUN_SCHEMA
assert doc['written_at'] == '2026-08-31T09:00:00'
assert doc['written_by'] == 'TicketQC 1.2.3'
assert doc['doc_key'] == 'a-doc-key'
assert doc['model_stem'] == 'sample_job'
assert doc['knowledge_sources'][0]['label'] == 'defaults'
# Which spec source each piece was judged from travels per ticket.
assert doc['tickets'][0]['spec_source'] == 'spec'
assert doc['tickets'][0]['design'] == 'PNL1.0'
def test_a_run_with_no_provenance_says_so_rather_than_omitting_the_keys():
"""A null and an absent key look identical across a file boundary, and
both look like there was nothing to record."""
doc = TV.qc_json([], [], tickets=[_run()])
for key in ('written_at', 'written_by', 'doc_key', 'model_stem',
'knowledge_sources', 'build'):
assert key in doc
assert doc[key] is None
assert 'nothing here can be re-judged' in doc['missing']['envelope']
def test_a_piece_with_no_spec_source_names_that_rather_than_guessing():
entry = _run()
entry['spec_source'] = None
doc = TV.qc_json([], [], tickets=[entry])
assert doc['tickets'][0]['spec_source'] is None
assert 'weaker regex reading' in doc['tickets'][0]['missing']['spec_source']
# ----------------------------------------------- schema: discard whole
def test_a_schema_mismatch_discards_the_whole_file_not_part_of_it(tmp_path):
"""`reinforcecore.load()` is the reference implementation. The keys an old
`qc-run.json` still parses under are exactly `findings` and `model_read`
-- adopting those and skipping `tickets` would be reading the very file
this schema replaces, which reads as a clean run."""
path = os.path.join(str(tmp_path), 'qc-run.json')
stale = {'findings': [{'severity': 'FAIL', 'code': 'DUN-OPENING',
'message': 'a real failure'}],
'model_read': [], 'model_read_disclaimer': 'x',
'tickets': [{'mark': 'PNL101', 'rows': [{'item': '1.1'}]}]}
with open(path, 'w', encoding='utf-8') as fh:
json.dump(stale, fh)
back = TV.load_run_json(path)
assert back['tickets'] == []
assert back['findings'] == []
assert back['model_read'] == []
assert back['ticket_count'] == 0
assert 'schema' in back['_load_error']
assert TV.RUN_SCHEMA in back['_load_error']
def test_a_file_of_the_right_schema_round_trips_whole(tmp_path):
doc = TV.qc_json([_finding('FAIL', 'DUN-OPENING', 'x')], [],
tickets=[_run(log=[('check_jacking_force', 'SKIPPED',
'no printed force')])])
path = TV.write_run_json(str(tmp_path), doc)
back = TV.load_run_json(path)
assert '_load_error' not in back
assert back['schema'] == TV.RUN_SCHEMA
assert len(back['tickets'][0]['rows']) == len(TP.CHECKLIST_ITEMS)
got = dict((r['item'], r) for r in back['tickets'][0]['rows'])
assert got['4.3']['reason'] == 'no printed force'
def test_a_damaged_or_absent_file_judges_nothing_not_half_a_run(tmp_path):
missing = os.path.join(str(tmp_path), 'nope.json')
assert TV.load_run_json(missing)['tickets'] == []
assert 'no run file' in TV.load_run_json(missing)['_load_error']
broken = os.path.join(str(tmp_path), 'qc-run.json')
with open(broken, 'w', encoding='utf-8') as fh:
fh.write('{not json')
assert TV.load_run_json(broken)['tickets'] == []
assert TV.load_run_json(broken)['_load_error']
# ------------------------------- the model-read quarantine still holds
def test_the_disposition_never_lets_a_model_read_occupy_a_checklist_row():
"""`AI-*` codes own no row in `CHECKLIST_ITEMS`, and adding the rows to
the file must not have given them one. 5.5 stays FOR ENGINEER."""
read = [TV.verdict_row(_result(), what='the bar marks')]
entry = _run()
entry['rows'] = TV.annotate_rows(entry['rows'], read)
entry['rows_unmuted'] = list(entry['rows'])
doc = TV.qc_json([], read, tickets=[entry])
got = _rows_by_item(doc)
assert got['5.5']['state'] == 'FOR ENGINEER'
assert got['5.5']['state_key'] == 'for_engineer'
assert got['5.5']['judged'] is False
assert 'a model also looked' in got['5.5']['note']
# The read itself is still in its own key and still not computed.
assert [r['code'] for r in doc['model_read']] == ['AI-BARMARK']
assert doc['findings'] == []
assert all(r['computed'] is False for r in doc['model_read'])
# And no AI-* code ever appears as a row's owner.
for row in doc['tickets'][0]['rows']:
assert not any(str(c).startswith('AI-') for c in row['owner_codes'])
# --------------------------------------- and the human output did not move
def _page(entry):
"""Exactly what the button prints for one piece: the HTML table, the
plain-text table that goes into `last-ticketqc.txt`, and the headline.
`_TABLE_SEQ` is reset first. It is a process-wide counter that numbers the
DOM ids of the note-fold divs (`wt_ckn_t1_...`, `wt_ckn_t2_...`), so two
renders of the SAME rows differ in those ids alone -- by design, because
two tables on one page may not share an id. Resetting it is what makes a
byte comparison of the rendered page meaningful rather than a comparison
of a counter; nothing else about the output varies between calls.
"""
TP._TABLE_SEQ[0] = 0
details = TP.checklist_details([])
counts = TP.checklist_tally(entry['rows'])
return (TP.checklist_table_html(entry['rows'], details),
TP.checklist_table_text(entry['rows'], details),
TP.checklist_headline(counts))
def test_the_page_render_is_byte_stable_once_its_id_counter_is_reset():
"""The control for the test below: with the DOM-id counter reset, two
renders of the same rows are byte-identical. Without this, "the report did
not move" would be an assertion about a counter."""
entry = _run(log=[('check_sheet_name', 'RAN', None)])
assert _page(entry) == _page(entry)
def test_writing_the_json_moves_nothing_a_human_sees():
"""PROVEN, not asserted. The page, the text file and the findings are
rendered, the JSON is then built from the same objects, and everything is
rendered again and compared byte for byte -- plus a deep-equality check
that `qc_json` did not mutate a row, a note or a finding on the way."""
findings = [_finding('FAIL', 'DUN-OPENING',
'a block sits under an opening at 3 ft'),
_finding('OK', 'SHEET-NAME', 'sheet name matches')]
entry = _run(findings=findings,
log=[('check_dunnage_openings', 'RAN', None),
('check_sheet_name', 'RAN', None),
('check_jacking_force', 'SKIPPED', 'no printed force')],
muted={'3.3': 'no piece on this job has one'})
before_page = _page(entry)
before_entry = copy.deepcopy(entry)
before_findings = [(f.severity, f.code, f.message) for f in findings]
doc = TV.qc_json(findings, [], tickets=[entry])
assert doc['tickets'], 'the JSON really was built'
assert _page(entry) == before_page, 'the rendered report moved'
assert entry == before_entry, 'qc_json mutated the run result'
assert [(f.severity, f.code, f.message) for f in findings] == \
before_findings, 'qc_json mutated a finding'
def test_the_json_is_built_from_the_same_rows_the_page_is_rendered_from():
"""The other half of "the file and the page cannot say different things":
every state word in the file is the word the page printed for that row."""
entry = _run(findings=[_finding('WARN', 'CLASH', 'two members touch')],
log=[('check_member_interference', 'RAN', None),
('check_invisible', 'SKIPPED', 'no ticket text')],
muted={'3.3': 'quiet'})
doc = TV.qc_json([], [], tickets=[entry])
got = _rows_by_item(doc)
for row in entry['rows']:
assert got[row[0]]['state'] == row[3]
assert got[row[0]]['note'] == row[4]
text = TP.checklist_table_text(entry['rows'], TP.checklist_details([]))
for row in entry['rows']:
if row[3]:
assert row[3] in text
def test_the_findings_half_of_the_file_is_unchanged_by_the_disposition():
"""The addition is an addition. The two-keys-never-one-list quarantine and
every key a consumer already read are exactly as they were."""
computed = [{'severity': 'FAIL', 'code': 'DUN-OPENING', 'message': 'x'}]
read = [TV.verdict_row(_result(), what='the bar marks')]
doc = TV.qc_json(computed, read, tickets=[_run()])
assert [r['code'] for r in doc['findings']] == ['DUN-OPENING']
assert [r['code'] for r in doc['model_read']] == ['AI-BARMARK']
assert all(r['computed'] is True for r in doc['findings'])
assert all(r['computed'] is False for r in doc['model_read'])
assert 'not a verdict' in doc['model_read_disclaimer']
assert json.loads(json.dumps(doc)) == doc
# ============================================================================
# THE MACHINE-READABLE SIDE OF THE FIFTH STATE
# ============================================================================
#
# `qc-run/2`. A consumer must be able to tell a row that PASSED from a row
# NOBODY ASKED from a row the plant declares is not a question about this
# piece -- and, for the last of those, to see the classification the
# declaration rested on. The conclusion without the premise is not auditable.
def _classified(panel_type='insulated', verified=True):
return {'panel_type': panel_type, 'basis': 'signals',
'verified': verified, 'why': 'two signals agree',
'candidates': [panel_type] if panel_type else [],
'signals': {'insulated': {'thickness_in': {'agrees': True,
'why': '16 in'}}},
'facts': {'thickness_in': 16.0}}
def _na_run(mark='PNL101', classification=None):
"""One press's entry with 3.3 switched off by a declaration."""
rows = TP.checklist_states([], [])
decisions = {'3.3': {'not_applicable': True, 'condition': 'insulated',
'reason': 'these have no release requirement',
'since': '2026-08-31', 'declared_by': 'Will',
'layer': 'plant'}}
kept, notes = TP.apply_applicability(rows, decisions)
muted_kept, muted_notes = TP.apply_muting(kept, {})
ticket = {'mark': mark}
if classification is not None:
ticket['panel_type'] = classification
return {'ticket': ticket, 'design': 'PNL1.0', 'spec_source': 'spec',
'rows': muted_kept, 'rows_unmuted': rows, 'run_log': [],
'muted_notes': muted_notes, 'applicability_notes': notes,
'applicability_problems': []}
def test_the_schema_is_bumped_because_the_shape_changed():
"""Rule 1 of the file contract: discard WHOLE on mismatch, never
migrate. So a new key is a bump, and a `qc-run/1` file is refused rather
than read as a document that happens to be missing the key saying a row
was never asked."""
assert TV.RUN_SCHEMA == 'qc-run/3'
assert TV.qc_json([], [])['schema'] == 'qc-run/3'
def test_the_new_state_is_in_the_vocabulary_the_file_publishes():
states = dict((s['state'], s) for s in TV.qc_json([], [])
['checklist_states'])
entry = states[TP.NOT_APPLICABLE_STATE]
assert entry['state_key'] == 'not_applicable'
assert entry['judged'] is False
def test_a_switched_off_row_carries_its_declaration_and_says_it_never_ran():
doc = TV.qc_json([], [], tickets=[_na_run(classification=_classified())])
row = _rows_by_item(doc)['3.3']
assert row['state'] == TP.NOT_APPLICABLE_STATE
assert row['state_key'] == 'not_applicable'
assert row['judged'] is False
assert row['not_applicable']['condition'] == 'insulated'
assert row['not_applicable']['reason'] == 'these have no release ' \
'requirement'
assert row['not_applicable']['since'] == '2026-08-31'
assert row['not_applicable']['check_ran'] is False
assert row['reason']
def test_the_classification_travels_with_its_basis():
doc = TV.qc_json([], [], tickets=[_na_run(classification=_classified())])
got = doc['tickets'][0]['panel_type']
assert got['panel_type'] == 'insulated'
assert got['basis'] == 'signals'
assert got['verified'] is True
assert got['used_for_applicability'] is True
assert got['signals']['insulated']['thickness_in']['agrees'] is True
assert got['facts']['thickness_in'] == 16.0
def test_an_unverified_classification_says_it_switched_nothing_off():
doc = TV.qc_json([], [], tickets=[
_na_run(classification=_classified(verified=False))])
got = doc['tickets'][0]['panel_type']
assert got['panel_type'] == 'insulated'
assert got['used_for_applicability'] is False
def test_a_run_with_no_classification_says_so_rather_than_omitting_it():
"""Absence is not a value. A missing key reads downstream as "there was
nothing to classify", which is the silence-looks-like-a-pass failure one
layer out."""
doc = TV.qc_json([], [], tickets=[_na_run(classification=None)])
entry = doc['tickets'][0]
assert entry['panel_type']['panel_type'] is None
assert entry['panel_type']['used_for_applicability'] is False
assert 'panel_type' in entry['missing']
def test_the_piece_entry_lists_every_row_that_was_not_asked():
doc = TV.qc_json([], [], tickets=[_na_run(classification=_classified())])
off = doc['tickets'][0]['not_applicable']
assert [e['item'] for e in off] == ['3.3']
assert off[0]['layer'] == 'plant'
assert off[0]['layer_known'] is True
assert off[0]['declined'] is False
def test_a_refused_declaration_reaches_the_file_so_somebody_finds_out():
entry = _na_run(classification=_classified())
entry['applicability_problems'] = [('5.4', 'the entry has no date')]
doc = TV.qc_json([], [], tickets=[entry])
assert doc['tickets'][0]['applicability_refused'] == [
{'item': '5.4', 'problem': 'the entry has no date'}]
def test_muting_and_inapplicability_stay_different_things_in_the_file():
"""Both quieten a row and they are not the same claim. A muted row
records what the check REALLY reported; a not-applicable row records that
the check did not run at all."""
doc = TV.qc_json([], [], tickets=[
_na_run(classification=_classified()),
_run(muted={'5.4': 'no mesh on this job'})])
off = _rows_by_item(doc, 0)['3.3']
grey = _rows_by_item(doc, 1)['5.4']
assert 'muted' not in off
assert off['not_applicable']['check_ran'] is False
assert 'not_applicable' not in grey
assert 'reported_state' in grey['muted']