fix: 如实显示型号不存在提示并简化飞书异常回复

This commit is contained in:
ar51
2026-10-22 17:10:10 +08:00
parent d5eef9de57
commit a017c4b48c
4 changed files with 164 additions and 69 deletions

View File

@@ -60,11 +60,28 @@ Rules and expected results:
- a valid API date in another calendar year remains usable without a local holiday calendar; - a valid API date in another calendar year remains usable without a local holiday calendar;
- accept zero workdays; reject negative, fractional, boolean, or non-numeric workday values and invalid dates; - accept zero workdays; reject negative, fractional, boolean, or non-numeric workday values and invalid dates;
- if either new field is missing or invalid, report the lead-time result as unavailable instead of falling back to old fields or estimating; - if either new field is missing or invalid, report the lead-time result as unavailable instead of falling back to old fields or estimating;
- a stopped original-model reply includes `原因:` identifying each missing, empty, or invalid field and a suggestion to check the API data; no record is appended; - a stopped original-model reply includes `原因:` identifying each missing, empty, or invalid item in everyday Chinese and a suggestion to check the query result; no record is appended;
- each unavailable candidate is listed with its own reason; HTTP 404 or missing fields must not be summarized as a confirmed nonexistent model; - each unavailable candidate is listed with its own reason; HTTP 404 or missing fields must not be summarized as a confirmed nonexistent model;
- controlled-query failures show their safe category (configuration, authentication/permissions, HTTP, TLS, DNS/network, timeout, or JSON format), without displaying raw response bodies or secret-bearing error strings; - controlled-query failures explain their safe known cause in everyday Chinese, without HTTP codes, TLS, DNS, JSON, API Key, field-type requirements, arbitrary response bodies, or secret-bearing error strings;
- choose the fastest candidate using the API workday count, even when the natural-day totals would rank candidates differently. - choose the fastest candidate using the API workday count, even when the natural-day totals would rank candidates differently.
## Sample 1C | Product Model Not Found Notice
Local mocked response, matching AR51's screenshot:
```json
{"success": true, "data": "产品型号不存在,请检查产品型号是否正确"}
```
Expected:
- for `eRob70H50I-FS-18CNC[V5]`, quantity 5, show `查询系统提示:产品型号不存在,请检查产品型号是否正确。`;
- if this is the original model, stop before candidate queries and record append, with the reason and a plain-language suggestion to check the input;
- if this is a candidate, retain its batch position and show its own system notice under `不可用候选`, for both initial and continuation batches;
- do not show `data 格式错误` or assume a delivery date from `success=true`;
- unknown text and missing/unreadable query results must not be labeled as confirmed nonexistent models or echoed verbatim; explain the known issue in everyday Chinese;
- the fixed disclaimer appears once at the end, and no new dependency or configuration is required.
## Sample 1A | Continue With Other Candidates ## Sample 1A | Continue With Other Candidates
Preconditions: Preconditions:

View File

