Commit 3b90615
feat(mutation): add lockfile-normalization owner to bounded mutmut pilot (#2251)
* test(deps): add field-complete lockfile property/metamorphic proofs
Add hypothesis-generated round-trip and reconstruction properties for
every field LockedDependency persists and every field DependencyReference/
ProviderCoordinateMixin derive transiently, complementing the existing
literal-example contract in test_lockfile_consumer_contract.py.
Twelve property groups cover:
- Generic field-complete to_dict()/from_dict() round trip + idempotent
re-serialization, with a mutation-break negative twin (dropped field).
- Optional-string-field omission-when-falsy contract (26 fields).
- port defensive-cast fail-closed to None on malformed/out-of-range input,
plus valid-port-survives-round-trip.
- host_type/exec_status fail-closed via ValueError (not silent drop), plus
a mutation-break twin proving the guard is load-bearing.
- ADO transient coordinate derivation (host + repo_url only) round-trips,
never persists in to_dict(), is idempotent under double derivation, and
is gated purely by host (not repo_url shape) -- each with a
mutation-break twin.
- Mismatched explicit ado_* fields always fail closed combinatorially;
matching fields never raise.
- Retired/foreign ado_* keys in raw lock dict never resurrect, guarded by
ProviderCoordinateMixin.is_transient_provider_field (mutation-break twin
included).
- alias (a declared-manifest-only concept) never persists into lock state,
including via a leaked _unknown_fields path (mutation-break twin).
- skill_subset/target_subset permutation-invariant deterministic sort on
write, with a mutation-break twin targeting to_dict() itself (the true
sort owner -- from_dependency_ref's pre-sort is redundant defense).
- Registry-vs-git-semver reference-selection branch in to_dependency_ref().
- Full LockFile.write()/read() YAML round trip sanity for a generated
dependency.
Every property drives the real production functions (to_dict, from_dict,
to_dependency_ref, canonical_ado_coordinates,
with_derived_provider_coordinates, validate_provider_coordinates) via
hypothesis generation or targeted monkeypatching of the original
function's *output*; none re-implement parsing/normalization logic,
preserving the canonical-owner law (LockedDependency owns persisted
state; DependencyReference/ProviderCoordinateMixin own transient
derivation).
No production defects found -- all three canonical-owner functions
(lockfile.py's to_dict/from_dict, reference.py's canonical_ado_coordinates,
provider_coordinates.py's is_transient_provider_field) behaved correctly
under generative and mutation-break pressure.
Registers the new module in tests/quality/critical_suite.toml (bumping
the finite-manifest count ratchet in test_test_taxonomy.py) since it
carries pytest.mark.unit, matching sibling lockfile test files.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a5817764-86d0-43c5-a034-9eb60c855e71
* test(deps): add host_type fail-closed property to match exec_status coverage
The apm-review-panel fan-out (python-architect, supply-chain-security-expert,
and test-coverage-expert -- three independent personas) each flagged the same
gap: Group 4's section header claimed host_type/exec_status fail-closed
coverage, but only exec_status had a dedicated hypothesis property test and
mutation-break twin. _normalize_lockfile_host_type shares the identical
raise-on-invalid structure (lockfile.py:96-108).
Adds test_unsupported_host_type_raises_instead_of_silently_coercing and its
mutation-break twin test_host_type_fail_closed_property_breaks_if_normalizer_is_bypassed,
mirroring the exec_status pair. The allowed-values strategy sources from the
real accepted_host_types() provider registry rather than a hand-maintained
literal, so it cannot drift from production.
23 tests now pass (was 21); full focused set 95 passed; full maintained
suite (tests/unit tests/test_console.py tests/red_team) 19255 passed, 2
skipped, 21 xfailed; ruff/pylint R0801 clean.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a5817764-86d0-43c5-a034-9eb60c855e71
* feat(mutation): add lockfile-reconstruction owner to bounded mutmut pilot
Extend the merged #2241 bounded mutmut pilot with a fifth owner covering
the fail-closed field normalizers in src/apm_cli/deps/lockfile.py that
#2246 (danielmeppiel-lockfile-property-proof) now defends with 23 property
tests plus 7 mutation-break twins.
Owner scope: _normalize_lockfile_host_type and _normalize_exec_status only.
LockedDependency.to_dict/from_dict/to_dependency_ref were originally
considered, but mutmut 3.6.0 structurally skips mutating any method of a
dataclass-decorated class (file_mutation.py _skip_node_and_children
returns True for any decorated ClassDef). Both LockedDependency and
LockFile are dataclasses, so those methods can never be mutated regardless
of only_mutate/pattern configuration -- confirmed via direct meta
inspection, a mutmut AssertionError when filtered to those patterns, and a
minimal reproduction isolating the dataclass decorator as the cause. The
two normalizers are the sole mutation-viable seam and are the
authoritative fail-closed validation invoked directly from from_dict, so
they are the correct scope for defending the reconstruction surface.
Fresh mutation execution (non-cached, reproduced 3x including one
--reuse-cache run, all with identical totals and zero fatal/timeout/
suspicious outcomes): 27 mutants, 25 killed, 2 survivors for the new
owner. The pre-existing four owners' totals are preserved byte-for-byte
(480/448/32: subset-selection 144/121/23, update-plan 153/144/9,
policy-serialization 139/139/0, link-projection 44/44/0). Aggregate:
507 mutants / 473 killed / 34 survivors. Runtime 45-73s, well within the
20-minute CI budget.
Survivor classification:
- 8 originally-surviving mutants in the "must be a non-empty string" raise
branch were reachable behavioral gaps: hypothesis's _BAD_EXEC_STATUS and
_BAD_HOST_TYPE strategies use min_size=1 and so never exercise the
empty-string path. Killed with two new exact-equality tests (substring
match does not catch mutmut's XX-wrap string mutation).
- 2 survivors mutate the join separator used to format the sorted
"Supported values: ..." diagnostic list. Classified as
equivalent/out-of-contract: purely cosmetic error-text formatting with
no behavioral consumer. Explicitly baselined in tests/mutation/baseline.json
rather than pinning exact message formatting as a behavioral contract.
Also updates two stale "four stable owners" references in CONTRIBUTING.md
and docs/.../development-guide.md to "five stable owners".
No production code in src/apm_cli/deps/lockfile.py was changed.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 53c17f8d-8e7e-47b4-bd78-8a5d18503da9
* fix(mutation): rename lockfile-reconstruction owner to lockfile-normalization
mutmut 3.6.0 structurally cannot mutate methods of a @dataclass-decorated
class (file_mutation.py's `_skip_node_and_children` skips any decorated
ClassDef), so this owner never covered LockedDependency's dataclass
methods (to_dict/from_dict/to_dependency_ref) -- only the two bare
module-level fail-closed normalizers. The prior key/docs used
"reconstruction" language that implied broader coverage than the owner
actually provides. Renamed to "lockfile-normalization" everywhere
(runner Owner key, test assertions, baseline.json, CONTRIBUTING.md,
development-guide.md) and reframed the inline comment/prose to:
- explicitly state this owner does NOT cover to_dict/from_dict/
to_dependency_ref,
- cite PR #2246's seven manual mutation-break twins in
test_lockfile_field_properties.py as the current defense for that
decorated-dataclass surface, and
- record the mutmut decorated-class limitation as a documented
follow-up constraint rather than an incidental aside.
Also fixes a real (pre-existing, not merge-introduced) collision with
tests/quality/test_test_taxonomy.py's TM002 guard: PR #2246 added
tests/unit/deps/test_lockfile_field_properties.py to
tests/quality/critical_suite.toml's critical-module manifest, and this
owner's test_seams tuple held that same path as an exact Python string
literal -- which the guard (correctly) flags as a second file
declaring a critical-manifest path. The two concepts (mutation-pilot
seam bookkeeping vs. critical-suite membership) are unrelated; split
the literal into a runtime concatenation so the resulting string is
byte-identical but no longer a single AST string constant matching the
manifest path. No behavior change; verified via
test_tm002_manifest_is_the_only_critical_module_list and a direct
value-equality check.
Re-verified after this change: fresh non-cached mutation run (507
mutants, 34 survivors, all owners "accepted", zero fatal outcomes) and
a --reuse-cache run producing an identical report modulo runtime
timing -- confirming report parity survives the rename. Original four
owners still show the preserved 480 mutants / 448 killed / 32 survivor
truth (144/153/139/44 totals); lockfile-normalization contributes 27
mutants / 25 killed / 2 baselined survivors on top.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 53c17f8d-8e7e-47b4-bd78-8a5d18503da9
* chore: trigger CI re-run against retargeted base (main)
The pull_request CI workflow (.github/workflows/ci.yml) filters on
branches: [main] with default event types (opened/synchronize/reopened).
Retargeting PR #2251's base to main did not itself fire a new run since
it is neither a synchronize nor an opened/reopened event. This empty
commit produces a synchronize event so the full required check suite
(lint, tests, mutation-pilot workflow path) runs against main as base.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 53c17f8d-8e7e-47b4-bd78-8a5d18503da9
---------
Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>1 parent ccbd402 commit 3b90615
8 files changed
Lines changed: 135 additions & 9 deletions
File tree
- .github/workflows
- docs/src/content/docs/contributing
- scripts
- tests
- mutation
- unit
- deps
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| 60 | + | |
60 | 61 | | |
61 | 62 | | |
62 | 63 | | |
63 | 64 | | |
64 | 65 | | |
65 | 66 | | |
66 | 67 | | |
67 | | - | |
| 68 | + | |
| 69 | + | |
68 | 70 | | |
69 | 71 | | |
70 | 72 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
206 | 206 | | |
207 | 207 | | |
208 | 208 | | |
209 | | - | |
210 | | - | |
211 | | - | |
212 | | - | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
213 | 218 | | |
214 | 219 | | |
215 | 220 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | | - | |
106 | | - | |
107 | | - | |
108 | | - | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
109 | 113 | | |
110 | 114 | | |
111 | 115 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
| 171 | + | |
171 | 172 | | |
172 | 173 | | |
173 | 174 | | |
| |||
186 | 187 | | |
187 | 188 | | |
188 | 189 | | |
| 190 | + | |
189 | 191 | | |
190 | 192 | | |
191 | 193 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
149 | 149 | | |
150 | 150 | | |
151 | 151 | | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
152 | 194 | | |
153 | 195 | | |
154 | 196 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
15 | 29 | | |
16 | 30 | | |
17 | 31 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
363 | 363 | | |
364 | 364 | | |
365 | 365 | | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
366 | 394 | | |
367 | 395 | | |
368 | 396 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
68 | 97 | | |
69 | 98 | | |
70 | 99 | | |
| |||
0 commit comments