feat(manual-batch): canonical plate + manual mode as default
ci / smoke (push) Canceled after 0s

- canonical_plate (uppercase alnum, strips space/dot/hyphen) at all plate
  write sites; pretty_plate for render (history, XLSX, operator tile) so
  B 1234 XYZ and B1234XYZ are one plate everywhere
- group_dos_by_plate compares canonical plates -> no false mixed_plates
- warn-only (never blocking) plate format hint in operator + monitoring modals
- batch.default_mode: manual (auto merged truck loads when a sack sat in the
  counting ROI); operator banner explains plate -> start -> stop
- docs + AGENTS known-limitation note; 85 tests pass
This commit is contained in:
andrew committed 2026-10-05 09:36:32 +07:00
1 parent 7ad995d8d1
commit 6c5b1c8b30
11 files changed
+128 -32

No files matched your search

+1 -1
View File
@@ -41,7 +41,7 @@ def test_repo_config_loads_and_validates(repo_config_path):
def test_do_block_and_batch_default_mode(repo_config_path):
cfg = load_config(repo_config_path)
assert cfg.batch.default_mode == "auto"
assert cfg.batch.default_mode == "manual"
# timeout_seconds = sack-idle (pause); truck_gone_tolerance_seconds = finalize
assert cfg.batch.timeout_seconds == 20.0
assert cfg.batch.truck_gone_tolerance_seconds == 30.0
+34 -3
View File
@@ -8,7 +8,9 @@ import pytest
from src.do_batch import (
BATCH_MODES,
OCR_ENGINES,
canonical_plate,
classify_unit_token,
pretty_plate,
empty_do_settings,
group_dos_by_plate,
is_valid_batch_mode,
@@ -23,6 +25,35 @@ from src.do_batch import (
from src.do_ocr import parse_fields_from_text
def test_canonical_plate_strips_separators():
assert canonical_plate(" b 1234 xyz ") == "B1234XYZ"
assert canonical_plate("B-1234-XYZ") == "B1234XYZ"
assert canonical_plate("B.1234.XYZ") == "B1234XYZ"
assert canonical_plate("b1234xyz") == canonical_plate("B 1234 XYZ")
assert canonical_plate(None) == ""
assert canonical_plate(" ") == ""
def test_pretty_plate_display_form():
assert pretty_plate("B1234XYZ") == "B 1234 XYZ"
assert pretty_plate("b 1234. xyz") == "B 1234 XYZ"
assert pretty_plate("B1") == "B 1"
assert pretty_plate("1234AB") == "1234 AB"
assert pretty_plate("") == ""
assert pretty_plate("!!") == "!!" # not a plate pattern, raw fallback
def test_group_dos_by_plate_ignores_spacing_only_difference():
dos = [
{"no_do": "D1", "plate": "B 1234 XYZ"},
{"no_do": "D2", "plate": "b1234xyz"},
{"no_do": "D3", "plate": "B-1234-XYZ"},
]
plate, out = group_dos_by_plate(dos)
assert plate == "B1234XYZ"
assert all(d["plate"] == "B1234XYZ" for d in out)
def test_normalize_batch_mode_defaults_auto():
assert normalize_batch_mode(None) == "auto"
assert normalize_batch_mode("bogus") == "auto"
@@ -59,7 +90,7 @@ def test_group_dos_same_plate():
{"no_do": "DO-2", "plate": "B 1234 XYZ"},
]
plate, out = group_dos_by_plate(dos)
assert plate == "B 1234 XYZ"
assert plate == "B1234XYZ"
assert [d["no_do"] for d in out] == ["DO-1", "DO-2"]
@@ -293,10 +324,10 @@ def test_manual_start_with_plate_normalizes_and_persists(dash_client):
assert res.status_code == 200
body = res.get_json()
assert body["success"] is True
assert body["plate"] == "B 1234 XYZ"
assert body["plate"] == "B1234XYZ"
assert state_path.exists()
state = json.loads(state_path.read_text())
assert state["plate"] == "B 1234 XYZ"
assert state["plate"] == "B1234XYZ"
assert state["batch_mode"] == "manual"
assert state["manual_control"] is True