@@ -129,10 +129,11 @@ Do not use temporary curl, WebFetch, or any API method outside the controlled sc
## Unavailable Result Reasons ## Unavailable Result Reasons
- When the original model has no usable result, stop before recording and include a separate `原因:` line. Identify missing or empty `data` / `交期汇总` objects, or each missing, empty, or invalid required workday/date field. If old lead-time fields were returned, explain that they cannot substitute for the required fields. - When the original model has no usable result, stop before recording and include a separate `原因:` line. Explain missing/empty query results, missing/empty delivery summaries, or missing/empty/invalid workday counts and delivery dates in everyday Chinese. If other lead-time information was returned, explain that it cannot replace the required workday count and final delivery date.
- If `data` is the known business notice `产品型号不存在,请检查产品型号是否正确`, show `查询系统提示:产品型号不存在,请检查产品型号是否正确。` in both original-model errors and unavailable candidates, including continuation batches. This is a business result even when `success=true`; do not mislabel it as a response-format error. This notice does not provide a delivery prediction.
- Mention missing BOM only when the response explicitly marks it as absent. Field omissions alone do not establish that a model does not exist or that materials are short. - Mention missing BOM only when the response explicitly marks it as absent. Field omissions alone do not establish that a model does not exist or that materials are short.
- Preserve a separate reason for every unavailable candidate in initial and continuation replies; do not merge them into `不存在或接口未返回交期汇总`. - Preserve a separate reason for every unavailable candidate in initial and continuation replies; do not merge them into `不存在或接口未返回交期汇总`.
- For controlled-query failures, explain the known category: missing API Key, HTTP status and its meaning, TLS validation, DNS/connection failure, timeout, or response-format failure. Do not copy raw error messages, response bodies, secrets, or ERP details into replies. - For controlled-query failures, explain the known cause in everyday Chinese, such as incomplete setup, failed identity verification, missing permission, frequent queries, unavailable service, connection failure, timeout, or unreadable results. Do not show `data`, JSON, HTTP codes, TLS, DNS, API Key, field-type requirements, or other implementation details in Feishu replies. Do not copy arbitrary error messages, response bodies, secrets, or ERP details; only the explicitly recognized business notice above may be displayed as a system notice.
- If no deeper service-side cause can be confirmed, explicitly say it was not provided and needs runtime/service-log verification. Do not invent a business cause. Keep the fixed disclaimer once at the end. - If no deeper service-side cause can be confirmed, explicitly say it was not provided and needs runtime/service-log verification. Do not invent a business cause. Keep the fixed disclaimer once at the end.
## Shortage Material Names Rule ## Shortage Material Names Rule

View File

