Skip to content

Skip absent test dependencies, and stop unit runs deleting the integration cache - #27

Merged
gaurav merged 10 commits into
mainfrom
fix-integration-test-skips
Sep 1, 2026
Merged

Skip absent test dependencies, and stop unit runs deleting the integration cache#27
gaurav merged 10 commits into
mainfrom
fix-integration-test-skips

Conversation

@gaurav

@gaurav gaurav commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Running the full test suite against a Babel release that actually publishes the DuckDB Parquet files produced 4 failures and 4 errors, and not one of them was a defect in the code under test. Chasing those turned up a fifth problem that had been corrupting the runs themselves: any pytest invocation deleted the integration cache, including the -m "not integration" loop you run constantly while editing.

This PR makes the suite report absent dependencies honestly and stop sabotaging itself, so that a red run means a real problem again. It is tests and documentation only — no change to src/.

Closes #19.

What's here

A file being published is settled per file, not per session. A release can publish Concord.parquet without Identifiers.parquet — the current one does exactly that, serving a 4.6 GB Concord with no Identifiers beside it — so "does this release have the Parquet files?" is not one question. Every DuckDB file now goes through _download_or_skip(), which skips on the MissingBabelFileError the downloader already raises for a 404 on a duckdb/ path. shared_downloader is left answering only what is genuinely session-wide — is the server reachable — and probes the release root rather than a named file, so it cannot be misread as a publication check that forgot to check anything.

A unit-test run no longer deletes the integration cache. pytest_sessionfinish removed data/test unconditionally, and that path is fixed rather than per-run, so any session did it. A pytest -m "not integration" in one terminal silently destroyed a multi-gigabyte download in progress in another, and the integration run simply started over — which reads as a downloader defect rather than an unrelated pytest two windows away. The cleanup now runs only when the session actually selected integration tests. The unit suite never creates or reads that directory, so it has no business removing it.

An unreachable NodeNorm skips instead of failing. The nodenorm fixture only constructed a client, so a connection error surfaced inside each test body and failed it. CI runs integration-test on every push to main and on a weekly cron, so a RENCI outage turned the build red for reasons unrelated to any code change — the exact failure the Babel-side skip was written to avoid. The fixture now probes status once and skips on a RequestException.

NCIT:C55060 is removed from the test CURIEs. It has no rows at all in Concord.parquet as of Babel 2026jul22, so the four tests parametrized over it asserted len(results) > 0 against an empty list. NodeNorm still resolves it — as "Hypertension, CTCAE", a biolink:PhenotypicFeature with a clique of 2, from Babel 2025sep1 — so this is not a typo or a retired identifier, and something changed between those two releases. That question is #29, not this PR's: whether Babel was right to drop the cross-references is a question about Babel's data, and if a good one went for a bad reason it belongs upstream. valid_curies.txt keeps a two-line pointer so the short list explains itself.

Identifiers.parquet is checked for being a Parquet file, not for being large. The assertion was size > 2 GB, a figure picked against a release long since superseded. It would fail a legitimately smaller future release while passing a 2.5 GB truncated download. It now checks the PAR1 marker at both ends, which tests what actually goes wrong: a download cut short, or an error page saved under a .parquet name.

What it produces

Against a release that publishes the Parquet files, the full suite is 299 passed, 3 skipped, 0 failures, 0 errors (16m31s, dominated by the 4.6 GB download). It was 296 passed / 4 failed / 4 errors / 28 skipped before. The 25 tests that turned from skips into passes are the Parquet-dependent ones that had never actually run — they now exercise real data, including the DuckDB query paths and the recursive expansion. Against a release without the files, the suite still skips cleanly, as documented.

Nine new unit tests cover the three guards, because this whole class of bug is otherwise invisible: these code paths only execute during an integration run, and CI's integration job skips, so nothing would notice a guard being dropped. It is the same blind spot that hid ten stale get_curie_xref.cache_clear() calls until a review found them by reading rather than running. Each guard was confirmed non-vacuous by removing it and watching the test fail.

Four things that cost real time are now in CLAUDE.md: that a release can publish one DuckDB file without the other; that pytest_sessionfinish deletes data/test so a full run re-pays the download; that two concurrent integration runs still clash, with how to detect, avoid and recover; and that addopts carries -n auto, so a serial run needs -n0 rather than -p no:xdist.

