Skip to content

test: Phase 0 verification harness for the 4.0.0 audit remediation - #113

Merged
isayev merged 21 commits into
mainfrom
phase0/verification-harness
Jul 30, 2026
Merged

test: Phase 0 verification harness for the 4.0.0 audit remediation#113
isayev merged 21 commits into
mainfrom
phase0/verification-harness

Conversation

@isayev

@isayev isayev commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Builds the verification harness that the six audit-remediation phases will be measured against. No production code changesgit diff against the base is empty under src/.

The mechanism

Each of the 28 new tests asserts the behavior the code should have, which today's code violates, and carries @pytest.mark.xfail(strict=True, reason="<finding-id>: ..."). The test fails now, so CI stays green. When the owning phase fixes the defect the test XPASSes, and strict=True turns that into a hard failure — forcing the marker's removal in the same PR. The markers are tripwires in the code rather than a checklist in a document.

docs/superpowers/plans/2026-07-30-phase0-red-list.md maps every marker to its owning phase, with handoff notes for the traps a fix could otherwise walk into.

Coverage

28 tripwires — 19 fast-tier, 6 slow-tier, 3 requiring torchani. They cover the stereochemistry identity defects (E/Z isomers discarded as enantiomers; tautomer enumeration erasing specified centers), the ANI2xt atomic-number/species-index mismatch, the thermochemistry Hessian-geometry mismatch, silent molecule loss with a zero exit code, model-load failures misreported as convergence failures, and the configuration validation gaps.

Also fixed

Six tests that could never fail: three that re-implemented production logic in the test body or grepped inspect.getsource, a duplicated TestCombineSmi class that shadowed an earlier one so its test never ran, and an assert ... or True. The ruff config now keeps F811 and SIM222 enabled for tests/ so these cannot recur silently.

New passing coverage where there was none: analytic force values on the toy NNP (a sign flip previously passed the gate), per-engine padding invariance, and the atomic-rewrite failure path in reorder_sdf.

CI

The slow job now runs the full -m slow tier instead of three files — five end-to-end modules previously ran in no job at all. A model-cache warm step hard-fails when the registry is unreachable, so an unavailable gate can never read as a passing one. tests/ is now linted.

Verification

680 passed, 9 skipped, 19 xfailed, 0 xpassed, 0 failed. ruff check src/Auto3D/ tests/ clean.

Note: 9 of the 28 tripwires have not been executed — they need a real model or torchani, unavailable on the development box — and are asserted red by source-level tracing. strict=True fails safe there: an unexpectedly-passing test goes red rather than dark. The red list names four specific items to watch on the first CI slow run.

isayev added 21 commits July 30, 2026 11:31
… fixtures

Markers were declared in both conftest.py and pyproject.toml and could drift.
The blanket DeprecationWarning/UserWarning ignores hid warnings that are part
of Auto3D's public contract, so no test could assert them.

Adds job_dir/isolated_input fixtures so pipeline tests stop sharing output
directories -- the shared state behind the ordering flakiness that kept the
end-to-end modules out of CI.
test_convergence_mask_excludes_oscillating and
test_convergence_with_energy_stability re-implemented batchopt's flag
derivation in the test body and asserted it against itself, so neither could
ever fail. test_run_method_includes_gpu_cleanup asserted on the output of
inspect.getsource -- a source-text grep, not behavior.

Replaced with a test that runs the optimizer and asserts the invariant: a
structure dropped for oscillation is never reported Converged=True.
Fix round 1 review finding: the new convergence-flag test asserted the
Converged/Dropped_Oscillating invariant only inside a branch with no check
that the branch was ever reached, so it could pass without exercising the
oscillation path at all on some platform/torch version.

Split into two assertions: the invariant now holds unconditionally over every
output molecule (so it can never no-op), and a separate assertion confirms the
oscillation path was actually taken, with a message that attributes failure to
the fixture rather than the invariant.
The custom-NNP tests used E = sum(coords^2), where F = -2*coords is exactly
checkable, but asserted only shape and finiteness -- so a sign flip in the
adapter force path passed the fast gate. Now asserts values.

Adds padding invariance per engine, which is the only test that would catch a
change in torchani's -1 masked-atom convention that ANI2xt_no_rep.py documents
as an unverified dependency.
Auto3D currently discards one geometric isomer of any achiral molecule with
unspecified C=C stereo, because enantiomer([], []) returns True vacuously and
FindMolChiralCenters never reports double-bond stereo. Verified: CC=CC
enumerates both E and Z, and enantiomer_helper keeps only C/C=C/C. Fumaric and
maleic acid collapse the same way despite differing by ~5 kcal/mol.

Tautomer enumeration separately strips specified sp3 stereo: none of the five
tautomers of C[C@H](C(=O)C)N retains the center.

All marked xfail(strict=True) so CI stays green until Phase 2 fixes them, at
which point the XPASS forces the marker's removal.

