Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions src/test/test_term_info_fallback_partial_doc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""The term_info fallback must fire on a document that has no term_info,
not only when there is no document at all.

The vfb_json collection is written by several indexers, each setting only
its own field with an atomic update. So a record that one of the
query-result indexers reaches before the term_info indexer does has a
document -- ``hits == 1`` -- with no ``term_info`` in it. Berg2025a and
Bates2026 were exactly that on 2026-09-04: ``all_datasets_query`` present
(listed under All Datasets, thumbnail and all), ``term_info`` absent (blank
term-info panel, ``get_term_info`` -> None), and the fallback never ran
because it was keyed on the hit count.

Offline: SOLR and the PDB rebuild are both stubbed, so these pin the
trigger and the plumbing, not the graph.
"""
import dataclasses
import json
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

from vfbquery import vfb_queries as vq
from vfbquery.term_info_queries import VfbTerminfo


DATASET_TYPES = ["Entity", "Individual", "DataSet", "has_image"]


def _term_info(short_form, label, types):
"""A minimal but schema-complete term_info document (every VfbTerminfo
field present, as the real SOLR documents are)."""
doc = {
"term": {
"core": {
"short_form": short_form,
"iri": "http://virtualflybrain.org/data/" + short_form,
"label": label,
"types": types,
"unique_facets": types,
"symbol": "",
},
"description": [],
"comment": [],
},
"version": "test",
}
for field in dataclasses.fields(VfbTerminfo):
doc.setdefault(field.name, None)
return json.dumps(doc)


class _SolrResults:
"""What pysolr hands back: ``hits`` plus a list of dict documents."""

def __init__(self, docs):
self.hits = len(docs)
self.docs = docs


# The shape SOLR actually held for Berg2025a: a document, no term_info.
PARTIAL_DOC = {
"id": "Berg2025a",
"all_datasets_query": ['{"dataset": {"short_form": "Berg2025a"}}'],
"template_2_roi_browser_query": ["[]"],
}


# ---------------------------------------------------------------------------
# The trigger
# ---------------------------------------------------------------------------

def test_no_document_is_a_miss():
assert vq._has_term_info_document(_SolrResults([])) is False


def test_a_document_without_term_info_is_a_miss():
"""The Berg2025a case. hits == 1, nothing to read."""
assert vq._has_term_info_document(_SolrResults([PARTIAL_DOC])) is False


def test_an_empty_term_info_field_is_a_miss():
doc = dict(PARTIAL_DOC, term_info=[])
assert vq._has_term_info_document(_SolrResults([doc])) is False


def test_a_real_term_info_document_is_a_hit():
doc = {"id": "Berg2025a",
"term_info": [_term_info("Berg2025a", "Male CNS", DATASET_TYPES)]}
assert vq._has_term_info_document(_SolrResults([doc])) is True


def test_the_fallbacks_own_stand_in_counts_as_a_hit():
"""_FallbackSolrResult is fed straight back through the same check."""
payload = _term_info("Berg2025a", "Male CNS", DATASET_TYPES)
assert vq._has_term_info_document(vq._FallbackSolrResult(payload)) is True


# ---------------------------------------------------------------------------
# get_term_info end to end, with SOLR and the rebuild stubbed
# ---------------------------------------------------------------------------

def test_get_term_info_rebuilds_a_partial_document(monkeypatch):
"""A document with other fields but no term_info must be rebuilt and
served, exactly as a wholly missing document is."""
payload = _term_info("Berg2025a",
"Male CNS version 1.0 connectome neurons from Berg et al. (2025).",
DATASET_TYPES)
asked = []
monkeypatch.setattr(vq.vfb_solr, "search",
lambda q, **kw: _SolrResults([PARTIAL_DOC]))
monkeypatch.setattr(vq, "backfill_term_info",
lambda short_form: asked.append(short_form) or payload)

result = vq.get_term_info("Berg2025a", preview=False)

assert asked == ["Berg2025a"], "the rebuild was not attempted"
assert result is not None, "a partial document still served nothing"
assert result["Id"] == "Berg2025a"
assert result["Name"].startswith("Male CNS version 1.0")


def test_get_term_info_does_not_rebuild_when_term_info_is_present(monkeypatch):
"""The fallback is for gaps only; a served document is left alone."""
doc = {"id": "Berg2025a",
"term_info": [_term_info("Berg2025a", "Male CNS", DATASET_TYPES)]}
asked = []
monkeypatch.setattr(vq.vfb_solr, "search", lambda q, **kw: _SolrResults([doc]))
monkeypatch.setattr(vq, "backfill_term_info",
lambda short_form: asked.append(short_form) or None)

result = vq.get_term_info("Berg2025a", preview=False)

assert asked == []
assert result is not None and result["Id"] == "Berg2025a"


def test_get_term_info_still_returns_none_when_the_rebuild_fails(monkeypatch):
"""No PDB node / no indexer for the type: behave as before, no crash."""
monkeypatch.setattr(vq.vfb_solr, "search",
lambda q, **kw: _SolrResults([PARTIAL_DOC]))
monkeypatch.setattr(vq, "backfill_term_info", lambda short_form: None)

assert vq.get_term_info("Berg2025a", preview=False) is None
29 changes: 26 additions & 3 deletions src/vfbquery/vfb_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -2574,6 +2574,25 @@ def __init__(self, term_info_payload):
self.docs = [{"term_info": [term_info_payload]}]


def _has_term_info_document(results):
"""True when the SOLR result carries a document with a ``term_info`` field.

A document can exist without one. The vfb_json collection is written by
several indexers with atomic updates, each setting only its own field,
so a record that one of the query-result indexers (``all_datasets_query``,
``anat_image_query``, ...) reaches before the term_info indexer does has
a document -- ``hits == 1`` -- but nothing to read. Berg2025a and
Bates2026 were exactly that on 2026-09-04: listed under All Datasets,
blank in term info. Test the field, not the hit count.
"""
if not getattr(results, "hits", 0):
return False
docs = getattr(results, "docs", None) or []
if not docs:
return False
return bool(docs[0].get("term_info"))


# term_info SOLR loaders: fetch one term's term_info doc by short_form and
# return it either as a deserialized object (attribute access) or as the raw
# JSON dict, whichever the caller works with.
Expand Down Expand Up @@ -2877,8 +2896,8 @@ def get_term_info(short_form: str, preview: bool = True, force_refresh: bool = F
try:
# Search for the term in the SOLR server
results = vfb_solr.search('id:' + short_form)
# SOLR has no term_info document for this id. That is routine for a
# record newer than the last successful run of the bulk indexer (the
# SOLR has no term_info for this id. That is routine for a record
# newer than the last successful run of the bulk indexer (the
# `precompute live query results` Jenkins job), which can be months
# behind: the term is fine in the PDB, it just has nothing to read.
# Build the document live from the indexer's own query and index it,
Expand All @@ -2889,7 +2908,11 @@ def get_term_info(short_form: str, preview: bool = True, force_refresh: bool = F
# there are no hits and returns the initialised skeleton, which then
# fails schema validation on Name/Id/Meta and is returned raw -- so a
# missing term used to surface as a truthy object with no Id, not None.
if not getattr(results, "hits", 0):
#
# And tested on the field, not the hit count: a document written by
# one of the other indexers (all_datasets_query et al.) exists with
# no term_info at all, and the parser turns that into a bare None.
if not _has_term_info_document(results):
fallback_payload = backfill_term_info(short_form)
if fallback_payload:
results = _FallbackSolrResult(fallback_payload)
Expand Down
Loading