Files
TheRON 48d566574e design record: an authorship field, recorded and in neither id
The person is identified by an email address. A handle may be chosen later
and does not replace it: the handle is a display name, the address is the id.

Excluded from input_id and build_id. input_id answers whether two records
describe the same part as specified, so two people specifying the same part
must collide there; hashing the author would make identical parts claim to be
different designs. The guarantee is structural rather than careful -- the ids
are computed from the resolved parameter set and the author never enters it.

That exclusion narrows the one-way door it was scheduled against but does not
close it. A record regenerated later with the author filled in keeps its
input_id, but build_id covers the code revision and the Shapely and GEOS
versions, and boolean results on near-degenerate geometry can shift between
GEOS releases -- the F-034 mechanism. Regenerate after a GEOS bump and you have
attribution under a different build identity beside a printed part nobody can
tie to either. Recoverable, not free. Priority order in HANDOFF section 4 is
reversed accordingly: this before STL export, so the first coupon off the
printer carries its author.

The rendered line carries its own limit -- "(self-declared, unverified)".
Nothing checks that the address belongs to whoever typed it, and a bare email
in a field called author reads as identity to someone finding the file in five
years. The record is meant to outlive everyone present, so it states what it
knows. When an authenticated identity exists the qualifier changes and the
distinction stays legible. Same discipline as Provenance.describe(), a separate
type: provenance is about measurement, and design_record imports nothing from
mechcomp, which is what keeps a failure in the record off the build path.

parse() gets its own branch because authorship is neither input nor output and
inherits neither rule. It recovers the address and never the verification: a
text file cannot attest to its own verification, so reading a verified record
back as self-declared understates the claim, which is the only safe direction.

REVISION stays 8.0.0. The field reaches no geometry.

547 passed. The 529 are unchanged and no oracle case moved. The eighteen new
assertions were mutation-tested with PYTHONDONTWRITEBYTECODE=1 and caches
cleared between runs: leaking the author into input_canonical, deafening the
parser, trusting the adjective, dropping the qualifier, and turning an empty
author into an empty claim each fail the suite.
2026-09-12 03:56:50 -05:00

222 lines
7.5 KiB
Python

"""
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