0f1e609888
CycloneStore split precedent (SP21) — lift three helpers from scheduler.py + api.py into one module. Both callers will switch to this in Task 6. The new package also defines HandleResult and the HANDLERS registry; handle_999 / handle_ta1 / handle_277ca / handle_835 fill in over Tasks 2-5. The registry is lazy + best-effort: register_handlers() catches broad Exception so a partial-modification SyntaxError in one in-flight handler module can't break scheduler or API import. 11 tests added; existing suite unchanged (1 failed + 2 errors pre-existing in test_provider_extended_response.py are not introduced by this commit).
143 lines
4.6 KiB
Python
143 lines
4.6 KiB
Python
"""Tests for the dedup-ed ack ID helpers (SP27 Task 1).
|
|
|
|
Locks the contract for the helpers that ``scheduler.py`` + ``api.py``
|
|
will both import from one place in ``cyclone.handlers._ack_id``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from pathlib import Path
|
|
|
|
from cyclone.handlers._ack_id import (
|
|
ack_count_summary,
|
|
ack_synthetic_source_batch_id,
|
|
two77ca_synthetic_source_batch_id,
|
|
)
|
|
from cyclone.parsers.parse_999 import parse_999_text
|
|
|
|
|
|
ACCEPTED = Path(__file__).parent / "fixtures" / "minimal_999.txt"
|
|
REJECTED = Path(__file__).parent / "fixtures" / "minimal_999_rejected.txt"
|
|
|
|
|
|
def _parse(path: Path):
|
|
return parse_999_text(path.read_text(), input_file=path.name)
|
|
|
|
|
|
def test_ack_count_summary_all_accepted():
|
|
result = _parse(ACCEPTED)
|
|
recv, acc, rej, code = ack_count_summary(result)
|
|
assert recv == 1
|
|
assert acc == 1
|
|
assert rej == 0
|
|
assert code == "A"
|
|
|
|
|
|
def test_ack_count_summary_all_rejected():
|
|
result = _parse(REJECTED)
|
|
recv, acc, rej, code = ack_count_summary(result)
|
|
assert recv == 1
|
|
assert acc == 0
|
|
assert rej == 1
|
|
assert code == "R"
|
|
|
|
|
|
def test_ack_count_summary_partial_uses_p_code():
|
|
# Synthesize a 2-set 999 inline with one AK5=A and one AK5=R.
|
|
# ISA is exactly 106 chars (positions 0-105); the parser slices
|
|
# positionally then splits the body on ``~``.
|
|
# SE count = ST + AK1 + AK2_1 + AK5_1 + AK2_2 + AK5_2 + AK9 + SE = 8.
|
|
partial_999 = (
|
|
"ISA*00* *00* *ZZ*SUBMITTERID *ZZ*RECEIVERID "
|
|
"*240101*1200*^*00501*000000001*0*P*:~"
|
|
"GS*FA*SUBMITTERID*RECEIVERID*20240101*1200*1*X*005010X231A1~"
|
|
"ST*999*0001*005010X231A1~"
|
|
"AK1*HC*0001*1*2~"
|
|
"AK2*837*1~"
|
|
"AK5*A~"
|
|
"AK2*837*2~"
|
|
"AK5*R~"
|
|
"AK9*P*2*1*1~"
|
|
"SE*8*0001~"
|
|
"GE*1*1~"
|
|
"IEA*1*000000001~"
|
|
)
|
|
result = parse_999_text(partial_999, input_file="partial.999")
|
|
recv, acc, rej, code = ack_count_summary(result)
|
|
assert recv == 2
|
|
assert acc == 1
|
|
assert rej == 1
|
|
assert code == "P"
|
|
|
|
|
|
def test_ack_synthetic_with_pcn_and_filename_is_deterministic_and_prefix():
|
|
bsid1 = ack_synthetic_source_batch_id(
|
|
"000000001", pcn="PCN-12345", source_filename="tp_999.x12",
|
|
)
|
|
bsid2 = ack_synthetic_source_batch_id(
|
|
"000000001", pcn="PCN-12345", source_filename="tp_999.x12",
|
|
)
|
|
assert bsid1 == bsid2, "same inputs must produce same id"
|
|
assert bsid1.startswith("999-PCN-12345-")
|
|
# Hash is 8 hex chars
|
|
suffix = bsid1.split("-")[-1]
|
|
assert len(suffix) == 8 and all(
|
|
c in "0123456789abcdef" for c in suffix
|
|
), suffix
|
|
|
|
|
|
def test_ack_synthetic_with_pcn_strips_whitespace():
|
|
bsid = ack_synthetic_source_batch_id(
|
|
"000000001", pcn=" PCN ", source_filename="f.x12",
|
|
)
|
|
assert bsid.startswith("999-PCN-")
|
|
|
|
|
|
def test_ack_synthetic_without_pcn_uses_icn():
|
|
bsid = ack_synthetic_source_batch_id(
|
|
"777777777", pcn=None, source_filename="f.x12",
|
|
)
|
|
assert bsid.startswith("999-777777777-")
|
|
# <= 32 chars total (VARCHAR(32) constraint)
|
|
assert len(bsid) <= 32
|
|
|
|
|
|
def test_ack_synthetic_default_icn_when_empty():
|
|
bsid = ack_synthetic_source_batch_id("", pcn=None, source_filename=None)
|
|
# No PCN, no filename → falls through to default ICN ``000000001``
|
|
# (no hash suffix without a filename).
|
|
assert bsid == "999-000000001"
|
|
|
|
|
|
def test_two77ca_synthetic_uses_icn():
|
|
assert two77ca_synthetic_source_batch_id("000012345") == "277CA-000012345"
|
|
|
|
|
|
def test_two77ca_synthetic_empty_falls_back_to_default():
|
|
assert two77ca_synthetic_source_batch_id("") == "277CA-000000001"
|
|
assert two77ca_synthetic_source_batch_id(None) == "277CA-000000001"
|
|
|
|
|
|
def test_ack_count_summary_empty_set_responses_returns_zeros():
|
|
"""A 999 envelope without any AK2/IK5 sets is malformed per the spec,
|
|
but the helper must still return a sane (0, 0, 0, 'A') tuple — the
|
|
scheduler adds it to result.errors and the caller decides."""
|
|
class _StubResult:
|
|
set_responses = []
|
|
recv, acc, rej, code = ack_count_summary(_StubResult())
|
|
assert recv == 0
|
|
assert acc == 0
|
|
assert rej == 0
|
|
# rejected == 0 → "A" (no rejection signal in this degenerate case)
|
|
assert code == "A"
|
|
|
|
|
|
def test_ack_synthetic_uses_real_sha1_truncation():
|
|
"""The hash should match hashlib.sha1(filename).hexdigest()[:8] so
|
|
a future migration to a different hash is intentional, not drift."""
|
|
expected = hashlib.sha1(b"my-file.x12").hexdigest()[:8]
|
|
bsid = ack_synthetic_source_batch_id(
|
|
"000000001", pcn=None, source_filename="my-file.x12",
|
|
)
|
|
assert bsid.endswith(f"-{expected}")
|