""" The authorship field: what it records, and what it must never touch. WHY A SEPARATE FILE RATHER THAN LINES IN ``test_design_record.py`` Delivery here is a tarball that expands over the tree, so every file in the set is shipped whole. Reproducing a 16 kB test module in order to add assertions to it risks corrupting sixteen kilobytes of working tests to add one; a new file overwrites nothing and cannot damage what is already green. If these belong beside the others later, moving them is a local edit made in the container. WHAT THESE TESTS ARE ACTUALLY FOR One claim carries the whole design: an author reaches the record and neither id. Most of what follows exists to make that claim falsifiable rather than merely stated -- every negative assertion is paired with a positive control showing the same assertion could have failed (F-027). """ from __future__ import annotations import pytest from mechcomp.design_record import ( Author, DesignRecord, author_from, record_for, ) DEFAULTS = { "strap_width_mm": 15.875, "strap_thickness_mm": 0.508, "fit_clearance_mm": 0.25, "bundle_count": 1, } EMAIL = "theron@kane-il.us" def make(author=None, params=None) -> DesignRecord: return record_for( family="3x", profile="Y", defaults=DEFAULTS, params=params or {}, code_revision="deadbee", generator_revision="8.0.0", author=author, ) # --------------------------------------------------------------------------- # The claim: authorship is in neither hash # --------------------------------------------------------------------------- def test_author_changes_neither_id(): without = make() with_one = make(author=EMAIL) assert with_one.input_id == without.input_id assert with_one.build_id == without.build_id def test_a_different_author_changes_neither_id(): """Not just presence -- identity. Two people, one part, one design id.""" a = make(author=EMAIL) b = make(author="someone.else@example.org") assert a.input_id == b.input_id assert a.build_id == b.build_id def test_the_ids_are_sensitive_to_something(): """ The positive control for both tests above. Without this, an ``input_id`` that ignored its inputs entirely would pass them, and the suite would be asserting that a constant equals itself. """ base = make(author=EMAIL) moved = make(author=EMAIL, params={"fit_clearance_mm": 0.15}) assert moved.input_id != base.input_id assert moved.build_id != base.build_id def test_author_is_not_a_parameter(): """ The mechanism behind the claim, asserted directly. The ids are computed from the resolved parameter set, so the guarantee is structural: if ``author`` never enters ``params``, it cannot reach a hash however the hashing is later rewritten. """ rec = make(author=EMAIL) assert "author" not in rec.params assert EMAIL not in rec.input_canonical assert EMAIL not in rec.build_canonical # --------------------------------------------------------------------------- # What the rendered line says # --------------------------------------------------------------------------- def test_rendered_line_carries_its_own_limit(): text = make(author=EMAIL).render() line = [ln for ln in text.splitlines() if ln.startswith("author ")] assert len(line) == 1 assert EMAIL in line[0] assert "self-declared, unverified" in line[0] def test_absent_author_renders_no_line_at_all(): text = make().render() assert not [ln for ln in text.splitlines() if ln.startswith("author ")] def test_empty_and_whitespace_are_absent_not_empty(): for value in ("", " ", None): assert author_from(value) is None assert not [ln for ln in make(author=value).render().splitlines() if ln.startswith("author ")] def test_a_verified_author_says_so(): """ The positive control for the downgrade test below: the qualifier really does change, so a parser that always reported "unverified" would be distinguishable from one that read the text. """ author = Author(email=EMAIL, verified=True, method="OIDC, 2026-09-11") line = author.describe() assert "verified: OIDC, 2026-09-11" in line assert "self-declared" not in line def test_handle_decorates_but_does_not_replace_the_address(): author = Author(email=EMAIL, handle="TheRON") described = author.describe() assert described.startswith(EMAIL) assert "TheRON" in described # --------------------------------------------------------------------------- # Reading a record back # --------------------------------------------------------------------------- def test_parse_recovers_the_address(): back = DesignRecord.parse(make(author=EMAIL).render()) assert back.author is not None assert back.author.email == EMAIL def test_parse_recovers_the_address_when_a_handle_is_present(): rec = make(author=Author(email=EMAIL, handle="TheRON")) back = DesignRecord.parse(rec.render()) assert back.author.email == EMAIL def test_parse_never_upgrades_a_claim(): """ A verified record read back is self-declared, and that asymmetry is the point: a text file cannot attest to its own verification, so the parser trusts the address and not the adjective. """ rec = make(author=Author(email=EMAIL, verified=True, method="OIDC")) assert "verified: OIDC" in rec.render() # it really was in the text back = DesignRecord.parse(rec.render()) assert back.author.verified is False def test_reading_and_rewriting_does_not_strip_attribution(): once = make(author=EMAIL).render() twice = DesignRecord.parse(once).render() assert EMAIL in twice def test_parse_of_a_record_with_no_author_yields_none(): back = DesignRecord.parse(make().render()) assert back.author is None def test_parse_tolerates_a_record_written_before_the_field_existed(): """ Old records have no author line and must still read. The format version is unchanged because nothing that identifies a design changed. """ text = make().render() assert "MECHCOMP DESIGN RECORD 1" in text back = DesignRecord.parse(text) assert back.family == "3x" and back.profile == "Y" assert back.author is None # --------------------------------------------------------------------------- # Accepting what a caller has # --------------------------------------------------------------------------- def test_author_from_accepts_a_bare_address_or_an_author(): assert author_from(EMAIL) == Author(email=EMAIL) explicit = Author(email=EMAIL, verified=True, method="OIDC") assert author_from(explicit) is explicit def test_a_bare_address_is_never_verified(): assert author_from(EMAIL).verified is False # --------------------------------------------------------------------------- # Through the real build path # --------------------------------------------------------------------------- def test_build_carries_the_author_and_moves_no_id(): """ The same claim again, through ``build()`` rather than ``record_for``, so a wiring mistake between the two cannot hide behind a passing unit test. """ profiles = pytest.importorskip("mechcomp.profiles") plain = profiles.build("3x", "Y") signed = profiles.build("3x", "Y", author=EMAIL) assert signed.record.author.email == EMAIL assert plain.record.author is None assert signed.record.input_id == plain.record.input_id assert signed.record.build_id == plain.record.build_id assert signed.report == plain.report