Also marks test_enantiomer_helper_keeps_non_chiral, which asserted the buggy
behavior was correct.
The M19 test previously drove bare RDKit ETKDG on a SMILES-built mol,
never touching Auto3D's RDKitSdfIsomer/RDKitSdfIsomerAdapter. That made
it permanently decoupled from a Phase 2 fix: it could never XPASS, so
it could never force the xfail marker's removal.

It now writes a genuine flat SDF into job_dir and drives it through the
real create_isomer_engine("rdkit_sdf", ...) -> RDKitSdfIsomerAdapter ->
RDKitSdfIsomer.run() path, then inspects Auto3D's own output SDF grouped
by species name. Both R and S still land as numbered conformers under
one species name, so the test keeps xfailing for the right reason.

Also drops the false C9 coverage claim from the module docstring (no
test here exercises it), and clarifies in the C2 test's docstring that
this stereocenter is genuinely epimerizable via its own enolization --
the bug is SetRemoveSp3Stereo(True) stripping it indiscriminately from
every tautomer, including ones enolized through the ketone's other,
non-stereogenic alpha carbon.
reorder_sdf's tmp+os.replace pattern and the del supp fix from 74474ed had no
test at all -- grep for 'os.replace' across tests/ returned nothing, so the fix
for the identified weak spot was the untested part. Now covered.

opt_geometry does the opposite of reorder_sdf: it truncates the file it just
read, so a failure mid-rewrite destroys a completed optimization. Marked
xfail(strict=True) pending Phase 6.
The writer stand-ins in TestReorderSdfDurability fully replaced Chem.SDWriter
and never touched disk, so both tests passed identically whether reorder_sdf
used the tmp+os.replace pattern or a regression that wrote directly to the
original path -- confirmed empirically that a direct-write regression
produced the same "survived" result as the real implementation under the old
stubs.

Both stand-ins now wrap the real SDWriter (same _real_sdwriter technique
already used by FlakyWriter in TestOptGeometryDurability) so the genuine
truncate-on-open and tmp-file creation happen against whatever path
reorder_sdf actually opens. Verified against two temporary, reverted
regressions: writing directly to the original path now fails
test_original_survives_a_writer_failure, and removing the tmp-file cleanup
now fails test_no_temp_file_is_left_behind.
A cold model cache behind a firewall is the single most likely first-run
failure, and it currently reports 'no 3D structure converged' with three
candidate causes -- memory, invalid SMILES, patience -- none of which applies,
because the model is constructed inside the spawned worker's blanket except.

