feat(reject-history): fix silent data loss by propagating partial failure metadata to frontend
Chunk failures in BatchQueryEngine were silently discarded — `has_partial_failure` was tracked in Redis but never surfaced to the API response or frontend. Users could see incomplete data without any warning. This commit closes the gap end-to-end: Backend: - Track failed chunk time ranges (`failed_ranges`) in batch engine progress metadata - Add single retry for transient Oracle errors (timeout, connection) in `_execute_single_chunk` - Read `get_batch_progress()` after merge but before `redis_clear_batch()` cleanup - Inject `has_partial_failure`, `failed_chunk_count`, `failed_ranges` into API response meta - Persist partial failure flag to independent Redis key with TTL aligned to data storage layer - Add shared container-resolution policy module with wildcard/expansion guardrails - Refactor reason filter from single-value to multi-select (`reason` → `reasons`) Frontend: - Add client-side date range validation (730-day limit) before API submission - Display amber warning banner on partial failure with specific failed date ranges - Support generic fallback message for container-mode queries without date ranges - Update FilterPanel to support multi-select reason chips Specs & tests: - Create batch-query-resilience spec; update reject-history-api and reject-history-page specs - Add 7 new tests for retry, memory guard, failed ranges, partial failure propagation, TTL - Cross-service regression verified (hold, resource, job, msd — 411 tests pass) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -90,7 +90,7 @@ class TestValidateDateRange:
|
||||
assert '格式' in result or 'format' in result.lower()
|
||||
|
||||
|
||||
class TestValidateLotInput:
|
||||
class TestValidateLotInput:
|
||||
"""Tests for validate_lot_input function."""
|
||||
|
||||
def test_valid_lot_ids(self):
|
||||
@@ -117,53 +117,24 @@ class TestValidateLotInput:
|
||||
assert result is not None
|
||||
assert '至少一個' in result
|
||||
|
||||
def test_exceeds_lot_id_limit(self):
|
||||
"""Should reject LOT IDs exceeding limit."""
|
||||
values = [f'GA{i:09d}' for i in range(MAX_LOT_IDS + 1)]
|
||||
result = validate_lot_input('lot_id', values)
|
||||
assert result is not None
|
||||
assert '超過上限' in result
|
||||
assert str(MAX_LOT_IDS) in result
|
||||
|
||||
def test_exceeds_serial_number_limit(self):
|
||||
"""Should reject serial numbers exceeding limit."""
|
||||
values = [f'SN{i:06d}' for i in range(MAX_SERIAL_NUMBERS + 1)]
|
||||
result = validate_lot_input('serial_number', values)
|
||||
assert result is not None
|
||||
assert '超過上限' in result
|
||||
assert str(MAX_SERIAL_NUMBERS) in result
|
||||
|
||||
def test_exceeds_work_order_limit(self):
|
||||
"""Should reject work orders exceeding limit."""
|
||||
values = [f'WO{i:06d}' for i in range(MAX_WORK_ORDERS + 1)]
|
||||
result = validate_lot_input('work_order', values)
|
||||
assert result is not None
|
||||
assert '超過上限' in result
|
||||
assert str(MAX_WORK_ORDERS) in result
|
||||
def test_large_input_list_allowed_when_no_count_cap(self, monkeypatch):
|
||||
"""Should allow large lists when count cap is disabled."""
|
||||
monkeypatch.setenv("CONTAINER_RESOLVE_INPUT_MAX_VALUES", "0")
|
||||
values = [f'GA{i:09d}' for i in range(MAX_LOT_IDS + 50)]
|
||||
result = validate_lot_input('lot_id', values)
|
||||
assert result is None
|
||||
|
||||
def test_exceeds_gd_work_order_limit(self):
|
||||
"""Should reject GD work orders exceeding limit."""
|
||||
values = [f'GD{i:06d}' for i in range(MAX_GD_WORK_ORDERS + 1)]
|
||||
result = validate_lot_input('gd_work_order', values)
|
||||
def test_rejects_too_broad_wildcard_pattern(self, monkeypatch):
|
||||
"""Should reject broad wildcard like '%' to prevent full scan."""
|
||||
monkeypatch.setenv("CONTAINER_RESOLVE_PATTERN_MIN_PREFIX_LEN", "2")
|
||||
result = validate_lot_input('lot_id', ['%'])
|
||||
assert result is not None
|
||||
assert '超過上限' in result
|
||||
assert str(MAX_GD_WORK_ORDERS) in result
|
||||
|
||||
def test_exactly_at_limit(self):
|
||||
"""Should accept values exactly at limit."""
|
||||
values = [f'GA{i:09d}' for i in range(MAX_LOT_IDS)]
|
||||
result = validate_lot_input('lot_id', values)
|
||||
assert result is None
|
||||
|
||||
def test_unknown_input_type_uses_default_limit(self):
|
||||
"""Should use default limit for unknown input types."""
|
||||
values = [f'X{i}' for i in range(MAX_LOT_IDS)]
|
||||
result = validate_lot_input('unknown_type', values)
|
||||
assert result is None
|
||||
|
||||
values_over = [f'X{i}' for i in range(MAX_LOT_IDS + 1)]
|
||||
result = validate_lot_input('unknown_type', values_over)
|
||||
assert result is not None
|
||||
assert '萬用字元條件過於寬鬆' in result
|
||||
|
||||
def test_accepts_wildcard_with_prefix(self, monkeypatch):
|
||||
monkeypatch.setenv("CONTAINER_RESOLVE_PATTERN_MIN_PREFIX_LEN", "2")
|
||||
result = validate_lot_input('lot_id', ['GA25%'])
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestValidateEquipmentInput:
|
||||
|
||||
Reference in New Issue
Block a user