feat: convert AM516 lead times to workdays

This commit is contained in:
ar51
2026-09-03 17:34:42 +08:00
parent fa8aee91b9
commit c7c5c3259c
6 changed files with 332 additions and 27 deletions

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import sys
import types
import unittest
from datetime import date
from types import SimpleNamespace
from unittest import mock
@@ -103,11 +104,16 @@ class TriggerGateTests(unittest.TestCase):
with (
mock.patch.object(bridge, "message_text", return_value="他怎么什么消息都回复啊"),
mock.patch.object(bridge, "build_report") as build_report,
mock.patch.object(
bridge,
"reply_received_acknowledgement",
) as acknowledge,
mock.patch.object(bridge, "reply") as reply,
):
bridge.handle_message(object(), data)
build_report.assert_not_called()
acknowledge.assert_not_called()
reply.assert_not_called()
def test_valid_message_builds_and_replies(self) -> None:
@@ -121,11 +127,16 @@ class TriggerGateTests(unittest.TestCase):
"message_text",
return_value="eRob70H50I-BHM-18CTC[V5] 10台",
),
mock.patch.object(
bridge,
"reply_received_acknowledgement",
) as acknowledge,
mock.patch.object(bridge, "build_report", return_value="report") as build_report,
mock.patch.object(bridge, "reply") as reply,
):
bridge.handle_message("client", data)
acknowledge.assert_called_once_with("client", "om_valid")
build_report.assert_called_once_with(
bridge.RequestInput(model="eRob70H50I-BHM-18CTC[V5]", quantity=10)
)
@@ -177,11 +188,16 @@ class TriggerGateTests(unittest.TestCase):
return_value="还有没有其他候选型号?",
),
mock.patch.object(bridge, "build_report") as build_report,
mock.patch.object(
bridge,
"reply_received_acknowledgement",
) as acknowledge,
mock.patch.object(bridge, "reply") as reply,
):
bridge.handle_message("client", data)
build_report.assert_not_called()
acknowledge.assert_not_called()
reply.assert_not_called()
def test_follow_up_continues_from_fifth_candidate_in_same_conversation(self) -> None:
@@ -201,6 +217,10 @@ class TriggerGateTests(unittest.TestCase):
"build_report",
side_effect=["initial report", "continued report"],
) as build_report,
mock.patch.object(
bridge,
"reply_received_acknowledgement",
) as acknowledge,
mock.patch.object(bridge, "reply") as reply,
):
bridge.handle_message("client", initial_data)
@@ -210,6 +230,13 @@ class TriggerGateTests(unittest.TestCase):
build_report.call_args_list,
[mock.call(request), mock.call(request, candidate_offset=4)],
)
self.assertEqual(
acknowledge.call_args_list,
[
mock.call("client", "om_initial"),
mock.call("client", "om_follow_up"),
],
)
self.assertEqual(
reply.call_args_list,
[
@@ -229,6 +256,10 @@ class TriggerGateTests(unittest.TestCase):
"message_text",
side_effect=[request_text, "还有没有其他候选型号?"],
),
mock.patch.object(
bridge,
"reply_received_acknowledgement",
) as acknowledge,
mock.patch.object(bridge, "build_report", return_value="report") as build_report,
mock.patch.object(bridge, "reply") as reply,
):
@@ -238,6 +269,7 @@ class TriggerGateTests(unittest.TestCase):
build_report.assert_called_once_with(
bridge.RequestInput(model="eRob70H50I-BHM-18CTC[V5]", quantity=10)
)
acknowledge.assert_called_once_with("client", "om_small")
reply.assert_called_once_with("client", "om_small", "report")
def test_expired_candidate_context_is_removed(self) -> None:
@@ -313,5 +345,90 @@ class CandidateBatchTests(unittest.TestCase):
self.assertNotIn(model, report)
class WorkdayConversionTests(unittest.TestCase):
def test_natural_day_conversion_rule(self) -> None:
expected = {
0: 0,
1: 0,
2: 1,
3: 2,
4: 3,
5: 3,
6: 4,
7: 5,
18: 13,
23: 16,
26: 18,
31: 22,
33: 23,
36: 25,
43: 30,
48: 34,
}
for natural_days, workdays in expected.items():
with self.subTest(natural_days=natural_days):
self.assertEqual(
bridge.natural_days_to_workdays(natural_days),
workdays,
)
def test_2026_official_holiday_and_makeup_workdays(self) -> None:
self.assertTrue(bridge.is_china_official_workday(date(2026, 9, 20)))
self.assertFalse(bridge.is_china_official_workday(date(2026, 9, 25)))
self.assertFalse(bridge.is_china_official_workday(date(2026, 10, 1)))
self.assertTrue(bridge.is_china_official_workday(date(2026, 10, 10)))
self.assertFalse(bridge.is_china_official_workday(date(2026, 10, 11)))
def test_workday_dates_match_validation_samples(self) -> None:
start = date(2026, 9, 2)
samples = {
18: date(2026, 9, 20),
23: date(2026, 9, 23),
26: date(2026, 9, 28),
16: date(2026, 9, 17),
31: date(2026, 10, 9),
33: date(2026, 10, 10),
36: date(2026, 10, 13),
43: date(2026, 10, 20),
48: date(2026, 10, 26),
}
for natural_days, expected_date in samples.items():
with self.subTest(natural_days=natural_days):
workdays = bridge.natural_days_to_workdays(natural_days)
self.assertEqual(
bridge.add_china_official_workdays(start, workdays),
expected_date,
)
def test_api_natural_days_drive_displayed_delivery_date(self) -> None:
payload = successful_payload()
summary = payload["data"]["交期汇总"]
summary["AM516预测交期"] = "2026-10-03"
summary["AM516总交期"] = 31
details = bridge.lead_time_details(
payload,
requested_quantity=10,
calculation_date=date(2026, 9, 2),
)
self.assertEqual(details["total_days"], 31)
self.assertEqual(details["workdays"], 22)
self.assertEqual(details["delivery_date"], "2026-10-09")
self.assertEqual(
details["prediction"],
"AM516预测交期2026-10-09原始31个自然日折算22个工作日",
)
def test_unconfigured_calendar_year_is_not_silently_estimated(self) -> None:
with self.assertRaisesRegex(
bridge.WorkdayCalendarUnavailableError,
"2027年国家法定工作日历未配置",
):
bridge.add_china_official_workdays(date(2026, 12, 31), 1)
if __name__ == "__main__":
unittest.main()