feat(wip): preserve filters between Overview and Detail with thundering-herd fix

URL is now single source of truth for filter state (workorder, lotid,
package, type, status) across WIP Overview and Detail pages. Drill-down
carries all filters + status; back button dynamically reflects Detail
changes. Backend Detail API now supports pj_type filter parameter.

Harden concurrency: add pagehide abort for MPA navigation, double-check
locking on Redis JSON parse and snapshot build to prevent thread pool
saturation during rapid page switching. Fix watchdog setsid and PID
discovery. Fix test_realtime_equipment_cache RUNCARDLOTID field mismatch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
egg
2026-02-11 07:22:48 +08:00
parent 3a15b0abaf
commit be22571421
24 changed files with 1522 additions and 266 deletions

View File

@@ -4,13 +4,16 @@
Tests cache read/write functionality and fallback mechanism.
"""
import pytest
from unittest.mock import patch, MagicMock
import pandas as pd
import json
import pytest
from unittest.mock import patch, MagicMock
import pandas as pd
import json
import threading
import time
from concurrent.futures import ThreadPoolExecutor
class TestGetCachedWipData:
class TestGetCachedWipData:
"""Test get_cached_wip_data function."""
@pytest.fixture(autouse=True)
@@ -78,18 +81,61 @@ class TestGetCachedWipData:
assert len(result) == 2
assert 'LOTID' in result.columns
def test_handles_invalid_json(self, reset_redis):
"""Test handles invalid JSON gracefully."""
import mes_dashboard.core.cache as cache
def test_handles_invalid_json(self, reset_redis):
"""Test handles invalid JSON gracefully."""
import mes_dashboard.core.cache as cache
mock_client = MagicMock()
mock_client.get.return_value = 'invalid json {'
with patch.object(cache, 'REDIS_ENABLED', True):
with patch.object(cache, 'get_redis_client', return_value=mock_client):
with patch.object(cache, 'get_key', return_value='mes_wip:data'):
result = cache.get_cached_wip_data()
assert result is None
with patch.object(cache, 'get_key', return_value='mes_wip:data'):
result = cache.get_cached_wip_data()
assert result is None
def test_concurrent_requests_parse_redis_once(self, reset_redis):
"""Concurrent misses should trigger Redis parse exactly once."""
import mes_dashboard.core.cache as cache
test_data = [
{'LOTID': 'LOT001', 'QTY': 100, 'WORKORDER': 'WO001'},
{'LOTID': 'LOT002', 'QTY': 200, 'WORKORDER': 'WO002'}
]
cached_json = json.dumps(test_data)
mock_client = MagicMock()
mock_client.get.return_value = cached_json
parse_count_lock = threading.Lock()
parse_count = 0
def slow_read_json(*args, **kwargs):
nonlocal parse_count
with parse_count_lock:
parse_count += 1
time.sleep(0.05)
return pd.DataFrame(test_data)
start_event = threading.Event()
def call_cache():
start_event.wait(timeout=1)
return cache.get_cached_wip_data()
with patch.object(cache, 'REDIS_ENABLED', True):
with patch.object(cache, 'get_redis_client', return_value=mock_client):
with patch.object(cache, 'get_key', return_value='mes_wip:data'):
with patch.object(cache.pd, 'read_json', side_effect=slow_read_json):
with ThreadPoolExecutor(max_workers=6) as pool:
futures = [pool.submit(call_cache) for _ in range(6)]
start_event.set()
results = [future.result(timeout=3) for future in futures]
assert parse_count == 1
assert mock_client.get.call_count == 1
assert all(result is not None for result in results)
assert all(len(result) == 2 for result in results)
class TestGetCachedSysDate: