r"""
Input\ holds exactly one report, and it is always the current one.
python tests\test_input_archive.py
The problem this covers, in the words it was reported in: run the bot a second
time, and the agent takes the **old** file's claims instead of the new one.
Three days of testing left six reports in `Input\`, named
`Claim Report_09-07-2026.xlsx`, `... (2).xlsx`, `... (3).xlsx` and so on. Once
more than one is sitting there, "which report is current?" stops being obvious
— and the wrong answer is silent: the run looks clean and processes claims from
a stale queue.
Two things make the answer unambiguous, and both are tested here:
* the export moves every earlier report into `Input\Archive\` before saving
the new one, so there is only ever one to choose from;
* `latest_report()` picks by **modification time**, never by name. Sorting
names is a trap: `Claim Report_09-09-2026 (2).xlsx` sorts *before*
`Claim Report_09-09-2026.xlsx`, because a space (0x20) is lower than a dot
(0x2e) — so the alphabetically first file can be the older one.
Runs against a temporary Input\ ; the real one is never touched.
"""
import sys
import time
import pathlib
import tempfile
ROOT = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
from ecw_bot import settings as cfg, claim_file # noqa: E402
from ecw_bot.pages import claim_report # noqa: E402
CHECKS = []
def check(fn):
CHECKS.append(fn)
return fn
def sandbox():
"""A fresh temporary Input\\ , pointed at by settings."""
root = pathlib.Path(tempfile.mkdtemp())
cfg.INPUTS = root / "Input"
cfg.INPUTS_ARCHIVE = cfg.INPUTS / "Archive"
cfg.INPUTS.mkdir(parents=True)
return cfg.INPUTS
def report(where, name, minutes_ago=0, ids=("9115000",)):
"""Write a minimal xlsx that claim_file can actually read."""
import zipfile
ns = 'xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"'
rows = [''
'CLAIMS#
']
for i, cid in enumerate(ids, start=2):
rows.append(f''
f'{cid}
')
sheet = (f''
f'{"".join(rows)}')
path = where / name
with zipfile.ZipFile(path, "w") as z:
z.writestr("xl/worksheets/sheet1.xml", sheet)
if minutes_ago:
old = time.time() - minutes_ago * 60
import os
os.utime(path, (old, old))
return path
@check
def archive_leaves_only_the_new_report(page=None):
inp = sandbox()
report(inp, "Claim Report_09-07-2026.xlsx", minutes_ago=2880)
report(inp, "Claim Report_09-07-2026 (2).xlsx", minutes_ago=2900)
report(inp, "Claim Report_09-08-2026.xlsx", minutes_ago=1440)
report(inp, "rescued_20260908_163651.xlsx", minutes_ago=1400)
moved = claim_report._archive_old()
assert len(moved) == 4, [m.name for m in moved]
left = [f.name for f in inp.iterdir() if f.is_file()]
assert left == [], f"Input should be empty of reports, got {left}"
archived = sorted(f.name for f in cfg.INPUTS_ARCHIVE.iterdir())
assert len(archived) == 4, archived
# Each archived name carries the file's own mtime, so nothing collides and
# the order is readable.
assert all("__20" in n for n in archived), archived
@check
def the_new_report_is_the_one_that_gets_read(page=None):
"""The reported symptom, reproduced and then not happening."""
inp = sandbox()
report(inp, "Claim Report_09-08-2026.xlsx",
minutes_ago=1440, ids=("1111111", "2222222"))
# Second run: archive, then the new file lands.
claim_report._archive_old()
report(inp, "Claim Report_09-09-2026.xlsx", ids=("9115000", "9114000"))
info = claim_file.claim_ids() # no path - it has to find it
assert info["file"].name == "Claim Report_09-09-2026.xlsx", info["file"]
assert info["ids"] == ["9115000", "9114000"], info["ids"]
assert "1111111" not in info["ids"], "read the OLD report's claims"
@check
def newest_by_mtime_not_by_name(page=None):
"""
The name-sorting trap, with the two files that expose it.
`Claim Report_09-09-2026 (2).xlsx` sorts FIRST alphabetically - a space is
0x20, a dot is 0x2e - while being the OLDER file. Anything picking
`sorted(...)[0]` reads stale claims and looks perfectly healthy doing it.
"""
inp = sandbox()
old = report(inp, "Claim Report_09-09-2026 (2).xlsx",
minutes_ago=120, ids=("1111111",))
new = report(inp, "Claim Report_09-09-2026.xlsx", ids=("9115000",))
names = sorted(f.name for f in inp.iterdir())
assert names[0] == old.name, (
f"this test assumes '(2)' sorts first; it did not: {names}")
picked = claim_file.latest_report()
assert picked.name == new.name, (
f"picked {picked.name}, the alphabetically-first but OLDER file")
@check
def more_than_one_report_is_reported_not_guessed(page=None):
import io
import contextlib
inp = sandbox()
report(inp, "Claim Report_09-08-2026.xlsx", minutes_ago=1440)
report(inp, "Claim Report_09-09-2026.xlsx")
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
picked = claim_file.latest_report()
out = buf.getvalue()
assert picked.name == "Claim Report_09-09-2026.xlsx", picked
# A guess that goes unmentioned is how a stale read stays invisible.
assert "WARNING" in out and "should be one" in out, out
@check
def empty_input_says_where_the_old_ones_went(page=None):
inp = sandbox()
(inp / "Archive").mkdir()
try:
claim_file.latest_report()
except FileNotFoundError as exc:
assert "Archive" in str(exc), exc
return
raise AssertionError("expected FileNotFoundError naming Archive")
@check
def archiving_can_be_turned_off(page=None):
inp = sandbox()
report(inp, "Claim Report_09-08-2026.xlsx", minutes_ago=1440)
was = cfg.ARCHIVE_OLD_INPUTS
cfg.ARCHIVE_OLD_INPUTS = False
try:
assert claim_report._archive_old() == []
assert (inp / "Claim Report_09-08-2026.xlsx").exists()
finally:
cfg.ARCHIVE_OLD_INPUTS = was
def main() -> int:
real_in, real_arch = cfg.INPUTS, cfg.INPUTS_ARCHIVE
failures = 0
try:
for fn in CHECKS:
name = fn.__name__.replace("_", " ")
try:
fn()
print(f" ok {name}")
except AssertionError as exc:
print(f" FAIL {name}: {exc}")
failures += 1
except Exception as exc:
print(f" ERROR {name}: {type(exc).__name__}: {exc}")
failures += 1
finally:
cfg.INPUTS, cfg.INPUTS_ARCHIVE = real_in, real_arch
print(f"\n{len(CHECKS) - failures}/{len(CHECKS)} passed")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())