What it deliberately does not do

Nothing is blocking this merge

Everything outstanding is tracked in #18, #26, #28 and #29, and none of it makes what ships here wrong. #29 is the one worth reading before merging: it records that the test CURIE list is now two entries, both MONDO:, so the integration tests exercise no other namespace until a third is restored. That is reduced coverage, not a defect in this change.

How this landed — two things reached their final shape by correction rather than design. Kept for anyone tracing why a line looks the way it does; the durable conclusions are in the code comments and CLAUDE.md above.

The per-file skip arrived in two steps. The Identifiers.parquet skip was first added inline in the downloaded_identifiers fixture, duplicating the MissingBabelFileError handling that shared_downloader was doing with a HEAD probe. That left two mechanisms for one rule, which prompted collapsing both onto _download_or_skip() and reducing shared_downloader to a reachability check — and then re-pointing that check at the release root, since a probe naming Concord.parquet still looked like the publication check it had stopped being.

The cache deletion was misdiagnosed twice before the cause turned up. The symptom was a full run whose data/test shrank instead of growing and that never finished. It was blamed first on the new "a resume requires a validator" rule in the downloader — wrong, the server sends both an ETag and a Last-Modified, so that rule never fired — and then on two concurrent full suites, which was real but not the cause. The actual culprit was the unit suite: every pytest -m "not integration" run during the investigation deleted the download it was investigating. That is why pytest_sessionfinish now carries an explicit "do not remove this guard" comment naming the misdiagnosis, and why CLAUDE.md describes the symptom rather than just the rule — the failure mode is much easier to recognise than to reason out.

gaurav and others added 8 commits September 1, 2026 11:25
shared_downloader probes Concord.parquet and skips the session on a 404, but a
release can publish one DuckDB file without the other — 2026jul22 does exactly
that, serving a 4.6 GB Concord.parquet with no Identifiers.parquet beside it.
The downloaded_identifiers fixture had no equivalent guard, so four tests ended
in MissingBabelFileError out of fixture setup rather than skipping.

That reads as a broken test environment when it is the documented, expected
outcome for a release that does not publish the file. Catch the error the
downloader already raises for exactly this case and skip on it, which also
surfaces the downloader's own message about setting BABEL_RELEASES_URL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NCIT:C55060 has no rows at all in Concord.parquet as of Babel 2026jul22, so the
four tests parametrized over it asserted len(results) > 0 against an empty list
and failed. The other two CURIEs are unaffected.

Commented out rather than deleted, with the evidence beside it: NodeNorm still
resolves the CURIE — from Babel 2025sep1, with a clique of 2 — so this is not a
typo or a retired identifier, and something changed between those two releases.
Deleting the line would throw away the only record that it used to work, so it
stays in the file with a note to restore it once the cause is known.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…to see

The skip guard added in 584931a had no test, for the same reason the bug existed:
it only shows up in a run against a real Babel release, and those skip entirely
for anyone without one configured. Call the fixture function past its decorator
with a stubbed downloader, so the behaviour is exercised by the unit suite that
runs on every push — including that a non-404 failure still fails the run rather
than being swallowed as a skip.

Record two things in CLAUDE.md that cost time this session: a release can publish
Concord.parquet without Identifiers.parquet, so "does this release have the
Parquet files?" is two questions and not one; and pytest_sessionfinish deletes
data/test, so every full run re-pays the multi-gigabyte download.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two mechanisms had grown for one rule: shared_downloader HEADed Concord.parquet and
skipped the session on a 404, while downloaded_identifiers caught the downloader's
own MissingBabelFileError. The HEAD was also the wrong shape for the question — it
answered for the session what has to be answered per file, since a release can
publish Concord.parquet without Identifiers.parquet.

Route every DuckDB file through _download_or_skip, which skips on the error the
downloader already raises for exactly this case. shared_downloader keeps only the
guard that is genuinely session-wide: whether the server can be reached at all. A
release publishing Metadata.parquet but not Concord.parquet now runs the tests that
need only Metadata, rather than skipping them along with everything else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The nodenorm fixture just constructed a client, so a connection error surfaced
inside each test body and failed it. CI runs integration-test on every push to main
and on a weekly cron, so a RENCI outage turned the build red for reasons unrelated
to any code change — the exact failure the Babel-side skip was written to avoid.

