Files
doczyai-pipelines/fieldExtraction/tests/test_crosswalk_utils.py
T
Katon Minhas 0e2622f156 Merged in feature/1toN-Optimization (pull request #799)
Feature/1toN Optimization

* Update config - remove Haiku 3

* Merged in optimization/trigger-cap (pull request #792)

Optimization/trigger cap

* trigger cap breakout added

* Merge remote-tracking branch 'origin/feature/1toN-Optimization' into optimization/trigger-cap

* carveout breakout added


Approved-by: Katon Minhas

* Merged in bugfix/utah_issue_fixes (pull request #791)

Bugfix/utah issue fixes to feature/1toN-Optimization

* updated valid values for AARETE_DERIVED_REIMB_METHOD

* removed example reimbursements

* removed reciprocatory agreements section reimbs

* Merged feature/1toN-Optimization into bugfix/utah_issue_fixes

* update reimb primary


Approved-by: Katon Minhas

* Update unit tests

* Update llm_utils

* Merge branch 'feature/deprecate-haiku-3' into feature/1toN-Optimization

* fix over-filtering of lesser of

* Merged in bugfix/UT-methodology-breakout (pull request #794)

Bugfix/UT methodology breakout

* updated valid values for AARETE_DERIVED_REIMB_METHOD

* removed example reimbursements

* prompt update

* prompt update

* Merged feature/1toN-Optimization into bugfix/nv_issue_fixes

* add service term in mb prompts

* Merged feature/1toN-Optimization into bugfix/UT-methodology-breakout

* print statement removed

* Merge branch 'bugfix/UT-methodology-breakout' of https://bitbucket.org/aarete/doczy.ai into bugfix/UT-methodology-breakout

* primary prompt update

* remove duplicate prompt


Approved-by: Katon Minhas

* Merge branch 'main' into feature/1toN-Optimization

* Merge branch 'main' into feature/1toN-Optimization

* Update preprocessing to make the exhibit_chunk_mapping start at first page

* Merge row_funcs.py changes from feature/cross-exhibit-dynamic

* Merged in bugfix/UT-grouper-issues (pull request #796)

Bugfix/UT grouper issues

* updated valid values for AARETE_DERIVED_REIMB_METHOD

* removed example reimbursements

* prompt update

* prompt update

* Merged feature/1toN-Optimization into bugfix/nv_issue_fixes

* add service term in mb prompts

* Merged feature/1toN-Optimization into bugfix/UT-methodology-breakout

* print statement removed

* Merge branch 'bugfix/UT-methodology-breakout' of https://bitbucket.org/aarete/doczy.ai into bugfix/UT-methodology-breakout

* primary prompt update

* prompt update

* Merge remote-tracking branch 'origin/feature/1toN-Optimization' into bugfix/UT-grouper-issues

* removed temp changes

* removed temp changes

* Update reimb primary


Approved-by: Katon Minhas

* Merged in bugfix/validation_fixes (pull request #795)

bugfix/validation_fixes to feature/1toN-Optimization

* updated validation of clean claims reimbursement

* Merged feature/1toN-Optimization into bugfix/validation_fixes


Approved-by: Katon Minhas

* Re-add dynamic codes and reimb-info

* Re-structure empty reimbursement prompt

* Fix lesser of check overfiltering

* Merged in bugfix/generic-methodology-breakout (pull request #797)

Bugfix/generic methodology breakout

* fee schedule fix

* carveout+special case prompt changed

* fee schedule changes

* prompt changes and column order


Approved-by: Katon Minhas

* pytest fix

* remove prints

* Update mapping

* Update tests

* Update test

* Address PR review comments: add logging, fix return types, fix variable propagation, and add tests

- Add warning log in code_funcs.py try/except block for grouper code parsing failures
- Add warning log in hybrid_smart_chunking_funcs.py except block for JSON parsing failures
- Add return type annotation to lesser_of_distribution function
- Fix first_reimbursement_page variable propagation issue by returning updated value
- Add comprehensive tests for simplify_exhibit, chunk_by_exhibit, and lesser_of_distribution functions

* Merged in bugfix/prov_info_json (pull request #802)

Bugfix/prov info json to feature/1toN-Optimization

* prov_info_json added

* remove print statements


Approved-by: Katon Minhas


Approved-by: Siddhant Medar
2025-12-10 20:01:52 +00:00

233 lines
7.1 KiB
Python

import json
import pandas as pd
import pytest
from src.crosswalk.crosswalk_builder import CrosswalkBuilder
from src.utils.crosswalk_utils import apply_crosswalk
@pytest.fixture
def sample_excel_file(tmp_path):
data = {"from_col": ["A", "B", "C"], "to_col": ["one", "two", "three"]}
df = pd.DataFrame(data)
file_path = tmp_path / "sample.xlsx"
df.to_excel(file_path, index=False)
return file_path
@pytest.fixture
def sample_json_file(tmp_path):
data = {
"metadata": {"from_col": "from_col", "to_col": "to_col"},
"mapping": {"A": "1", "B": "2", "C": "3"},
}
file_path = tmp_path / "sample.json"
with open(file_path, "w") as f:
json.dump(data, f)
return file_path
def test_from_excel(sample_excel_file):
builder = CrosswalkBuilder()
builder.from_excel(sample_excel_file, "from_col", "to_col")
assert builder.mapping == {"A": "one", "B": "two", "C": "three"}
assert builder.metadata == {"from_col": "from_col", "to_col": "to_col"}
def test_from_excel_missing_column(sample_excel_file):
builder = CrosswalkBuilder()
with pytest.raises(ValueError, match="Column 'nonexistent' not found"):
builder.from_excel(sample_excel_file, "nonexistent", "to_col")
def test_from_dict():
mapping = {"A": "1", "B": "2", "C": "3"}
builder = CrosswalkBuilder()
builder.from_dict(mapping)
assert builder.mapping == mapping
assert builder.metadata == {"from_col": None, "to_col": None}
def test_from_json(sample_json_file):
builder = CrosswalkBuilder()
builder.from_json(sample_json_file)
assert builder.mapping == {"A": "1", "B": "2", "C": "3"}
assert builder.metadata == {"from_col": "from_col", "to_col": "to_col"}
def test_json_roundtrip(tmp_path):
# Create and save a crosswalk
original = CrosswalkBuilder()
original.from_dict({"A": "1"}, "source", "target")
json_path = tmp_path / "test.json"
original.save(json_path)
# Load it back
loaded = CrosswalkBuilder().from_json(json_path)
# Check everything matches
assert loaded.mapping == original.mapping
assert loaded.metadata == original.metadata
def test_save(tmp_path):
mapping = {"A": "1", "B": "2", "C": "3"}
builder = CrosswalkBuilder()
builder.from_dict(mapping, "from_col", "to_col")
output_path = tmp_path / "output.json"
builder.save(output_path)
with open(output_path, "r") as f:
data = json.load(f)
assert data["mapping"] == mapping
assert data["metadata"] == {"from_col": "from_col", "to_col": "to_col"}
def test_to_csv(tmp_path):
# Create a builder with some data
builder = CrosswalkBuilder()
builder.from_dict(
mapping={"A": "one", "B": "two", "C": "three"},
from_col="Source",
to_col="Target",
)
# Export to CSV
csv_path = tmp_path / "test.csv"
builder.to_csv(csv_path)
# Read back and verify
df = pd.read_csv(csv_path)
# Check column names match metadata
assert list(df.columns) == ["Source", "Target"]
# Check data matches original mapping
result_dict = dict(zip(df["Source"], df["Target"]))
assert result_dict == {"A": "one", "B": "two", "C": "three"}
def test_to_csv_no_metadata(tmp_path):
# Create a builder without metadata
builder = CrosswalkBuilder()
builder.from_dict({"A": "one", "B": "two"})
# Export to CSV
csv_path = tmp_path / "test.csv"
builder.to_csv(csv_path)
# Read back and verify
df = pd.read_csv(csv_path)
# Check default column names
assert list(df.columns) == ["source", "target"]
# Check data
result_dict = dict(zip(df["source"], df["target"]))
assert result_dict == {"A": "one", "B": "two"}
def test_apply_crosswalk():
mapping = {"A": "1", "B": "2", "C": "3"}
assert apply_crosswalk("A", mapping) == "1"
assert apply_crosswalk("D", mapping) == "N/A"
assert apply_crosswalk("D", mapping, default="0") == "N/A"
def test_create_reverse_mapping():
builder = CrosswalkBuilder()
builder.from_dict({"A": "1", "B": "1", "C": "2", "D": "2", "E": "3"})
reversed_mapping = builder.create_reverse_mapping()
assert reversed_mapping == {"1": "A|B", "2": "C|D", "3": "E"}
# Defensive programming tests
def test_apply_crosswalk_edge_cases():
"""Test defensive programming features in apply_crosswalk"""
mapping = {"A": "1", "B": "2", "C": "3"}
# Test None and empty values
assert apply_crosswalk(None, mapping, default="default") == "default"
assert apply_crosswalk("", mapping, default="default") == "default"
assert (
apply_crosswalk(" ", mapping, default="default") == "default"
) # Empty after strip, WITH default
assert apply_crosswalk(" ", mapping) == "N/A" # Empty after strip, no default
# Test non-string inputs (should return original as string)
assert apply_crosswalk(123, mapping) == "N/A" # No default = return original
assert apply_crosswalk(123, mapping, default="default") == "N/A" # With default
# Test list-like string input
assert apply_crosswalk("['A']", mapping) == "1" # String representation of list
# Test comma-separated values
result = apply_crosswalk("A,B", mapping)
assert result in ["1|2", "2|1"] # Order may vary due to set
# Test pipe-separated values
result = apply_crosswalk("A|B", mapping)
assert result in ["1|2", "2|1"]
# Test list string format
result = apply_crosswalk("['A', 'B']", mapping)
assert result in ["1|2", "2|1"]
def test_apply_crosswalk_nested_structures():
"""Test handling of nested data structures"""
mapping = {"A": "1", "B": "2", "C": "3"}
# Test nested lists
assert apply_crosswalk("[['A'], ['B']]", mapping) in ["1|2", "2|1"]
# Test mixed nested structures
assert apply_crosswalk("['A', ['B', 'C']]", mapping) in [
"1|2|3",
"1|3|2",
"2|1|3",
"2|3|1",
"3|1|2",
"3|2|1",
]
def test_apply_crosswalk_reverse_mapping():
"""Test when value is already in mapping.values()"""
mapping = {"A": "1", "B": "2", "C": "3"}
# Value already mapped should be preserved
assert apply_crosswalk("1", mapping) == "1"
assert apply_crosswalk("2", mapping) == "2"
def test_apply_crosswalk_empty_results():
"""Test when mapping results in empty values"""
mapping = {"A": "", "B": "2", "C": ""} # Some empty mappings
# Should return non-empty results only
assert apply_crosswalk("A,B", mapping) == "2"
assert apply_crosswalk("A,C", mapping, default="default") == "N/A"
def test_flatten_to_strings():
"""Test the flatten_to_strings helper function"""
from src.utils.string_utils import flatten_to_strings
# Test simple cases
assert flatten_to_strings("A") == ["A"]
assert flatten_to_strings(["A", "B"]) == ["A", "B"]
assert flatten_to_strings(("A", "B")) == ["A", "B"]
# Test nested structures
assert flatten_to_strings([["A"], ["B"]]) == ["A", "B"]
assert flatten_to_strings([["A", "B"], "C"]) == ["A", "B", "C"]
# Test mixed types
assert flatten_to_strings([1, ["2", 3]]) == ["1", "2", "3"]
# Test with whitespace
assert flatten_to_strings([" A ", [" B "]]) == ["A", "B"]