feat(lineage): unified LineageEngine, EventFetcher, and progressive trace API
Introduce a unified Seed→Lineage→Event pipeline replacing per-page Python BFS with Oracle CONNECT BY NOCYCLE queries, add staged /api/trace/* endpoints with rate limiting and L2 Redis caching, and wire progressive frontend loading via useTraceProgress composable. Key changes: - Add LineageEngine (split ancestors / merge sources / full genealogy) with QueryBuilder bind-param safety and batched IN clauses - Add EventFetcher with 6-domain support and L2 Redis cache - Add trace_routes Blueprint (seed-resolve, lineage, events) with profile dispatch, rate limiting, and Redis TTL=300s caching - Refactor query_tool_service to use LineageEngine and QueryBuilder, removing raw string interpolation (SQL injection fix) - Add rate limits and resolve cache to query_tool_routes - Integrate useTraceProgress into mid-section-defect with skeleton placeholders and fade-in transitions - Add lineageCache and on-demand lot lineage to query-tool - Add TraceProgressBar shared component - Remove legacy query-tool.js static script (3k lines) - Fix MatrixTable package column truncation (.slice(0,15) removed) - Archive unified-lineage-engine change, add trace-progressive-ui specs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ from unittest.mock import patch
|
||||
import pandas as pd
|
||||
|
||||
from mes_dashboard.services.mid_section_defect_service import (
|
||||
build_trace_aggregation_from_events,
|
||||
query_analysis,
|
||||
query_analysis_detail,
|
||||
query_all_loss_reasons,
|
||||
@@ -126,3 +127,116 @@ def test_query_all_loss_reasons_cache_miss_queries_and_caches_sorted_values(
|
||||
{'loss_reasons': ['A_REASON', 'B_REASON']},
|
||||
ttl=86400,
|
||||
)
|
||||
|
||||
|
||||
@patch('mes_dashboard.services.mid_section_defect_service.cache_set')
|
||||
@patch('mes_dashboard.services.mid_section_defect_service.cache_get', return_value=None)
|
||||
@patch('mes_dashboard.services.mid_section_defect_service.release_lock')
|
||||
@patch('mes_dashboard.services.mid_section_defect_service.try_acquire_lock', return_value=True)
|
||||
@patch('mes_dashboard.services.mid_section_defect_service._fetch_upstream_history')
|
||||
@patch('mes_dashboard.services.mid_section_defect_service._resolve_full_genealogy')
|
||||
@patch('mes_dashboard.services.mid_section_defect_service._fetch_tmtt_data')
|
||||
def test_trace_aggregation_matches_query_analysis_summary(
|
||||
mock_fetch_tmtt_data,
|
||||
mock_resolve_genealogy,
|
||||
mock_fetch_upstream_history,
|
||||
_mock_lock,
|
||||
_mock_release_lock,
|
||||
_mock_cache_get,
|
||||
_mock_cache_set,
|
||||
):
|
||||
tmtt_df = pd.DataFrame([
|
||||
{
|
||||
'CONTAINERID': 'CID-001',
|
||||
'CONTAINERNAME': 'LOT-001',
|
||||
'TRACKINQTY': 100,
|
||||
'REJECTQTY': 5,
|
||||
'LOSSREASONNAME': 'R1',
|
||||
'WORKFLOW': 'WF-A',
|
||||
'PRODUCTLINENAME': 'PKG-A',
|
||||
'PJ_TYPE': 'TYPE-A',
|
||||
'TMTT_EQUIPMENTNAME': 'TMTT-01',
|
||||
'TRACKINTIMESTAMP': '2025-01-10 10:00:00',
|
||||
'FINISHEDRUNCARD': 'FR-001',
|
||||
},
|
||||
{
|
||||
'CONTAINERID': 'CID-002',
|
||||
'CONTAINERNAME': 'LOT-002',
|
||||
'TRACKINQTY': 120,
|
||||
'REJECTQTY': 6,
|
||||
'LOSSREASONNAME': 'R2',
|
||||
'WORKFLOW': 'WF-B',
|
||||
'PRODUCTLINENAME': 'PKG-B',
|
||||
'PJ_TYPE': 'TYPE-B',
|
||||
'TMTT_EQUIPMENTNAME': 'TMTT-02',
|
||||
'TRACKINTIMESTAMP': '2025-01-11 10:00:00',
|
||||
'FINISHEDRUNCARD': 'FR-002',
|
||||
},
|
||||
])
|
||||
|
||||
ancestors = {
|
||||
'CID-001': {'CID-101'},
|
||||
'CID-002': set(),
|
||||
}
|
||||
upstream_normalized = {
|
||||
'CID-101': [{
|
||||
'workcenter_group': '中段',
|
||||
'equipment_id': 'EQ-01',
|
||||
'equipment_name': 'EQ-01',
|
||||
'spec_name': 'SPEC-A',
|
||||
'track_in_time': '2025-01-09 08:00:00',
|
||||
}],
|
||||
'CID-002': [{
|
||||
'workcenter_group': '中段',
|
||||
'equipment_id': 'EQ-02',
|
||||
'equipment_name': 'EQ-02',
|
||||
'spec_name': 'SPEC-B',
|
||||
'track_in_time': '2025-01-11 08:00:00',
|
||||
}],
|
||||
}
|
||||
upstream_events = {
|
||||
'CID-101': [{
|
||||
'WORKCENTER_GROUP': '中段',
|
||||
'EQUIPMENTID': 'EQ-01',
|
||||
'EQUIPMENTNAME': 'EQ-01',
|
||||
'SPECNAME': 'SPEC-A',
|
||||
'TRACKINTIMESTAMP': '2025-01-09 08:00:00',
|
||||
}],
|
||||
'CID-002': [{
|
||||
'WORKCENTER_GROUP': '中段',
|
||||
'EQUIPMENTID': 'EQ-02',
|
||||
'EQUIPMENTNAME': 'EQ-02',
|
||||
'SPECNAME': 'SPEC-B',
|
||||
'TRACKINTIMESTAMP': '2025-01-11 08:00:00',
|
||||
}],
|
||||
}
|
||||
|
||||
mock_fetch_tmtt_data.return_value = tmtt_df
|
||||
mock_resolve_genealogy.return_value = ancestors
|
||||
mock_fetch_upstream_history.return_value = upstream_normalized
|
||||
|
||||
summary = query_analysis('2025-01-01', '2025-01-31')
|
||||
staged_summary = build_trace_aggregation_from_events(
|
||||
'2025-01-01',
|
||||
'2025-01-31',
|
||||
seed_container_ids=['CID-001', 'CID-002'],
|
||||
lineage_ancestors={
|
||||
'CID-001': ['CID-101'],
|
||||
'CID-002': [],
|
||||
},
|
||||
upstream_events_by_cid=upstream_events,
|
||||
)
|
||||
|
||||
assert staged_summary['available_loss_reasons'] == summary['available_loss_reasons']
|
||||
assert staged_summary['genealogy_status'] == summary['genealogy_status']
|
||||
assert staged_summary['detail_total_count'] == len(summary['detail'])
|
||||
|
||||
assert staged_summary['kpi']['total_input'] == summary['kpi']['total_input']
|
||||
assert staged_summary['kpi']['lot_count'] == summary['kpi']['lot_count']
|
||||
assert staged_summary['kpi']['total_defect_qty'] == summary['kpi']['total_defect_qty']
|
||||
assert abs(
|
||||
staged_summary['kpi']['total_defect_rate'] - summary['kpi']['total_defect_rate']
|
||||
) <= 0.01
|
||||
|
||||
assert staged_summary['daily_trend'] == summary['daily_trend']
|
||||
assert staged_summary['charts'].keys() == summary['charts'].keys()
|
||||
|
||||
Reference in New Issue
Block a user