Give the fixture the same treatment: probe `status` once, skip on a RequestException.
This cannot cover an outage that begins mid-run, but it covers the one that recurs.

Closes #19.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r 2 GB

The assertion was `size > 2 GB`, a hard figure picked against a release long since
superseded — the kind of number CLAUDE.md says drifts silently and then misleads,
and one that fails for a legitimately smaller future release while passing for a
2.5 GB truncated download.

Check the PAR1 marker at both ends instead. That tests what actually goes wrong: a
download cut short, or an error page saved under a .parquet name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The probe added in ae423c3 had no test, for the reason that made the bug it fixes
worth fixing: these fixtures only do anything during an integration run, and CI's
integration job skips, so nothing would notice the probe being dropped. Call the
fixture function past its decorator with requests.get patched — unreachable and 5xx
both skip, a reachable API still returns a client. Confirmed non-vacuous: removing
the probe fails the first two.

Also record in CLAUDE.md that addopts already carries `-n auto`, so a serial run
needs `-n0`; `-p no:xdist` leaves the parsed `-n` behind and pytest exits with
`unrecognized arguments: -n`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
shared_downloader HEADed duckdb/Concord.parquet and, until the per-file skip landed,
read the status to decide whether the release published the Parquet files. That
second job is now _download_or_skip's, and only the status told the two apart — so
what is left reads like a publication check that forgot to check anything. Point it
at the release root instead: reachability is a question about the server, not about
a file, and a 404 from a reachable server is still a reachable server.

Also warn in CLAUDE.md about running two full suites at once. data/test is a fixed
path shared by every run and pytest_sessionfinish deletes it unconditionally, so the
second run tears down the first one's download; the FileLock does not help, because
it guards one file rather than a session. The symptom is `du -sh data/test` going
down instead of up while a run that should take ~9 minutes never finishes, which is
easy to misread as a bug in the downloader. Recorded with how to detect, avoid and
recover from it, and the two stale descriptions of the old Concord probe are
corrected in the same pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pytest_sessionfinish removed data/test unconditionally, so any pytest invocation
did it — including `-m "not integration"`, the fast loop run constantly while
editing. That silently destroyed the multi-gigabyte download of an integration run
in another terminal, and the integration run simply started over, which looks like
a downloader defect rather than an unrelated pytest two windows away. It was
diagnosed as one twice during this work: first as a resume bug in the new
"no validator, no resume" rule, then as two concurrent full suites. Both were wrong;
the fast unit suite was doing it.

Skip the cleanup when the session selected no integration tests. The unit suite
never creates or reads data/test, so it has no business deleting it. Two concurrent
integration runs still clash — unavoidable while the path is shared, and now
documented with how to detect, avoid and recover from it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gaurav gaurav changed the title Stop the integration suite failing on absent services and missing Babel data Skip absent test dependencies, and stop unit runs deleting the integration cache Sep 1, 2026
It was commented out with the evidence inline, pending a decision. The decision is
that this is not a blocker for the test-infrastructure work: whether Babel was right
to drop the cross-references is a question about Babel's data, and if a good xref
went for a bad reason it belongs upstream rather than here.

