Files
DashBoard/tests/test_portal_shell_wave_b_native_smoke.py
egg f14591c7dc feat(mid-section-defect): full-line bidirectional defect trace center with dual query mode
Transform /mid-section-defect from TMTT-only backward analysis into a full-line
bidirectional defect traceability center supporting all detection stations.

Key changes:
- Parameterized station detection: any workcenter group as detection station
- Bidirectional tracing: backward (upstream attribution) + forward (downstream reject rates)
- Dual query mode: date range OR LOT/工單/WAFER container-based seed resolution
- Multi-select filters for upstream station, equipment model (RESOURCEFAMILYNAME), and loss reasons
- Progressive 3-stage trace pipeline (seed-resolve → lineage → events) with streaming UI
- Equipment model lookup via resource cache instead of SPECNAME
- Session caching, auto-refresh, searchable MultiSelect with fuzzy matching
- Remove legacy tmtt-defect module (fully superseded)
- Archive openspec change artifacts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 16:16:33 +08:00

357 lines
12 KiB
Python

# -*- coding: utf-8 -*-
"""Wave B native-route smoke coverage for shell migration."""
from __future__ import annotations
import io
from unittest.mock import patch
import pytest
from mes_dashboard.app import create_app
@pytest.fixture
def app():
app = create_app("testing")
app.config["TESTING"] = True
return app
@pytest.fixture
def client(app):
return app.test_client()
def _login_as_admin(client) -> None:
with client.session_transaction() as sess:
sess["admin"] = {"displayName": "Admin", "employeeNo": "A001"}
def _build_excel_file() -> io.BytesIO:
import openpyxl
workbook = openpyxl.Workbook()
sheet = workbook.active
sheet["A1"] = "LOT_ID"
sheet["B1"] = "QTY"
sheet["A2"] = "LOT001"
sheet["B2"] = 100
sheet["A3"] = "LOT002"
sheet["B3"] = 200
buffer = io.BytesIO()
workbook.save(buffer)
buffer.seek(0)
return buffer
def test_job_query_native_smoke_query_search_export(client):
shell = client.get("/portal-shell/job-query?start_date=2026-02-01&end_date=2026-02-11")
assert shell.status_code == 200
page = client.get("/job-query", follow_redirects=False)
assert page.status_code == 302
assert page.location.endswith("/portal-shell/job-query")
with (
patch(
"mes_dashboard.services.resource_cache.get_all_resources",
return_value=[
{
"RESOURCEID": "EQ-01",
"RESOURCENAME": "Machine-01",
"WORKCENTERNAME": "WC-A",
"RESOURCEFAMILYNAME": "FAMILY-A",
}
],
),
patch(
"mes_dashboard.routes.job_query_routes.get_jobs_by_resources",
return_value={
"data": [{"JOBID": "JOB001", "RESOURCENAME": "Machine-01"}],
"total": 1,
"resource_count": 1,
},
),
patch(
"mes_dashboard.routes.job_query_routes.export_jobs_with_history",
return_value=iter(["JOBID,RESOURCEID\n", "JOB001,EQ-01\n"]),
),
):
resources = client.get("/api/job-query/resources")
assert resources.status_code == 200
assert resources.get_json()["total"] == 1
query = client.post(
"/api/job-query/jobs",
json={
"resource_ids": ["EQ-01"],
"start_date": "2026-02-01",
"end_date": "2026-02-11",
},
)
assert query.status_code == 200
assert query.get_json()["total"] == 1
export = client.post(
"/api/job-query/export",
json={
"resource_ids": ["EQ-01"],
"start_date": "2026-02-01",
"end_date": "2026-02-11",
},
)
assert export.status_code == 200
assert "text/csv" in export.content_type
def test_excel_query_native_smoke_upload_detect_query_export(client):
_login_as_admin(client)
shell = client.get("/portal-shell/excel-query?mode=upload")
assert shell.status_code == 200
page = client.get("/excel-query", follow_redirects=False)
assert page.status_code == 302
assert page.location.endswith("/portal-shell/excel-query")
from mes_dashboard.routes.excel_query_routes import _uploaded_excel_cache
_uploaded_excel_cache.clear()
upload = client.post(
"/api/excel-query/upload",
data={"file": (_build_excel_file(), "smoke.xlsx")},
content_type="multipart/form-data",
)
assert upload.status_code == 200
assert "LOT_ID" in upload.get_json()["columns"]
detect = client.post(
"/api/excel-query/column-type",
json={"column_name": "LOT_ID"},
)
assert detect.status_code == 200
assert detect.get_json()["column_name"] == "LOT_ID"
with (
patch(
"mes_dashboard.routes.excel_query_routes.execute_advanced_batch_query",
return_value={
"data": [{"LOT_ID": "LOT001", "QTY": 100}],
"columns": ["LOT_ID", "QTY"],
"total": 1,
},
),
patch(
"mes_dashboard.routes.excel_query_routes.execute_batch_query",
return_value={
"data": [{"LOT_ID": "LOT001", "QTY": 100}],
"columns": ["LOT_ID", "QTY"],
"total": 1,
},
),
):
query = client.post(
"/api/excel-query/execute-advanced",
json={
"table_name": "DWH.DW_MES_WIP",
"search_column": "LOT_ID",
"return_columns": ["LOT_ID", "QTY"],
"search_values": ["LOT001"],
"query_type": "in",
},
)
assert query.status_code == 200
assert query.get_json()["total"] == 1
export = client.post(
"/api/excel-query/export-csv",
json={
"table_name": "DWH.DW_MES_WIP",
"search_column": "LOT_ID",
"return_columns": ["LOT_ID", "QTY"],
"search_values": ["LOT001"],
},
)
assert export.status_code == 200
assert "text/csv" in export.content_type
def test_query_tool_native_smoke_resolve_history_association(client):
_login_as_admin(client)
shell = client.get("/portal-shell/query-tool?input_type=lot_id")
assert shell.status_code == 200
page = client.get("/query-tool", follow_redirects=False)
assert page.status_code == 302
assert page.location.endswith("/portal-shell/query-tool")
with (
patch(
"mes_dashboard.routes.query_tool_routes.resolve_lots",
return_value={
"data": [{"container_id": "488103800029578b"}],
"total": 1,
"input_count": 1,
"not_found": [],
},
),
patch(
"mes_dashboard.routes.query_tool_routes.get_lot_history",
return_value={"data": [{"CONTAINERID": "488103800029578b"}], "total": 1},
),
patch(
"mes_dashboard.routes.query_tool_routes.get_lot_materials",
return_value={"data": [{"MATERIALLOTID": "MAT001"}], "total": 1},
),
):
resolve = client.post(
"/api/query-tool/resolve",
json={"input_type": "lot_id", "values": ["GA23100020-A00-001"]},
)
assert resolve.status_code == 200
assert resolve.get_json()["total"] == 1
history = client.get("/api/query-tool/lot-history?container_id=488103800029578b")
assert history.status_code == 200
assert history.get_json()["total"] == 1
associations = client.get(
"/api/query-tool/lot-associations?container_id=488103800029578b&type=materials"
)
assert associations.status_code == 200
assert associations.get_json()["total"] == 1
def test_reject_history_native_smoke_query_sections_and_export(client):
_login_as_admin(client)
shell = client.get("/portal-shell/reject-history?start_date=2026-02-01&end_date=2026-02-11")
assert shell.status_code == 200
page = client.get("/reject-history", follow_redirects=False)
if page.status_code == 302:
assert page.status_code == 302
assert page.location.endswith("/portal-shell/reject-history")
elif page.status_code == 200:
assert page.status_code == 200
else:
raise AssertionError(f"unexpected status for /reject-history: {page.status_code}")
with (
patch(
"mes_dashboard.routes.reject_history_routes.get_filter_options",
return_value={
"workcenter_groups": [{"name": "WB", "sequence": 1}],
"reasons": ["R1"],
"meta": {"include_excluded_scrap": False},
},
),
patch(
"mes_dashboard.routes.reject_history_routes.query_summary",
return_value={
"MOVEIN_QTY": 100,
"REJECT_TOTAL_QTY": 10,
"DEFECT_QTY": 2,
"REJECT_RATE_PCT": 10.0,
"DEFECT_RATE_PCT": 2.0,
"REJECT_SHARE_PCT": 83.3333,
"AFFECTED_LOT_COUNT": 5,
"AFFECTED_WORKORDER_COUNT": 3,
"meta": {"include_excluded_scrap": False},
},
),
patch(
"mes_dashboard.routes.reject_history_routes.query_trend",
return_value={
"items": [
{
"bucket_date": "2026-02-01",
"MOVEIN_QTY": 100,
"REJECT_TOTAL_QTY": 10,
"DEFECT_QTY": 2,
"REJECT_RATE_PCT": 10.0,
"DEFECT_RATE_PCT": 2.0,
}
],
"granularity": "day",
"meta": {"include_excluded_scrap": False},
},
),
patch(
"mes_dashboard.routes.reject_history_routes.query_reason_pareto",
return_value={
"items": [
{
"reason": "R1",
"category": "CAT1",
"metric_value": 10,
"pct": 100.0,
"cumPct": 100.0,
}
],
"metric_mode": "reject_total",
"pareto_scope": "top80",
"meta": {"include_excluded_scrap": False},
},
),
patch(
"mes_dashboard.routes.reject_history_routes.query_list",
return_value={
"items": [
{
"TXN_DAY": "2026-02-01",
"WORKCENTER_GROUP": "WB",
"WORKCENTERNAME": "WB01",
"LOSSREASONNAME": "R1",
"REJECT_TOTAL_QTY": 10,
"DEFECT_QTY": 2,
}
],
"pagination": {"page": 1, "perPage": 50, "total": 1, "totalPages": 1},
"meta": {"include_excluded_scrap": False},
},
),
patch(
"mes_dashboard.routes.reject_history_routes.export_csv",
return_value=iter(
[
"TXN_DAY,REJECT_TOTAL_QTY,DEFECT_QTY\n",
"2026-02-01,10,2\n",
]
),
),
):
options = client.get("/api/reject-history/options?start_date=2026-02-01&end_date=2026-02-11")
assert options.status_code == 200
assert options.get_json()["success"] is True
assert options.get_json()["data"]["reasons"] == ["R1"]
summary = client.get("/api/reject-history/summary?start_date=2026-02-01&end_date=2026-02-11")
assert summary.status_code == 200
summary_payload = summary.get_json()
assert summary_payload["success"] is True
assert summary_payload["data"]["REJECT_TOTAL_QTY"] == 10
trend = client.get("/api/reject-history/trend?start_date=2026-02-01&end_date=2026-02-11")
assert trend.status_code == 200
assert trend.get_json()["success"] is True
assert trend.get_json()["data"]["items"][0]["bucket_date"] == "2026-02-01"
pareto = client.get("/api/reject-history/reason-pareto?start_date=2026-02-01&end_date=2026-02-11")
assert pareto.status_code == 200
assert pareto.get_json()["success"] is True
assert pareto.get_json()["data"]["items"][0]["reason"] == "R1"
detail = client.get("/api/reject-history/list?start_date=2026-02-01&end_date=2026-02-11")
assert detail.status_code == 200
assert detail.get_json()["success"] is True
assert detail.get_json()["data"]["pagination"]["total"] == 1
export = client.get("/api/reject-history/export?start_date=2026-02-01&end_date=2026-02-11")
assert export.status_code == 200
assert "text/csv" in export.content_type