@@ -469,45 +469,56 @@ def model_difference_text(source_model: str, candidate_model: str) -> str:
return ";".join(differences) if differences else "无" return ";".join(differences) if differences else "无"
def business_unavailable_reason(payload: dict[str, Any]) -> str | None:
"""Recognize verified business notices without echoing arbitrary response text."""
data = payload.get("data")
if (
isinstance(data, str)
and data.strip().rstrip("。.") == "产品型号不存在,请检查产品型号是否正确"
):
return "查询系统提示:产品型号不存在,请检查产品型号是否正确。"
return None
def query_failure_reason(payload: dict[str, Any]) -> str: def query_failure_reason(payload: dict[str, Any]) -> str:
"""Translate known failure categories without echoing raw API bodies or secrets.""" """Translate known failure categories without echoing raw API bodies or secrets."""
error = payload.get("error") error = payload.get("error")
error = error.lower() if isinstance(error, str) else "" error = error.lower() if isinstance(error, str) else ""
if error.startswith("missing api key:"): if error.startswith("missing api key:"):
return "运行环境未配置交期预测 API Key,请由运行维护人员完成配置后重试。" return "交期查询尚未配置好,请联系维护人员处理后重试。"
if error == "tls certificate verification failed": if error == "tls certificate verification failed":
return "TLS 证书验证失败,请检查运行环境的证书信任配置。" return "暂时无法安全连接交期查询系统,请联系维护人员检查。"
if error == "tls verification bypass is not permitted for this target host": if error == "tls verification bypass is not permitted for this target host":
return "请求目标不满足已批准的域名和 TLS 校验要求,请检查接口配置。" return "交期查询的连接设置不符合要求,请联系维护人员检查。"
status = payload.get("status_code") status = payload.get("status_code")
http_reasons = { http_reasons = {
400: "接口拒绝请求参数", 400: "查询信息未被系统接受,请核对型号和数量后重试。",
401: "接口身份认证失败,请检查 API Key 是否有效", 401: "交期查询身份验证未通过,请联系维护人员处理。",
403: "接口拒绝访问,请检查账号或 API Key 权限", 403: "当前没有交期查询权限,请联系维护人员处理。",
404: "请求的接口或资源未找到;接口未说明是否为型号不存在", 404: "暂未找到查询服务或所需数据,无法确认是否与型号有关,请联系维护人员核查。",
422: "请求参数未通过接口校验", 422: "查询信息未通过系统检查,请核对型号和数量后重试。",
429: "接口请求频率受限,请稍后重试", 429: "查询过于频繁,请稍后重试。",
} }
if isinstance(status, int) and not isinstance(status, bool): if isinstance(status, int) and not isinstance(status, bool):
if status in http_reasons: if status in http_reasons:
return f"HTTP {status}:{http_reasons[status]}。" return http_reasons[status]
if 500 <= status <= 599: if 500 <= status <= 599:
return f"HTTP {status}:交期预测服务异常,请稍后重试。" return "交期查询系统暂时异常,请稍后重试。"
return f"接口返回 HTTP {status},未提供可安全展示的具体原因。" return "交期查询未成功,系统未说明具体原因,请联系维护人员核查。"
if error.startswith("request failed:"): if error.startswith("request failed:"):
if "timed out" in error or "timeout" in error: if "timed out" in error or "timeout" in error:
return "交期预测接口请求超时,请稍后重试。" return "交期查询等待时间过长,请稍后重试。"
if any( if any(
token in error token in error
for token in ("name or service not known", "nodename nor servname", "name resolution", "getaddrinfo") for token in ("name or service not known", "nodename nor servname", "name resolution", "getaddrinfo")
): ):
return "接口域名解析失败,请检查运行环境的网络和 DNS 配置。" return "暂时无法找到交期查询系统,请联系维护人员检查网络。"
return "网络连接失败,请检查运行环境的网络、代理和接口可达性。" return "暂时无法连接交期查询系统,请检查网络或稍后重试。"
if error.startswith("response is not valid json:"): if error.startswith("response is not valid json:"):
return "接口响应不是有效 JSON,无法读取交期结果。" return "交期查询系统返回的内容无法读取,请联系维护人员核查。"
if payload.get("success") is False: if payload.get("success") is False:
return "接口标记本次查询失败(success=false),未提供可安全展示的具体原因,需核对服务端日志。" return business_unavailable_reason(payload) or "交期查询未成功,系统未说明具体原因,请联系维护人员核查。"
return "受控查询脚本执行失败,未提供可安全展示的具体原因,需核对运行日志。" return "交期查询未完成,暂未查明具体原因,请联系维护人员核查。"
def run_controlled_query(request: RequestInput) -> dict[str, Any]: def run_controlled_query(request: RequestInput) -> dict[str, Any]:
@@ -530,17 +541,17 @@ def run_controlled_query(request: RequestInput) -> dict[str, Any]:
json_start = output.find("{") json_start = output.find("{")
if json_start < 0: if json_start < 0:
reason = ( reason = (
"受控查询脚本缺少运行依赖,请检查 Python 环境。" "交期查询的运行环境不完整,请联系维护人员处理。"
if "ModuleNotFoundError" in completed.stderr if "ModuleNotFoundError" in completed.stderr
else "受控查询脚本未输出 JSON 结果,请检查运行环境和脚本日志。" else "未收到交期查询结果,请联系维护人员核查。"
) )
raise RuntimeError(f"未生成候选结果。\n原因:{reason}") raise RuntimeError(f"未生成候选结果。\n原因:{reason}")
try: try:
payload = json.loads(output[json_start:]) payload = json.loads(output[json_start:])
except json.JSONDecodeError as exc: except json.JSONDecodeError as exc:
raise RuntimeError("未生成候选结果。\n原因:受控查询脚本输出的 JSON 格式异常。") from exc raise RuntimeError("未生成候选结果。\n原因:交期查询返回的内容无法读取,请联系维护人员核查。") from exc
if not isinstance(payload, dict): if not isinstance(payload, dict):
raise RuntimeError("未生成候选结果。\n原因:接口响应的顶层数据不是对象。") raise RuntimeError("未生成候选结果。\n原因:交期查询返回的内容无法读取,请联系维护人员核查。")
if completed.returncode != 0 or not payload.get("ok", True): if completed.returncode != 0 or not payload.get("ok", True):
status_code = payload.get("status_code") status_code = payload.get("status_code")
if isinstance(status_code, int) and status_code in {400, 404, 422}: if isinstance(status_code, int) and status_code in {400, 404, 422}:
@@ -571,38 +582,41 @@ def valid_delivery_date(value: Any) -> str | None:
def lead_time_unavailable_reason(payload: dict[str, Any]) -> str | None: def lead_time_unavailable_reason(payload: dict[str, Any]) -> str | None:
business_reason = business_unavailable_reason(payload)
if business_reason:
return business_reason
if "data" not in payload: if "data" not in payload:
return "接口响应缺少 data 字段,无法读取交期汇总。" return "查询系统未返回查询结果,暂时无法提供交期。"
data = payload.get("data") data = payload.get("data")
if data is None: if data is None:
return "接口响应的 data 为空,无法读取交期汇总。" return "查询结果为空,暂时无法提供交期。"
if not isinstance(data, dict): if not isinstance(data, dict):
return "接口响应的 data 格式错误(应为数据对象),无法读取交期汇总。" return "查询系统未返回可用的交期信息,请联系维护人员核查。"
if "交期汇总" not in data: if "交期汇总" not in data:
return "接口数据缺少“交期汇总”字段。" return "查询系统未返回交期汇总,暂时无法提供交期。"
summary = data.get("交期汇总") summary = data.get("交期汇总")
if summary is None: if summary is None:
return "接口返回的“交期汇总”为空。" return "查询系统返回的交期汇总为空,暂时无法提供交期。"
if not isinstance(summary, dict): if not isinstance(summary, dict):
return "接口返回的“交期汇总”格式错误(应为数据对象)。" return "查询系统返回的交期汇总无法读取,请联系维护人员核查。"
reasons: list[str] = [] reasons: list[str] = []
for field, validator, requirement in ( for field, validator, label in (
("AM516工作日交期", whole_day_count, "应为非负整数工作日"), ("AM516工作日交期", whole_day_count, "工作日交期"),
("AM516最终交货日期", valid_delivery_date, "应为有效的 YYYY-MM-DD 日期"), ("AM516最终交货日期", valid_delivery_date, "最终交货日期"),
): ):
if field not in summary: if field not in summary:
reasons.append(f"缺少“{field}”字段") reasons.append(f"查询系统未返回{label}")
elif summary[field] is None or summary[field] == "": elif summary[field] is None or summary[field] == "":
reasons.append(f"“{field}”返回空值") reasons.append(f"查询系统返回的{label}为空")
elif validator(summary[field]) is None: elif validator(summary[field]) is None:
reasons.append(f"“{field}”值无效({requirement})") reasons.append(f"查询系统返回的{label}无效,请联系维护人员核查")
if not reasons: if not reasons:
return None return None
if any(field in summary for field in ("AM516预测交期", "AM516总交期", "总交期")): if any(field in summary for field in ("AM516预测交期", "AM516总交期", "总交期")):
reasons.append("已返回旧交期或自然日字段,无法替代上述必需字段") reasons.append("已有的其他交期信息不能代替工作日交期和最终交货日期")
bom = data.get("BOM信息") bom = data.get("BOM信息")
if isinstance(bom, dict) and bom.get("是否有BOM(False则需提醒无BOM)") is False: if isinstance(bom, dict) and bom.get("是否有BOM(False则需提醒无BOM)") is False:
reasons.append("接口同时标记无有效 BOM,需核对 BOM 数据") reasons.append("查询系统同时提示缺少有效的产品物料清单,请联系维护人员核查")
return ";".join(reasons) + "。" return ";".join(reasons) + "。"
@@ -1019,7 +1033,7 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
raise RuntimeError( raise RuntimeError(
"原型号未生成可用交期结果,未生成候选结果。\n" "原型号未生成可用交期结果,未生成候选结果。\n"
f"原因:{baseline['unavailable_reason']}\n" f"原因:{baseline['unavailable_reason']}\n"
"处理建议:请接口维护人员核对上述原因、库存口径及服务端计算结果后重试。" "处理建议:请按上述提示核对;如确认信息无误,请联系维护人员核查。"
) )
candidates.append( candidates.append(
{ {

View File

@@ -586,15 +586,76 @@ class CandidateBatchTests(unittest.TestCase):
class UnavailableReasonTests(unittest.TestCase): class UnavailableReasonTests(unittest.TestCase):
def test_model_not_found_notice_survives_controlled_query_and_stops_baseline(self) -> None:
# Reproduce the actual success=true / string data response in AR51's screenshot.
payload = {"success": True, "data": "产品型号不存在,请检查产品型号是否正确"}
request = bridge.RequestInput("eRob70H50I-FS-18CNC[V5]", 5)
completed = SimpleNamespace(stdout=bridge.json.dumps(payload), stderr="", returncode=0)
with (
mock.patch.object(bridge.subprocess, "run", return_value=completed) as run,
mock.patch.object(bridge, "write_record") as record,
):
with self.assertRaises(RuntimeError) as caught:
bridge.build_report(request)
self.assertIn("查询系统提示:产品型号不存在,请检查产品型号是否正确。", str(caught.exception))
self.assertNotIn("data", str(caught.exception))
self.assertNotIn("格式错误", str(caught.exception))
run.assert_called_once()
record.assert_not_called()
def test_model_not_found_notice_is_used_in_initial_and_continuation_replies(self) -> None:
request = bridge.RequestInput("eRob70H50I-FS-18CN[V5]", 5)
specs = bridge.generate_candidate_specs(request.model)
def query(query_request: bridge.RequestInput) -> dict[str, object]:
if query_request.model == request.model:
return successful_payload()
return {"success": True, "data": "产品型号不存在,请检查产品型号是否正确"}
for offset in (0, bridge.CANDIDATE_BATCH_SIZE):
with (
self.subTest(offset=offset),
mock.patch.object(bridge, "run_controlled_query", side_effect=query) as run,
mock.patch.object(bridge, "write_record") as record,
):
report = bridge.build_report(request, candidate_offset=offset)
for spec in specs[offset : offset + bridge.CANDIDATE_BATCH_SIZE]:
self.assertIn(
f"- {spec['model']}:原因:查询系统提示:产品型号不存在,请检查产品型号是否正确。",
report,
)
self.assertEqual(run.call_count, bridge.CANDIDATE_BATCH_SIZE + (offset == 0))
self.assertEqual(len(record.call_args.args[2]), bridge.CANDIDATE_BATCH_SIZE)
self.assertNotIn("data 格式错误", report)
self.assertEqual(report.count(bridge.DISCLAIMER), 1)
def test_only_verified_business_notice_is_shown_without_echoing_arbitrary_text(self) -> None:
for success in (True, False):
payload = {"success": success, "data": "产品型号不存在,请检查产品型号是否正确"}
reason = (
bridge.lead_time_unavailable_reason(payload)
if success else bridge.query_failure_reason(payload)
)
self.assertEqual(reason, "查询系统提示:产品型号不存在,请检查产品型号是否正确。")
for data in (
"SECRET_MARKER sensitive raw data",
"产品型号不存在,请检查产品型号是否正确 SECRET_MARKER",
"",
):
reason = bridge.lead_time_unavailable_reason({"success": True, "data": data})
self.assertIn("未返回可用的交期信息", reason)
self.assertNotIn("SECRET_MARKER", reason)
self.assertNotIn("型号不存在", reason)
def test_missing_empty_and_invalid_required_fields_have_specific_reasons(self) -> None: def test_missing_empty_and_invalid_required_fields_have_specific_reasons(self) -> None:
cases = [ cases = [
("AM516工作日交期", "missing", "缺少“AM516工作日交期”字段"), ("AM516工作日交期", "missing", "未返回工作日交期"),
("AM516工作日交期", None, "“AM516工作日交期”返回空值"), ("AM516工作日交期", None, "工作日交期为空"),
("AM516工作日交期", -1, "应为非负整数工作日"), ("AM516工作日交期", -1, "工作日交期无效"),
("AM516工作日交期", True, "应为非负整数工作日"), ("AM516工作日交期", True, "工作日交期无效"),
("AM516最终交货日期", "missing", "缺少“AM516最终交货日期”字段"), ("AM516最终交货日期", "missing", "未返回最终交货日期"),
("AM516最终交货日期", "", "“AM516最终交货日期”返回空值"), ("AM516最终交货日期", "", "最终交货日期为空"),
("AM516最终交货日期", "2026-02-30", "应为有效的 YYYY-MM-DD 日期"), ("AM516最终交货日期", "2026-02-30", "最终交货日期无效"),
] ]
for field, value, expected in cases: for field, value, expected in cases:
with self.subTest(field=field, value=value): with self.subTest(field=field, value=value):
@@ -606,36 +667,36 @@ class UnavailableReasonTests(unittest.TestCase):
reason = bridge.lead_time_details(payload, 10)["unavailable_reason"] reason = bridge.lead_time_details(payload, 10)["unavailable_reason"]
self.assertIn(expected, reason) self.assertIn(expected, reason)
self.assertNotIn("型号不存在", reason) self.assertNotIn("型号不存在", reason)
self.assertIn("无法替代上述必需字段", reason) self.assertIn("不能代替工作日交期和最终交货日期", reason)
def test_missing_data_summary_and_bom_are_reported_from_response_evidence(self) -> None: def test_missing_data_summary_and_bom_are_reported_from_response_evidence(self) -> None:
for payload, expected in ( for payload, expected in (
({}, "缺少 data 字段"), ({}, "未返回查询结果"),
({"data": None}, "data 为空"), ({"data": None}, "查询结果为空"),
({"data": []}, "data 格式错误"), ({"data": []}, "未返回可用的交期信息"),
({"data": {}}, "缺少“交期汇总”字段"), ({"data": {}}, "未返回交期汇总"),
({"data": {"交期汇总": None}}, "“交期汇总”为空"), ({"data": {"交期汇总": None}}, "交期汇总为空"),
({"data": {"交期汇总": []}}, "“交期汇总”格式错误"), ({"data": {"交期汇总": []}}, "交期汇总无法读取"),
): ):
with self.subTest(payload=payload): with self.subTest(payload=payload):
self.assertIn(expected, bridge.lead_time_details(payload, 10)["unavailable_reason"]) self.assertIn(expected, bridge.lead_time_details(payload, 10)["unavailable_reason"])
payload = successful_payload() payload = successful_payload()
del payload["data"]["交期汇总"]["AM516工作日交期"] del payload["data"]["交期汇总"]["AM516工作日交期"]
payload["data"]["BOM信息"]["是否有BOM(False则需提醒无BOM)"] = False payload["data"]["BOM信息"]["是否有BOM(False则需提醒无BOM)"] = False
self.assertIn("接口同时标记无有效 BOM", bridge.lead_time_details(payload, 10)["unavailable_reason"]) self.assertIn("缺少有效的产品物料清单", bridge.lead_time_details(payload, 10)["unavailable_reason"])
def test_controlled_query_failure_reasons_do_not_echo_raw_error_content(self) -> None: def test_controlled_query_failure_reasons_do_not_echo_raw_error_content(self) -> None:
cases = [ cases = [
({"ok": False, "error": "missing API key: SECRET_MARKER"}, "未配置交期预测 API Key"), ({"ok": False, "error": "missing API key: SECRET_MARKER"}, "交期查询尚未配置好"),
({"ok": False, "status_code": 401}, "身份认证失败"), ({"ok": False, "status_code": 401}, "身份验证未通过"),
({"ok": False, "status_code": 403}, "拒绝访问"), ({"ok": False, "status_code": 403}, "没有交期查询权限"),
({"ok": False, "status_code": 404}, "接口未说明是否为型号不存在"), ({"ok": False, "status_code": 404}, "无法确认是否与型号有关"),
({"ok": False, "status_code": 429}, "请求频率受限"), ({"ok": False, "status_code": 429}, "查询过于频繁"),
({"ok": False, "status_code": 503}, "服务异常"), ({"ok": False, "status_code": 503}, "系统暂时异常"),
({"ok": False, "error": "TLS certificate verification failed"}, "TLS 证书验证失败"), ({"ok": False, "error": "TLS certificate verification failed"}, "无法安全连接"),
({"ok": False, "error": "request failed: nodename nor servname provided SECRET_MARKER"}, "域名解析失败"), ({"ok": False, "error": "request failed: nodename nor servname provided SECRET_MARKER"}, "无法找到交期查询系统"),
({"ok": False, "error": "request failed: timed out SECRET_MARKER"}, "请求超时"), ({"ok": False, "error": "request failed: timed out SECRET_MARKER"}, "等待时间过长"),
({"success": False, "message": "SECRET_MARKER"}, "未提供可安全展示的具体原因"), ({"success": False, "message": "SECRET_MARKER"}, "系统未说明具体原因"),
] ]
request = bridge.RequestInput(model="eRob70H120I-BHM-18CT[V5]", quantity=25) request = bridge.RequestInput(model="eRob70H120I-BHM-18CT[V5]", quantity=25)
for payload, expected in cases: for payload, expected in cases:
@@ -649,6 +710,8 @@ class UnavailableReasonTests(unittest.TestCase):
bridge.run_controlled_query(request) bridge.run_controlled_query(request)
self.assertIn(expected, str(caught.exception)) self.assertIn(expected, str(caught.exception))
self.assertNotIn("SECRET_MARKER", str(caught.exception)) self.assertNotIn("SECRET_MARKER", str(caught.exception))
for technical_term in ("HTTP", "TLS", "DNS", "JSON", "API Key", "success=false"):
self.assertNotIn(technical_term, str(caught.exception))
def test_stopped_feishu_reply_includes_actual_missing_field_and_no_record(self) -> None: def test_stopped_feishu_reply_includes_actual_missing_field_and_no_record(self) -> None:
payload = successful_payload() payload = successful_payload()
@@ -663,7 +726,7 @@ class UnavailableReasonTests(unittest.TestCase):
): ):
bridge.handle_message("client", data) bridge.handle_message("client", data)
report = reply.call_args.args[2] report = reply.call_args.args[2]
self.assertIn("原因:缺少“AM516工作日交期”字段", report) self.assertIn("原因:查询系统未返回工作日交期", report)
self.assertIn("处理建议", report) self.assertIn("处理建议", report)
self.assertEqual(report.count(bridge.DISCLAIMER), 1) self.assertEqual(report.count(bridge.DISCLAIMER), 1)
write_record.assert_not_called() write_record.assert_not_called()
@@ -686,7 +749,7 @@ class UnavailableReasonTests(unittest.TestCase):
): ):
report = bridge.build_report(request) report = bridge.build_report(request)
self.assertIn(f"- {specs[0]['model']}:原因:HTTP 422", report) self.assertIn(f"- {specs[0]['model']}:原因:HTTP 422", report)
self.assertIn(f"- {specs[1]['model']}:原因:缺少“AM516最终交货日期”字段", report) self.assertIn(f"- {specs[1]['model']}:原因:查询系统未返回最终交货日期", report)
self.assertNotIn("不存在或接口未返回", report) self.assertNotIn("不存在或接口未返回", report)
@@ -933,7 +996,7 @@ class ApiWorkdayOutputTests(unittest.TestCase):
mock.patch.object(bridge, "run_controlled_query", return_value=payload), mock.patch.object(bridge, "run_controlled_query", return_value=payload),
mock.patch.object(bridge, "write_record") as write_record, mock.patch.object(bridge, "write_record") as write_record,
): ):
with self.assertRaisesRegex(RuntimeError, "缺少“AM516工作日交期”字段"): with self.assertRaisesRegex(RuntimeError, "未返回工作日交期"):
bridge.build_report(request) bridge.build_report(request)
write_record.assert_not_called() write_record.assert_not_called()