Delete the CURIE and keep a two-line pointer to #29, which carries the evidence and
covers restoring a third CURIE — with only MONDO:0004979 and MONDO:0005044 left, the
integration tests no longer exercise any non-MONDO namespace, which is the coverage
NCIT:C55060 was actually providing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gaurav
gaurav merged commit a565574 into main Sep 1, 2026
3 checks passed
@gaurav
gaurav deleted the fix-integration-test-skips branch September 1, 2026 16:52
gaurav added a commit that referenced this pull request Sep 1, 2026
…t assumes (#31)

`CLAUDE.md` had grown to 396 lines by accretion — each hard-won rule
from a review round appended wherever it fit. It was accurate about this
repository's internals and silent about the system those internals exist
to read: it never said what Babel is, what a clique or a concord is, or
why NodeNorm's version matters. An agent arriving at the
`Concord.parquet` query code had no model of the data.

This renames the file to `AGENTS.md`, gives it that missing context,
moves the deep caching invariants to `docs/`, and removes what was being
said twice. **Documentation only — no change to `src/`.**

## What's here

**Renamed to `AGENTS.md`**, the filename coding agents other than Claude
Code look for; the guidance was never Claude-specific. `CLAUDE.md`
remains as a four-line pointer, so Claude Code still finds it. Four test
docstrings that referenced the old name are updated. The rename is the
first commit, so the substantive edits that follow read as edits rather
than as a delete plus an add.

**A Domain context section** covering only the terms this codebase
actually uses — clique, preferred identifier, concord, conflation,
Biolink type — and NodeNorm's relationship to a specific Babel release.
It is a summary with links rather than copied upstream prose, so it
cannot drift from upstream unnoticed.

The part that earns the most space is the boundary between concords and
cliques. Babel's own `AGENTS.md` says to answer clique-membership
questions from a finished build *"never from the concords that fed it"*
— and concords are exactly what this tool reads. That is not a defect,
it is the purpose: `xrefs` reports the evidence Babel read, which is
what you want when a merge looks wrong, while NodeNorm reports the
verdict. Written down explicitly so that nobody later "improves" `xrefs`
into a clique oracle.

**The caching invariants moved to
[`docs/Downloading.md`](docs/Downloading.md)** — near-verbatim, because
the wording is hard-won and each rule records a specific failure that
actually happened. `AGENTS.md` keeps a four-sentence summary and a
pointer. That is ~90 lines an agent needs rarely, which had been sitting
above the orientation it needs every session.

**Deduplication.** The DuckDB spill directory was described in three
places, the caching model in three, and the
never-commit-the-internal-URL rule in three. Each is now stated once,
where it belongs. `## Important Notes` is deleted outright — all three
of its bullets restated earlier sections. The command examples now point
at `README.md`, which documents every flag and is the user-facing
reference.

**Four factual corrections:**

- `MissingBabelFileError` is raised on **any** 404, not only for
`duckdb/` paths, as the file claimed in two places. `downloader.py:546`
has no path condition.
- CI budgeting pointed only at #18; #28 covers the same ground more
precisely.
- "recreated as #20-#24" predated #20 merging.
- `README.md`'s configuration table was missing
`BABEL_ALLOW_VERSION_MISMATCH`, which `env.default` has carried since
#27.

## What upstream does not document

Worth recording, because it shaped what could honestly be written.
Neither Babel's `README.md` nor its `releases/ARTIFACTS.md` describes
`Concord.parquet`, `Identifiers.parquet` or `Metadata.parquet`.
`conflate` / `drug_chemical_conflate` and the `/status` `babel_version`
field are absent from NodeNormalization's README.

So the file schema in the new section is what **this repository's code
assumes**, verified against real files — and it is labelled that way
rather than presented as a published contract. Release naming, `latest`
and `VERSION.txt` are undocumented upstream too. Filed upstream as
[NCATSTranslator/Babel#1077](NCATSTranslator/Babel#1077),
which links back to the Domain context section as the shortest statement
of what is missing.

## What it deliberately does not do

- **Document `search-xrefs`** — that is #21, not on `main`.
- **Rewrite the version-skew wording** — #30 will change that behaviour;
editing the prose now means editing it twice.
- **Split Architecture or Testing into `docs/`** — those are context an
agent needs often, and spreading them costs more than the length saves.

## Verification

272 unit tests pass and ruff is clean (no source changes, so both should
be unchanged). Every relative link and intra-file anchor in `AGENTS.md`,
`docs/Downloading.md` and `CLAUDE.md` resolves, no `.py` file still
references `CLAUDE.md`, and the symbols the reworked text names were
re-checked against the code.

Nothing is blocking this merge.

**One thing to judge:** `AGENTS.md` is 370 lines, down from 396, with a
further 84 in `docs/`. Total documentation went *up*, because the Domain
context section came out at 61 lines rather than the ~25 originally
scoped. The extra went on the concord/clique boundary above. If that
reads as too much for an agent file, the obvious trim is `## Testing`
(73 lines, now the largest section).
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.

NodeNorm integration tests fail instead of skipping when the API is unreachable

1 participant