Also covers a typo'd registry name (resolvable offline via a bundled YAML, so
validating it costs nothing) and a corrupted cache entry, which currently fails
identically forever with no hint to delete the file.
The two model-failure tests only exercised the worker's blanket except and
_finalize_output, never _validate_input. The natural fix for C8/M22 is a
parent-side pre-flight before spawning workers (workers run in separate
processes with no access to the parent's in-memory model/cache), so a fix
landing there would never be observed and the tests would stay xfail forever.

Each test now tries _validate_input() first and asserts on whatever it
raises; only when that passes through harmlessly (today's behavior) does it
fall through to the worker chain. A fix at either layer now makes the
corresponding test XPASS.
… M28)

CLIConfig constrains k, window, opt_steps, patience, threshold,
batchsize_atoms, mpi_np, capacity and memory, and forbids extra keys.
Auto3DOptions validates only k and window and only rejects strictly-negative --
so threshold=-1 is accepted end to end, which disables duplicate-conformer
removal while the output is still presented as deduplicated.

Also covers max_confs=0, k+window together, the charge guard missing from
calc_spe, and smiles2mols silently ignoring three documented options while
mutating the caller's config.

Also adds an M17 test (duplicate InChIKey inputs silently merging in
ranking's groupby, dropping one of two colliding SMILES) named in the
brief's own Findings line but missing from its Step 1 code.
ANI2xt is constructed with periodic_table_index=False at every site and nothing
passes True, so it expects 0-based indices H=0..Cl=6. Only padding.py converts;
ASE/thermo.py and the CLI health check pass raw atomic numbers, sending H(Z=1)
to the carbon network and C(Z=6) to the chlorine network, with N/O/F/S/Cl out of
range entirely.

ANI2x gets periodic_table_index=True at both its sites, which is why it is
unaffected -- that asymmetry is the evidence this is a defect, not a convention.
A run can lose 9 of 10 chunks and exit 0, because _finalize_output raises only
when zero outputs exist. One molecule with an element outside AIMNet2's set
takes its entire chunk down via a bare except/continue. And
find_smiles_not_in_sdf -- the reconciliation function that would catch all of
this -- exists, is exported, is tested, and has zero production callers.

Also adds the first end-to-end assertions on actual numbers. The existing
end-to-end test's docstring is 'Check that the program runs' with no assertion
on molecule count, conformer count, or energy.
test_every_input_is_present_or_reported previously ran 10 valid, mostly
duplicate-structure molecules through the default pipeline and hoped at least
one failed to converge -- a coin flip, not a defect demonstration, since 6 of
those 10 rows share structures already proven to converge reliably elsewhere
in the suite.

Rewritten to combine the guaranteed-unconvertible sodium counterion with
per-molecule job isolation (capacity=1, memory=1): the bad molecule's job now
fails alone via optim_rank_wrapper's bare except/continue while the good
molecules' jobs succeed independently, so the run produces real output (no
OptimizationError) but the failing ID silently vanishes with nothing to
reconcile it -- deterministic, not luck-dependent.

Also drops the unused _count_records helper.
The Hessian is taken at the pre-optimization geometry whenever BFGS runs,
because mol's conformer is synced only at the end of do_mol_thermo while the
energy and moments of inertia come from the relaxed atoms. Nothing signals the
mismatch, since the written coordinates are the relaxed ones.

Nothing checks opt.run()'s return, so G is reported for non-minima, and the
documented opt_tol is used only in a fallback branch. One malformed SDF record
aborts a whole batch because the accessors run before the try -- SPE.py filters
these for exactly this reason and thermo.py does not.
vib_hessian has exactly one production caller, do_mol_thermo, and the fix for
the stale-Hessian bug most plausibly lands as a reordered sync inside that
function rather than inside vib_hessian itself. Calling vib_hessian directly
would never observe a fix made at that layer, so the test now drives
do_mol_thermo and records what its real vib_hessian call returns via a
pass-through spy -- catching a fix at either layer while keeping the same
non-probabilistic geometry-identity assertion.
The slow job ran only three files; the end-to-end modules (test_auto3D,
test_SPE, test_thermo, test_isomer_engine, test_tauto) ran in no CI job at all,
so 55 of 744 test functions never executed -- including the only eV-vs-Hartree
guard and the only padded-slot force guard. Task 1's isolation fixtures remove
the shared output directories behind the ordering flakiness that motivated the
exclusion.

Adds a model-cache warm step that fails the job explicitly when the registry is
unreachable, so an unavailable gate can never be mistaken for a passing one.
Also lints tests/ and verifies clean collection separately, since
--continue-on-collection-errors hides import failures.
… rules

test_isomer_engine.py minted job directories from second-resolution
time.strftime with no disambiguator in two tests, while three later tests in
the same file already append a unique suffix for exactly this reason -- append
a uuid4 fragment at the two unpatched sites so a same-second collision or a
leftover directory from a prior failed run can no longer kill an unrelated
test.

Also drops F811 and SIM222 from the tests/ ruff narrowing added for CI wiring:
both were firing on real bugs, not style debt -- a duplicate TestCombineSmi
class silently shadowed one class's only test, and a CLI console test asserted
"... or True", which can never fail. Renamed the shadowed class and fixed the
assertion instead of silencing the rules that caught them.
28 xfail tests (27 strict=True, one strict=False by design), each failing
on a426cf4 and mapped to the phase that owns its fix per the audit
remediation design doc. When a phase lands, its strict tests XPASS and
strict=True forces the marker's removal in the same PR -- so the gate is a
tripwire in the code rather than a note in a PR description.

Also records the passing coverage Phase 0 adds for gaps that had no test at
all, why five findings are deliberately absent from the red list, per-phase
handoff notes surfaced during the task loop (M19's fix site, M8's property
names, C8/M22's XPASS conditions, M17's two independent sites), and watch
items for the first CI slow run.
- Rebind the C2 tautomer-stereo tripwire to drive Auto3D's real rdkit
  tautomer engine (create_tautomer_engine -> TautomerEngine.rd_taut)
  instead of a bare RDKit TautomerEnumerator, so the xfail reproduces an
  Auto3D defect rather than upstream RDKit behavior.
- Tighten the C1 enantiomer-helper marker in test_utils_stereochemistry.py
  from strict=False to strict=True; its body already asserts correct
  behavior, so the non-strict escape hatch was no longer justified.
- Correct the slow-tier CI comment to describe what actually protects the
  five newly un-skipped modules (job naming, serial execution, GPU
  teardown) instead of a job_dir/isolated_input fixture none of them use,
  and drop the no-op -p no:randomly flag.
- Add a non-vacuity guard to the M8 stationary-point-gating test so it
  cannot pass vacuously when a record never reports G_hartree, and fix an
  unrelated molecule-name/SMILES mismatch in the same fixture.
- Expand the phase0 red list with handoff notes for Phases 1, 3, and 5
  uncovered by this review (pad_from_mols 4-tuple fallout, Auto3DError vs
  bare ValueError, Thermo_failed naming precedent), two new slow-tier watch
  items, the deleted GPU-cleanup test, and a note on untracked working docs.

No production code changes; git diff a426cf4 HEAD -- src/ remains empty.
@isayev
isayev merged commit f3664e6 into main Jul 30, 2026
6 of 7 checks passed
@isayev
isayev deleted the phase0/verification-harness branch July 30, 2026 20:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant