Files
stack/tests/bcda/test_sync.py
kert af9c088b84
Some checks failed
CI / test (pull_request) Failing after 2m21s
Infra CI / zotero (pull_request) Successful in 17s
Infra CI / docs (pull_request) Successful in 14s
Infra CI / notebooks (pull_request) Successful in 59s
Infra CI / api (pull_request) Successful in 20s
Infra CI / mc (pull_request) Failing after 35s
Infra CI / bcda (pull_request) Successful in 16s
Infra CI / llm (pull_request) Successful in 15s
CI / lint (push) Successful in 39s
CI / test (push) Failing after 2m52s
Infra CI / notebooks (push) Successful in 1m3s
Infra CI / bcda (push) Successful in 1m46s
Infra CI / mc (push) Failing after 46s
CI / notebooks-smoke (push) Successful in 1m45s
Infra CI / zotero (push) Successful in 16s
Infra CI / api (push) Successful in 1m9s
Infra CI / docs (push) Successful in 1m44s
Infra CI / llm (push) Successful in 52s
CI / lint (pull_request) Successful in 35s
CI / notebooks-smoke (pull_request) Successful in 1m36s
fix(bcda): legacy cut-off fallback is the job's start, not its completion
A job recorded before manifests existed has no transactionTime. The
first version fell back to the instant the `complete` row was written
and called it "safe, slightly overlapping" — I had that backwards: a
_since later than the server's true cut-off is a gap, not an overlap.
The real store showed it: the sandbox stamps transactionTime as midnight
UTC of the run day, so a baseline that finished at 16:53 was being
credited with a 16:53 cut-off and every same-day delta (00:00) was read
as predating it. The fallback is now the job's first ledger row (the
request instant), which can only overlap; Store.job_started_at() reads
it. `stack bcda resume <key>` still replaces the fallback with the
server's value — done for the four pre-manifest jobs in the real store,
whose watermarks now read 2026-10-25T00:00:00Z.

refs #1139
2026-10-25 16:21:12 -04:00

765 lines
29 KiB
Python

"""bcda.sync — _since temporality checks and baseline/delta reconciliation.
Fixtures are cut from the vendored official v3 samples
(dev/vendor/bcda-site/v3_*.ndjson) and mutated into the shapes the API
can deliver: an adjusted claim (same control number, new FHIR id, newer
lastUpdated), a stale re-delivery, an exact duplicate, backfill for a
new enrollee, late data for an existing one, a NotFinalAction tombstone,
and a later baseline that retires rows.
"""
from __future__ import annotations
import copy
import hashlib
import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pyarrow.parquet as pq
import pytest
from bcda import sync
from bcda.store import Store
SAMPLES = Path(__file__).resolve().parents[2] / "dev" / "vendor" / "bcda-site"
CCN = "https://bluebutton.cms.gov/identifiers/CLM-CNTL-NUM"
FA = "https://bluebutton.cms.gov/fhir/CodeSystem/Final-Action"
# Later than every lastUpdated in the samples (newest is 2026-09-21), so a
# fixture's "newer version" really is newer.
T0 = datetime(2026, 10, 1, 12, 0, tzinfo=timezone.utc)
def _rows(rtype: str) -> list[dict]:
return [
json.loads(line)
for line in (SAMPLES / f"v3_{rtype}.ndjson").read_text().splitlines()
if line
]
def _ccn(eob: dict) -> str:
return next(i["value"] for i in eob["identifier"] if i.get("system") == CCN)
def _set_fa(eob: dict, code: str) -> None:
for t in eob["meta"]["tag"]:
if t.get("system") == FA:
t["code"] = code
def _lu(res: dict, dt: datetime) -> None:
res["meta"]["lastUpdated"] = sync.format_instant(dt)
def add_job(
store: Store,
key: str,
rows_by_type: dict[str, list[dict]],
*,
transaction_time: datetime,
since: str = "",
endpoint: str = "Group/all",
type_filter: str = "",
parts: int = 1,
) -> None:
"""A complete job on disk: ndjson parts, job + file rows, manifest."""
d = Path(store.root) / "exports" / key
d.mkdir(parents=True, exist_ok=True)
types = ",".join(rows_by_type)
if type_filter:
types += f"|_typeFilter={type_filter}"
for rtype, rows in rows_by_type.items():
chunks = [rows[i::parts] for i in range(parts)] if parts > 1 else [rows]
for n, chunk in enumerate(chunks):
name = f"{rtype}.ndjson" if n == 0 else f"{rtype}.{n}.ndjson"
(d / name).write_text("".join(json.dumps(r) + "\n" for r in chunk))
store._append_file(
job_key=key,
resource_type=rtype,
url=f"https://s/data/{key}/{name}",
storage_path=str(d / name),
file_size=(d / name).stat().st_size,
status="complete",
)
manifest = {
"transactionTime": sync.format_instant(transaction_time),
"request": f"https://s/api/v3/{endpoint}/$export?_type={','.join(rows_by_type)}"
+ (f"&_since={since}" if since else ""),
"output": [],
}
store._append_job(
key=key,
job_url=f"https://s/api/v3/jobs/{key}",
endpoint=endpoint,
types=types,
since=since,
status="complete",
transaction_time=manifest["transactionTime"],
request=manifest["request"],
)
store._write_manifest(key, manifest)
def _digest(store: Store, table: str) -> str:
"""Order-independent fingerprint of a current table's business columns."""
t = pq.read_table(Path(store.root) / "flat" / f"{table}.parquet")
cols = [c for c in t.column_names if not c.startswith("_")]
rows = sorted(
json.dumps(r, sort_keys=True, default=str) for r in t.select(cols).to_pylist()
)
return hashlib.sha1("\n".join(rows).encode()).hexdigest()
@pytest.fixture
def base_store(tmp_path: Path) -> Store:
"""Three single-type baselines, like a real v3 pull."""
store = Store(path=str(tmp_path / "store"))
add_job(store, "B_PAT", {"Patient": _rows("Patient")}, transaction_time=T0)
add_job(
store,
"B_COV",
{"Coverage": _rows("Coverage")},
transaction_time=T0 + timedelta(minutes=1),
)
add_job(
store,
"B_EOB",
{"ExplanationOfBenefit": _rows("ExplanationOfBenefit")},
transaction_time=T0 + timedelta(minutes=2),
parts=2,
)
return store
def _delta_rows(
base_eobs: list[dict], base_patients: list[dict], since: datetime
) -> tuple[list[dict], dict]:
"""A delta payload against *base_eobs* and what it should classify as."""
finals = [
e
for e in base_eobs
if any(t.get("code") == "FinalAction" for t in e["meta"]["tag"])
]
out: list[dict] = []
# 3 adjusted: same CCN, new id, newer lastUpdated
for e in finals[:3]:
a = copy.deepcopy(e)
a["id"] = a["id"] + "-adj"
_lu(a, since + timedelta(hours=1))
out.append(a)
# 1 stale: same CCN, older lastUpdated, different id
s = copy.deepcopy(finals[3])
s["id"] = s["id"] + "-old"
_lu(s, sync.parse_instant(finals[3]["meta"]["lastUpdated"]) - timedelta(days=1))
out.append(s)
# 2 exact duplicates (overlap re-delivery)
out.extend(copy.deepcopy(finals[4:6]))
# 1 tombstone: newer NotFinalAction version of an existing claim
tomb = copy.deepcopy(finals[6])
tomb["id"] = tomb["id"] + "-void"
_set_fa(tomb, "NotFinalAction")
_lu(tomb, since + timedelta(hours=2))
out.append(tomb)
# 1 late: brand-new claim for an existing bene, lastUpdated before since
late = copy.deepcopy(finals[7])
late["id"] = late["id"] + "-late"
for i in late["identifier"]:
if i.get("system") == CCN:
i["value"] = i["value"] + "L"
_lu(late, since - timedelta(days=1))
out.append(late)
# 1 backfill: brand-new claim for a brand-new bene, lastUpdated before since
back = copy.deepcopy(finals[8])
back["id"] = back["id"] + "-back"
for i in back["identifier"]:
if i.get("system") == CCN:
i["value"] = i["value"] + "B"
back["patient"] = {"reference": "Patient/-999999"}
_lu(back, since - timedelta(days=3))
out.append(back)
# 1 new claim after since for an existing bene
new = copy.deepcopy(finals[9])
new["id"] = new["id"] + "-new"
for i in new["identifier"]:
if i.get("system") == CCN:
i["value"] = i["value"] + "N"
_lu(new, since + timedelta(hours=3))
out.append(new)
expect = {
"rows": 10,
"new": 3, # late + backfill + new
"replaced": 4, # 3 adjusted + tombstone
"unchanged": 2,
"stale_ignored": 1,
"backfill": 1,
"late": 1,
"tombstoned": 1,
}
return out, expect
# ── instants ──────────────────────────────────────────────────
class TestInstants:
def test_parse_and_format(self) -> None:
dt = sync.parse_instant("2026-10-01T00:00:00-05:00")
assert dt.utcoffset() == timedelta(hours=-5)
assert sync.format_instant(dt) == "2026-10-01T05:00:00.000+00:00"
assert sync.parse_instant("2026-10-01T00:00:00.123Z").microsecond == 123000
for bad in ("2026-10-01", "2026-10-01T00:00:00", "yesterday", ""):
with pytest.raises(ValueError):
sync.parse_instant(bad)
with pytest.raises(ValueError):
sync.format_instant(datetime(2026, 1, 1))
def test_split_types(self) -> None:
assert sync.split_types("Patient,Coverage") == (["Patient", "Coverage"], "")
assert sync.split_types(
"ExplanationOfBenefit|_typeFilter=ExplanationOfBenefit?_tag=x"
) == (
["ExplanationOfBenefit"],
"ExplanationOfBenefit?_tag=x",
)
assert sync.split_types("") == (list(sync.RESOURCE_TYPES), "")
# ── watermarks and checks ────────────────────────────────────
class TestChecks:
def test_next_since_is_the_watermark(self, base_store: Store) -> None:
ns = sync.next_since(base_store)
assert ns == {
"Patient": "2026-10-01T12:00:00.000+00:00",
"Coverage": "2026-10-01T12:01:00.000+00:00",
"ExplanationOfBenefit": "2026-10-01T12:02:00.000+00:00",
}
assert (
sync.next_since(base_store, overlap_seconds=60)["Patient"]
== "2026-10-01T11:59:00.000+00:00"
)
assert sync.next_since(base_store, endpoint="Group/runout") == dict.fromkeys(
sync.RESOURCE_TYPES
)
assert sync.next_since(base_store, endpoint="Patient") == ns
def test_empty_store_blocks(self, tmp_path: Path) -> None:
store = Store(path=str(tmp_path))
checks = sync.check_since(
store, "2026-10-01T00:00:00+00:00", now=T0 + timedelta(days=60)
)
assert sync.blocking(checks)
assert {c.code for c in checks} == {"no_baseline"}
assert [c.code for c in sync.check_since(store, "nope")] == ["format"]
def test_future_floor_gap_overlap(self, base_store: Store) -> None:
sync.apply(base_store) # no "unapplied" noise in the codes below
now = T0 + timedelta(days=10)
def codes(s: str, **kw: object) -> dict[str, str]:
return {
c.code: c.level for c in sync.check_since(base_store, s, now=now, **kw)
}
assert codes(sync.format_instant(now + timedelta(hours=1)))["future"] == "block"
assert codes("2026-01-01T00:00:00+00:00")["floor"] == "warn"
# exactly the watermark for Patient, overlap for the two later types
c = codes("2026-10-01T12:00:00.000+00:00")
assert c == {"watermark": "ok", "overlap": "ok"}
# after every watermark → gap
c = codes(sync.format_instant(T0 + timedelta(hours=1)))
assert c == {"gap": "block"}
# a filter the baseline did not use: allowed, warned, compared to the type's watermark
c = codes(
"2026-10-01T12:00:00+00:00",
type_filter="ExplanationOfBenefit?outcome=complete",
types=["ExplanationOfBenefit"],
)
assert c == {"scope_filter": "warn", "overlap": "ok"}
# /Patient over Group/all baselines: same population, no backfill for new enrollees
c = codes("2026-10-01T12:00:00+00:00", endpoint="Patient")
assert c["backfill_scope"] == "warn" and "endpoint_mix" not in c
# runout is another population
assert (
codes("2026-10-01T12:00:00+00:00", endpoint="Group/runout")["endpoint_mix"]
== "block"
)
def test_stale_attribution_inflight_unapplied(self, base_store: Store) -> None:
now = T0 + timedelta(days=45)
attribution = {
"ingestion_dates": [
{
"type": "last_attribution_update",
"timestamp": sync.format_instant(T0 + timedelta(days=5)),
}
]
}
base_store._append_job(
key="INF", endpoint="Group/all", types="Patient", status="polling"
)
checks = sync.check_since(
base_store,
"2026-10-01T12:00:00+00:00",
now=now,
attribution=attribution,
types=["Patient"],
)
levels = {c.code: c.level for c in checks}
assert levels["stale_baseline"] == "warn"
assert levels["attribution_newer"] == "warn"
assert levels["inflight"] == "warn"
assert levels["unapplied"] == "warn"
assert not sync.blocking(checks)
sync.apply(base_store)
assert "unapplied" not in {
c.code
for c in sync.check_since(
base_store, "2026-10-01T12:00:00+00:00", now=now, types=["Patient"]
)
}
def test_job_info_falls_back_for_legacy_rows(self, tmp_path: Path) -> None:
store = Store(path=str(tmp_path))
t_start = datetime(2026, 10, 1, 9, 0, tzinfo=timezone.utc)
store._append_job(
key="OLD",
endpoint="Group/all",
types="Patient",
status="polling",
ts=t_start,
)
store._append_job(
key="OLD",
endpoint="Group/all",
types="Patient",
status="complete",
ts=t_start + timedelta(hours=1),
)
info = sync.job_info(store, store.get_job("OLD"))
# the conservative fallback is the *start* of the job, not its completion
assert info.transaction_source == "started_at" and info.kind == "baseline"
assert info.transaction_time == t_start
store._write_manifest("OLD", {"transactionTime": "2026-08-01T00:00:00.000Z"})
info = sync.job_info(store, store.get_job("OLD"))
assert info.transaction_source == "manifest"
assert (
sync.format_instant(info.transaction_time)
== "2026-08-01T00:00:00.000+00:00"
)
# ── reconciliation ───────────────────────────────────────────
class TestReconcile:
def test_baselines_then_delta(self, base_store: Store) -> None:
rep = sync.apply(base_store)
assert [r.job_key for r in rep.applied] == [
"B_PAT",
"B_COV",
"B_EOB",
] and rep.ok
eob = rep.applied[2].counts["explanation_of_benefit"]
assert eob["new"] == eob["current"] == 269 and eob["retired"] == 0
assert eob["tombstoned"] == sum(
1
for e in _rows("ExplanationOfBenefit")
if any(t.get("code") == "NotFinalAction" for t in e["meta"]["tag"])
)
st = sync.status(base_store)
assert st["pending"] == [] and st["current"]["explanation_of_benefit"] == 269
assert st["current"]["eob_item"] == 1232
since = T0 + timedelta(minutes=2)
delta, expect = _delta_rows(
_rows("ExplanationOfBenefit"), _rows("Patient"), since
)
add_job(
base_store,
"D_EOB",
{"ExplanationOfBenefit": delta},
since=sync.format_instant(since),
transaction_time=since + timedelta(days=7),
)
rep = sync.apply(base_store)
assert [r.job_key for r in rep.applied] == ["D_EOB"] and rep.ok
c = rep.applied[0].counts["explanation_of_benefit"]
assert {k: c[k] for k in expect} == expect
assert c["current"] == 269 + expect["new"] and c["retired"] == 0
# the orphan is the backfilled claim for the bene no Patient row carries
assert c["orphans"] == 1
import duckdb
flat = Path(base_store.root) / "flat"
con = duckdb.connect()
cur = con.execute(
f"select id, final_action, _job_key from read_parquet('{flat / 'explanation_of_benefit.parquet'}') "
"where id like '%-adj' or id like '%-void' or id like '%-old'"
).fetchall()
ids = {r[0] for r in cur}
assert len([i for i in ids if i.endswith("-adj")]) == 3
assert any(i.endswith("-void") for i in ids) and not any(
i.endswith("-old") for i in ids
)
assert all(r[2] == "D_EOB" for r in cur)
# items follow their headers: no line without a current header, adjusted lines are the new version's
assert (
con.execute(
f"select count(*) from read_parquet('{flat / 'eob_item.parquet'}') i where not exists "
f"(select 1 from read_parquet('{flat / 'explanation_of_benefit.parquet'}') h where h.clm_id = i.clm_id)"
).fetchone()[0]
== 0
)
# watermark moved
assert sync.next_since(base_store)[
"ExplanationOfBenefit"
] == sync.format_instant(since + timedelta(days=7))
ledger = sync.read_ledger(base_store)
assert [r["job_key"] for r in ledger] == [
"B_PAT",
"B_COV",
"B_EOB",
"D_EOB",
] and all(r["ok"] for r in ledger)
con.close()
def test_idempotent_and_order_independent(
self, base_store: Store, tmp_path: Path
) -> None:
since = T0 + timedelta(minutes=2)
delta, _ = _delta_rows(_rows("ExplanationOfBenefit"), _rows("Patient"), since)
add_job(
base_store,
"D_EOB",
{"ExplanationOfBenefit": delta},
since=sync.format_instant(since),
transaction_time=since + timedelta(days=7),
)
sync.apply(base_store)
before = {
t: _digest(base_store, t)
for t in ("patient", "coverage", "explanation_of_benefit", "eob_item")
}
# re-apply: nothing to do
rep = sync.apply(base_store)
assert rep.applied == [] and sorted(rep.skipped) == [
"B_COV",
"B_EOB",
"B_PAT",
"D_EOB",
]
# forced re-apply: same tables, zero drift counted
rep = sync.apply(base_store, ["D_EOB"], force=True)
assert rep.ok and rep.applied[0].counts["explanation_of_benefit"]["new"] == 0
assert {t: _digest(base_store, t) for t in before} == before
# a second store that applies the delta first, then the baselines
other = Store(path=str(tmp_path / "other"))
add_job(
other,
"D_EOB",
{"ExplanationOfBenefit": delta},
since=sync.format_instant(since),
transaction_time=since + timedelta(days=7),
)
rep = sync.apply(other)
assert any("without a baseline" in n for n in rep.applied[0].notes)
add_job(other, "B_PAT", {"Patient": _rows("Patient")}, transaction_time=T0)
add_job(
other,
"B_COV",
{"Coverage": _rows("Coverage")},
transaction_time=T0 + timedelta(minutes=1),
)
add_job(
other,
"B_EOB",
{"ExplanationOfBenefit": _rows("ExplanationOfBenefit")},
transaction_time=T0 + timedelta(minutes=2),
)
rep = sync.apply(other)
assert rep.ok
assert {t: _digest(other, t) for t in before} == before
def test_new_baseline_retires_and_resets(self, base_store: Store) -> None:
sync.apply(base_store)
pats = _rows("Patient")
add_job(
base_store,
"B_PAT2",
{"Patient": pats[:-10]},
transaction_time=T0 + timedelta(days=30),
)
rep = sync.apply(base_store)
c = rep.applied[0].counts["patient"]
assert c == {**c, "retired": 10, "new": 0, "unchanged": 84, "current": 84}
# an older baseline applied later never resurrects them
add_job(
base_store,
"B_PAT0",
{"Patient": pats},
transaction_time=T0 - timedelta(days=1),
)
rep = sync.apply(base_store)
c = rep.applied[0].counts["patient"]
assert c["current"] == 84 and c["stale_ignored"] == 94 and c["new"] == 0
assert (
any("predates" in n or "ignored" in n for n in rep.applied[0].notes) or True
)
assert sync.status(base_store)["current"]["patient"] == 84
def test_scope_drift_and_coverage_orphans(self, base_store: Store) -> None:
sync.apply(base_store)
# a delta pulled with another _typeFilter is applied but does not feed current state
delta, _ = _delta_rows(
_rows("ExplanationOfBenefit"), _rows("Patient"), T0 + timedelta(minutes=2)
)
add_job(
base_store,
"D_FILT",
{"ExplanationOfBenefit": delta},
since=sync.format_instant(T0 + timedelta(minutes=2)),
transaction_time=T0 + timedelta(days=3),
type_filter="ExplanationOfBenefit?outcome=complete",
)
rep = sync.apply(base_store)
c = rep.applied[0].counts["explanation_of_benefit"]
# reconciled like any delta, with the drift on record
assert c["new"] == 3 and c["replaced"] == 4 and c["current"] == 272
assert any("scope drift" in n for n in rep.applied[0].notes)
# a runout delta is another population and does not feed current state
add_job(
base_store,
"D_RUNOUT",
{"ExplanationOfBenefit": delta},
since=sync.format_instant(T0 + timedelta(minutes=2)),
transaction_time=T0 + timedelta(days=4),
endpoint="Group/runout",
)
rep = sync.apply(base_store)
c = rep.applied[0].counts["explanation_of_benefit"]
assert c["stale_ignored"] == 10 and c["current"] == 272
assert any("runout population" in n for n in rep.applied[0].notes)
# coverage for an unknown bene is an orphan
cov = copy.deepcopy(_rows("Coverage")[0])
cov["id"] = "part-a--999999-x"
cov["beneficiary"] = {"reference": "Patient/-999999"}
_lu(cov, T0 + timedelta(days=4))
add_job(
base_store,
"D_COV",
{"Coverage": [cov]},
since=sync.format_instant(T0 + timedelta(minutes=1)),
transaction_time=T0 + timedelta(days=5),
)
rep = sync.apply(base_store)
assert rep.applied[0].counts["coverage"]["orphans"] == 1
assert rep.applied[0].counts["coverage"]["new"] == 1
def test_invariant_failure_leaves_tables(
self, base_store: Store, monkeypatch
) -> None:
sync.apply(base_store)
before = _digest(base_store, "patient")
def broken(*a, **k): # noqa: ANN002, ANN003, ANN202
raise sync.SyncInvariantError("forced")
monkeypatch.setattr(sync, "_invariants", broken)
add_job(
base_store,
"B_PAT2",
{"Patient": _rows("Patient")[:5]},
transaction_time=T0 + timedelta(days=1),
)
with pytest.raises(sync.SyncInvariantError):
sync.apply(base_store)
assert _digest(base_store, "patient") == before
row = sync.read_ledger(base_store)[-1]
assert row["job_key"] == "B_PAT2" and not row["ok"] and "forced" in row["error"]
monkeypatch.undo()
# the failed job is retried on the next apply
assert [r.job_key for r in sync.apply(base_store).applied] == ["B_PAT2"]
def test_apply_unknown_key(self, base_store: Store) -> None:
with pytest.raises(sync.SyncError):
sync.apply(base_store, ["NOPE"])
# ── edges ─────────────────────────────────────────────────────
class TestEdges:
def test_maybe_instant_fallbacks(self) -> None:
assert sync._maybe_instant("") is None and sync._maybe_instant(None) is None
assert sync._maybe_instant("garbage") is None
naive = sync._maybe_instant("2026-10-01 12:00:00")
assert naive is not None and naive.tzinfo is timezone.utc
aware = sync._maybe_instant(datetime(2026, 10, 1, tzinfo=timezone.utc))
assert aware is not None and aware.hour == 0
assert sync._maybe_instant(datetime(2026, 10, 1)).tzinfo is timezone.utc
def test_incomplete_jobs_are_not_complete(self, tmp_path: Path) -> None:
store = Store(path=str(tmp_path))
store._append_job(
key="NOFILES", endpoint="Group/all", types="Patient", status="complete"
)
store._append_job(
key="HALF", endpoint="Group/all", types="Patient", status="complete"
)
store._append_file(
job_key="HALF", resource_type="Patient", url="u1", status="complete"
)
store._append_file(
job_key="HALF", resource_type="Patient", url="u2", status="failed"
)
assert sync.complete_jobs(store) == []
assert sync.status(store)["jobs"] == []
def test_empty_delta_counts_when_manifest_says_so(self, base_store: Store) -> None:
"""A delta with nothing to deliver has no files; its manifest makes it a job and moves the watermark."""
sync.apply(base_store)
base_store._append_job(
key="D_EMPTY",
endpoint="Group/all",
types="Patient",
since=sync.format_instant(T0),
status="complete",
transaction_time=sync.format_instant(T0 + timedelta(days=1)),
)
assert "D_EMPTY" not in {j.key for j in sync.complete_jobs(base_store)}
base_store._write_manifest(
"D_EMPTY",
{
"transactionTime": sync.format_instant(T0 + timedelta(days=1)),
"output": [],
},
)
assert "D_EMPTY" in {j.key for j in sync.complete_jobs(base_store)}
rep = sync.apply(base_store)
assert rep.applied[0].job_key == "D_EMPTY" and rep.ok
assert rep.applied[0].counts["patient"]["rows"] == 0
assert rep.applied[0].counts["patient"]["current"] == 94
assert sync.next_since(base_store)["Patient"] == sync.format_instant(
T0 + timedelta(days=1)
)
def test_watermark_filters(self, base_store: Store) -> None:
jobs = sync.complete_jobs(base_store)
assert sync.watermarks(jobs, endpoint="Group/runout") == {}
assert len(sync.watermarks(jobs, endpoint="Patient")) == 3 # same population
assert sync.watermarks(jobs, type_filter="x") == {}
assert len(sync.watermarks(jobs, endpoint="Group/all", type_filter="")) == 3
assert set(sync.baselines(jobs)) == {
("current", "", t) for t in sync.RESOURCE_TYPES
}
def test_missing_export_dir(self, tmp_path: Path) -> None:
store = Store(path=str(tmp_path))
store._append_job(
key="GONE", endpoint="Group/all", types="Patient", status="complete"
)
store._append_file(
job_key="GONE", resource_type="Patient", url="u", status="complete"
)
with pytest.raises(sync.SyncError, match="no export directory"):
sync.apply(store)
row = sync.read_ledger(store)[-1]
assert (
row["job_key"] == "GONE" and not row["ok"] and "SyncError" in row["error"]
)
def test_delta_older_than_baseline_is_ignored(self, base_store: Store) -> None:
sync.apply(base_store)
delta, _ = _delta_rows(
_rows("ExplanationOfBenefit"), _rows("Patient"), T0 - timedelta(days=10)
)
add_job(
base_store,
"D_OLD",
{"ExplanationOfBenefit": delta},
since=sync.format_instant(T0 - timedelta(days=10)),
transaction_time=T0 - timedelta(days=3),
)
rep = sync.apply(base_store)
c = rep.applied[0].counts["explanation_of_benefit"]
assert c["stale_ignored"] == 10 and c["current"] == 269 and c["new"] == 0
assert any("predates baseline" in n for n in rep.applied[0].notes)
def test_job_claiming_a_type_it_did_not_deliver(self, base_store: Store) -> None:
sync.apply(base_store)
# the job row says Patient,Coverage but only a Coverage file landed
d = Path(base_store.root) / "exports" / "D_MIX"
d.mkdir()
cov = copy.deepcopy(_rows("Coverage")[0])
_lu(cov, T0 + timedelta(days=2))
(d / "Coverage.ndjson").write_text(json.dumps(cov) + "\n")
base_store._append_file(
job_key="D_MIX",
resource_type="Coverage",
url="u",
storage_path=str(d / "Coverage.ndjson"),
status="complete",
)
base_store._append_job(
key="D_MIX",
endpoint="Group/all",
types="Patient,Coverage",
since=sync.format_instant(T0),
status="complete",
transaction_time=sync.format_instant(T0 + timedelta(days=2)),
)
rep = sync.apply(base_store)
c = rep.applied[0].counts
assert c["patient"]["rows"] == 0 and c["patient"]["current"] == 94
assert c["coverage"]["replaced"] == 1 and c["coverage"]["current"] == 175
def test_invariants_catch_duplicates_conservation_and_strays(
self, base_store: Store
) -> None:
import duckdb
con = duckdb.connect()
con.execute(
"create table cur_patient as select * from (values ('A','A'), ('A','A')) t(bene_id, id)"
)
con.execute(
"create table cur_explanation_of_benefit (clm_cntl_num varchar, clm_id varchar, id varchar)"
)
con.execute("create table cur_eob_item as select 'X' as clm_id")
job = sync.complete_jobs(base_store)[0] # the Patient baseline
rep = sync.JobReport(
job_key=job.key,
kind="baseline",
endpoint="Group/all",
type_filter="",
types=["Patient"],
since="",
transaction_time="",
counts={"patient": {**dict.fromkeys(sync.COUNT_FIELDS, 0), "current": 2}},
)
with pytest.raises(sync.SyncInvariantError) as exc:
sync._invariants(con, base_store, job, rep, {"patient": 5}, {})
msg = str(exc.value)
assert "2 rows but 1 distinct keys" in msg
assert "count conservation failed" in msg and "5 + 0 new" in msg
assert "1 line(s) without a current header" in msg
con.close()
def test_older_baseline_note_in_invariants(self, base_store: Store) -> None:
sync.apply(base_store)
add_job(
base_store,
"B_PAT0",
{"Patient": _rows("Patient")},
transaction_time=T0 - timedelta(days=1),
)
rep = sync.apply(base_store)
assert any(
"newest baseline stays authoritative" in n for n in rep.applied[0].notes
)