fix: use message reaction for query acknowledgement
This commit is contained in:
@@ -14,7 +14,7 @@ Expected:
|
|||||||
|
|
||||||
- model parses successfully;
|
- model parses successfully;
|
||||||
- quantity is accepted;
|
- quantity is accepted;
|
||||||
- the bot immediately replies with `👀` after the valid trigger is accepted and before the API query starts;
|
- the bot immediately adds the Feishu `OnIt` (typing) reaction to the original message after the valid trigger is accepted and before the API query starts;
|
||||||
- the controlled script uses the macOS system trust store and keeps TLS verification enabled;
|
- the controlled script uses the macOS system trust store and keeps TLS verification enabled;
|
||||||
- no temporary TLS-bypass parameter is required for the normal happy path;
|
- no temporary TLS-bypass parameter is required for the normal happy path;
|
||||||
- API script runs;
|
- API script runs;
|
||||||
@@ -72,7 +72,7 @@ is not required.
|
|||||||
|
|
||||||
Expected:
|
Expected:
|
||||||
|
|
||||||
- the bot immediately replies with `👀` after the continuation and active-context gates pass;
|
- the bot immediately adds the Feishu `OnIt` (typing) reaction to the original message after the continuation and active-context gates pass;
|
||||||
- the bot reuses the active model and quantity only for that same sender and chat;
|
- the bot reuses the active model and quantity only for that same sender and chat;
|
||||||
- the bot queries candidate positions 5-8 in the unchanged rule order;
|
- the bot queries candidate positions 5-8 in the unchanged rule order;
|
||||||
- the original model and candidate positions 1-4 are not queried or displayed again;
|
- the original model and candidate positions 1-4 are not queried or displayed again;
|
||||||
@@ -97,7 +97,7 @@ eRob110H160I-FHM-18ET[V6]
|
|||||||
Expected:
|
Expected:
|
||||||
|
|
||||||
- bot stays silent;
|
- bot stays silent;
|
||||||
- no acknowledgement emoji is sent;
|
- no acknowledgement reaction is added;
|
||||||
- no API call is made;
|
- no API call is made;
|
||||||
- no record is appended.
|
- no record is appended.
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
- [ ] If that domain-and-permission record is absent, outbound access remains blocked; a general AR51 functional-test approval is not sufficient by itself.
|
- [ ] If that domain-and-permission record is absent, outbound access remains blocked; a general AR51 functional-test approval is not sufficient by itself.
|
||||||
- [ ] Only after approval is recorded, network can reach the AR836 delivery prediction API.
|
- [ ] Only after approval is recorded, network can reach the AR836 delivery prediction API.
|
||||||
- [ ] Feishu bot bridge or local entry is configured.
|
- [ ] Feishu bot bridge or local entry is configured.
|
||||||
|
- [ ] The Feishu app has `im:message.reactions:write_only` so the bot can add the `OnIt` received reaction to valid query messages.
|
||||||
- [ ] Logs are stored outside Git or in an approved non-secret location.
|
- [ ] Logs are stored outside Git or in an approved non-secret location.
|
||||||
|
|
||||||
## 5. Acceptance
|
## 5. Acceptance
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
- [ ] The initial response queries no more than four recommended candidates in addition to the original-model baseline; same-sender, same-chat continuation queries the next four without duplication.
|
- [ ] The initial response queries no more than four recommended candidates in addition to the original-model baseline; same-sender, same-chat continuation queries the next four without duplication.
|
||||||
- [ ] Candidate continuation without matching active context stays silent; a bridge restart or six-hour expiry requires the complete model and quantity again.
|
- [ ] Candidate continuation without matching active context stays silent; a bridge restart or six-hour expiry requires the complete model and quantity again.
|
||||||
- [ ] Output includes the fixed internal-candidate disclaimer.
|
- [ ] Output includes the fixed internal-candidate disclaimer.
|
||||||
|
- [ ] A real valid query shows the `OnIt` typing reaction below the original message rather than creating a reply bubble or reply thread.
|
||||||
- [ ] Records append to `records/delivery_prediction_records.md`.
|
- [ ] Records append to `records/delivery_prediction_records.md`.
|
||||||
- [ ] A direct AR51 task in Mac Studio Codex can modify a test project file or perform the intended Git update without being blocked by the Feishu read-only rule.
|
- [ ] A direct AR51 task in Mac Studio Codex can modify a test project file or perform the intended Git update without being blocked by the Feishu read-only rule.
|
||||||
- [ ] A Feishu request to modify a rule or other project file is refused and leaves all project files unchanged.
|
- [ ] A Feishu request to modify a rule or other project file is refused and leaves all project files unchanged.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
4. Confirm a separate trusted AR51 approval record explicitly covers `api.zeroerr-agent.com`, the AM516 use case, and API-key use.
|
4. Confirm a separate trusted AR51 approval record explicitly covers `api.zeroerr-agent.com`, the AM516 use case, and API-key use.
|
||||||
5. If that domain-and-permission record is absent, stop before any outbound request; a general functional-test approval is insufficient by itself.
|
5. If that domain-and-permission record is absent, stop before any outbound request; a general functional-test approval is insufficient by itself.
|
||||||
6. Use model recommendation rules to generate the complete ordered internal candidate pool.
|
6. Use model recommendation rules to generate the complete ordered internal candidate pool.
|
||||||
7. Immediately reply with `👀` to acknowledge the valid request.
|
7. Immediately add the Feishu `OnIt` (typing) reaction to the original message to acknowledge the valid request.
|
||||||
8. Run the controlled script for the original model baseline and only the first four candidate models.
|
8. Run the controlled script for the original model baseline and only the first four candidate models.
|
||||||
9. Convert each numeric natural-day lead time to workdays with the approved quotient-and-remainder rule, then count those workdays from the local query date through the configured China statutory workday calendar. The query date itself is excluded.
|
9. Convert each numeric natural-day lead time to workdays with the approved quotient-and-remainder rule, then count those workdays from the local query date through the configured China statutory workday calendar. The query date itself is excluded.
|
||||||
10. Summarize the current batch, including the original natural days, converted workdays, and calculated delivery date; state how many candidates remain and invite the same sender to ask naturally for more when candidates remain.
|
10. Summarize the current batch, including the original natural days, converted workdays, and calculated delivery date; state how many candidates remain and invite the same sender to ask naturally for more when candidates remain.
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
1. Accept a clear request for more candidates only from the same sender in the same Feishu chat as the active initial query. Equivalent expressions such as `还有吗`、`有没有别的`、`再推荐几个`、`继续`、`下一批` or `more candidates` are valid; no exact phrase is required.
|
1. Accept a clear request for more candidates only from the same sender in the same Feishu chat as the active initial query. Equivalent expressions such as `还有吗`、`有没有别的`、`再推荐几个`、`继续`、`下一批` or `more candidates` are valid; no exact phrase is required.
|
||||||
2. The in-memory continuation context expires after six hours and is cleared by a bridge restart. If no matching context exists, stay silent and require a new complete model-and-quantity query.
|
2. The in-memory continuation context expires after six hours and is cleared by a bridge restart. If no matching context exists, stay silent and require a new complete model-and-quantity query.
|
||||||
3. Immediately reply with `👀`, then query the next four unqueried rule candidates; do not re-query the original baseline or any earlier candidate.
|
3. Immediately add the Feishu `OnIt` (typing) reaction to the original message, then query the next four unqueried rule candidates; do not re-query the original baseline or any earlier candidate.
|
||||||
4. Missing or unsupported candidates still consume their rule-order positions and remain under `不可用候选`.
|
4. Missing or unsupported candidates still consume their rule-order positions and remain under `不可用候选`.
|
||||||
5. Append one record block for the models queried in this continuation batch, then state whether more candidates remain.
|
5. Append one record block for the models queried in this continuation batch, then state whether more candidates remain.
|
||||||
6. A new complete model-and-quantity request replaces the earlier continuation context for that sender and chat.
|
6. A new complete model-and-quantity request replaces the earlier continuation context for that sender and chat.
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ Stop if:
|
|||||||
- Enter the query workflow only when one message contains exactly one complete,
|
- Enter the query workflow only when one message contains exactly one complete,
|
||||||
rule-valid eRob model and exactly one positive quantity.
|
rule-valid eRob model and exactly one positive quantity.
|
||||||
- After a valid initial query or valid same-conversation candidate continuation
|
- After a valid initial query or valid same-conversation candidate continuation
|
||||||
passes this gate, immediately reply with `👀` before starting the API query so
|
passes this gate, immediately add the Feishu `OnIt` (typing) reaction to the
|
||||||
the sender knows the request was received.
|
original message before starting the API query so the sender knows it was received.
|
||||||
- If the model or quantity is missing, invalid, multiple, or ambiguous, stay
|
- If the model or quantity is missing, invalid, multiple, or ambiguous, stay
|
||||||
silent: send no acknowledgement or other Feishu reply, make no API request,
|
silent: add no reaction, send no Feishu reply, make no API request,
|
||||||
and append no record.
|
and append no record.
|
||||||
- An @mention by itself does not bypass this content gate.
|
- An @mention by itself does not bypass this content gate.
|
||||||
- After a valid message passes the gate, API or runtime failures may still return
|
- After a valid message passes the gate, API or runtime failures may still return
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ Do not start the bridge or the delivery-prediction API request until AR51 has cr
|
|||||||
connection, then publish the app version.
|
connection, then publish the app version.
|
||||||
|
|
||||||
For this bridge, request only these scopes: `im:message` for reading and
|
For this bridge, request only these scopes: `im:message` for reading and
|
||||||
replying, plus `im:message.p2p_msg` for direct messages and/or
|
replying, `im:message.reactions:write_only` for adding the `OnIt` received
|
||||||
|
reaction, plus `im:message.p2p_msg` for direct messages and/or
|
||||||
`im:message.group_at_msg` for messages that @ the bot in a group. Do not grant
|
`im:message.group_at_msg` for messages that @ the bot in a group. Do not grant
|
||||||
the group-wide message scope unless the business case requires it.
|
the group-wide message scope unless the business case requires it.
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ ALLOWED_VERSIONS = {
|
|||||||
}
|
}
|
||||||
CANDIDATE_BATCH_SIZE = 4
|
CANDIDATE_BATCH_SIZE = 4
|
||||||
CANDIDATE_CONTINUATION_TTL_SECONDS = 6 * 60 * 60
|
CANDIDATE_CONTINUATION_TTL_SECONDS = 6 * 60 * 60
|
||||||
RECEIVED_ACKNOWLEDGEMENT = "👀"
|
RECEIVED_REACTION_TYPE = "OnIt"
|
||||||
|
|
||||||
|
|
||||||
def _inclusive_date_range(start: date, end: date) -> frozenset[date]:
|
def _inclusive_date_range(start: date, end: date) -> frozenset[date]:
|
||||||
@@ -1160,33 +1160,40 @@ def reply(client: lark.Client, message_id: str, text: str) -> None:
|
|||||||
logging.info("Feishu reply sent: message_id=%s parts=%s", message_id, len(post_contents))
|
logging.info("Feishu reply sent: message_id=%s parts=%s", message_id, len(post_contents))
|
||||||
|
|
||||||
|
|
||||||
def reply_received_acknowledgement(client: lark.Client, message_id: str) -> None:
|
def add_received_reaction(client: lark.Client, message_id: str) -> bool:
|
||||||
request = (
|
try:
|
||||||
lark.im.v1.ReplyMessageRequest.builder()
|
request_body = (
|
||||||
.message_id(message_id)
|
lark.im.v1.CreateMessageReactionRequestBody.builder()
|
||||||
.request_body(
|
.reaction_type({"emoji_type": RECEIVED_REACTION_TYPE})
|
||||||
lark.im.v1.ReplyMessageRequestBody.builder()
|
|
||||||
.msg_type("text")
|
|
||||||
.content(
|
|
||||||
json.dumps(
|
|
||||||
{"text": RECEIVED_ACKNOWLEDGEMENT},
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.uuid(f"{message_id[:44]}-ack")
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.build()
|
request = (
|
||||||
)
|
lark.im.v1.CreateMessageReactionRequest.builder()
|
||||||
response = client.im.v1.message.reply(request)
|
.message_id(message_id)
|
||||||
if not response.success():
|
.request_body(request_body)
|
||||||
logging.error(
|
.build()
|
||||||
"Feishu acknowledgement failed: code=%s message_id=%s",
|
)
|
||||||
response.code,
|
response = client.im.v1.message_reaction.create(request)
|
||||||
|
except Exception:
|
||||||
|
logging.exception(
|
||||||
|
"Feishu received reaction raised; continuing query: message_id=%s",
|
||||||
message_id,
|
message_id,
|
||||||
)
|
)
|
||||||
return
|
return False
|
||||||
logging.info("Feishu acknowledgement sent: message_id=%s", message_id)
|
if not response.success():
|
||||||
|
logging.error(
|
||||||
|
"Feishu received reaction failed: code=%s msg=%s message_id=%s",
|
||||||
|
response.code,
|
||||||
|
response.msg,
|
||||||
|
message_id,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
logging.info(
|
||||||
|
"Feishu received reaction added: type=%s message_id=%s",
|
||||||
|
RECEIVED_REACTION_TYPE,
|
||||||
|
message_id,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
_seen_message_ids: set[str] = set()
|
_seen_message_ids: set[str] = set()
|
||||||
@@ -1215,7 +1222,7 @@ def handle_message(client: lark.Client, data: lark.im.v1.P2ImMessageReceiveV1) -
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
reply_received_acknowledgement(client, message_id)
|
add_received_reaction(client, message_id)
|
||||||
|
|
||||||
if continuation_state is None:
|
if continuation_state is None:
|
||||||
clear_candidate_continuation(key)
|
clear_candidate_continuation(key)
|
||||||
|
|||||||
@@ -106,14 +106,14 @@ class TriggerGateTests(unittest.TestCase):
|
|||||||
mock.patch.object(bridge, "build_report") as build_report,
|
mock.patch.object(bridge, "build_report") as build_report,
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
bridge,
|
bridge,
|
||||||
"reply_received_acknowledgement",
|
"add_received_reaction",
|
||||||
) as acknowledge,
|
) as react,
|
||||||
mock.patch.object(bridge, "reply") as reply,
|
mock.patch.object(bridge, "reply") as reply,
|
||||||
):
|
):
|
||||||
bridge.handle_message(object(), data)
|
bridge.handle_message(object(), data)
|
||||||
|
|
||||||
build_report.assert_not_called()
|
build_report.assert_not_called()
|
||||||
acknowledge.assert_not_called()
|
react.assert_not_called()
|
||||||
reply.assert_not_called()
|
reply.assert_not_called()
|
||||||
|
|
||||||
def test_valid_message_builds_and_replies(self) -> None:
|
def test_valid_message_builds_and_replies(self) -> None:
|
||||||
@@ -129,19 +129,111 @@ class TriggerGateTests(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
bridge,
|
bridge,
|
||||||
"reply_received_acknowledgement",
|
"add_received_reaction",
|
||||||
) as acknowledge,
|
return_value=False,
|
||||||
|
) as react,
|
||||||
mock.patch.object(bridge, "build_report", return_value="report") as build_report,
|
mock.patch.object(bridge, "build_report", return_value="report") as build_report,
|
||||||
mock.patch.object(bridge, "reply") as reply,
|
mock.patch.object(bridge, "reply") as reply,
|
||||||
):
|
):
|
||||||
bridge.handle_message("client", data)
|
bridge.handle_message("client", data)
|
||||||
|
|
||||||
acknowledge.assert_called_once_with("client", "om_valid")
|
react.assert_called_once_with("client", "om_valid")
|
||||||
build_report.assert_called_once_with(
|
build_report.assert_called_once_with(
|
||||||
bridge.RequestInput(model="eRob70H50I-BHM-18CTC[V5]", quantity=10)
|
bridge.RequestInput(model="eRob70H50I-BHM-18CTC[V5]", quantity=10)
|
||||||
)
|
)
|
||||||
reply.assert_called_once_with("client", "om_valid", "report")
|
reply.assert_called_once_with("client", "om_valid", "report")
|
||||||
|
|
||||||
|
def test_received_reaction_targets_original_message_with_onit(self) -> None:
|
||||||
|
class ReactionBodyBuilder:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.value: dict[str, str] | None = None
|
||||||
|
|
||||||
|
def reaction_type(self, value: dict[str, str]) -> "ReactionBodyBuilder":
|
||||||
|
self.value = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def build(self) -> SimpleNamespace:
|
||||||
|
return SimpleNamespace(reaction_type=self.value)
|
||||||
|
|
||||||
|
class ReactionRequestBuilder:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.message_id_value: str | None = None
|
||||||
|
self.body: SimpleNamespace | None = None
|
||||||
|
|
||||||
|
def message_id(self, value: str) -> "ReactionRequestBuilder":
|
||||||
|
self.message_id_value = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def request_body(self, value: SimpleNamespace) -> "ReactionRequestBuilder":
|
||||||
|
self.body = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def build(self) -> SimpleNamespace:
|
||||||
|
return SimpleNamespace(
|
||||||
|
message_id=self.message_id_value,
|
||||||
|
request_body=self.body,
|
||||||
|
)
|
||||||
|
|
||||||
|
body_type = SimpleNamespace(builder=ReactionBodyBuilder)
|
||||||
|
request_type = SimpleNamespace(builder=ReactionRequestBuilder)
|
||||||
|
create_reaction = mock.Mock(
|
||||||
|
return_value=SimpleNamespace(
|
||||||
|
success=lambda: True,
|
||||||
|
code=0,
|
||||||
|
msg="ok",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
client = SimpleNamespace(
|
||||||
|
im=SimpleNamespace(
|
||||||
|
v1=SimpleNamespace(
|
||||||
|
message_reaction=SimpleNamespace(create=create_reaction),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
bridge.lark.im.v1,
|
||||||
|
"CreateMessageReactionRequestBody",
|
||||||
|
body_type,
|
||||||
|
create=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
bridge.lark.im.v1,
|
||||||
|
"CreateMessageReactionRequest",
|
||||||
|
request_type,
|
||||||
|
create=True,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = bridge.add_received_reaction(client, "om_original")
|
||||||
|
|
||||||
|
self.assertTrue(result)
|
||||||
|
sent_request = create_reaction.call_args.args[0]
|
||||||
|
self.assertEqual(sent_request.message_id, "om_original")
|
||||||
|
self.assertEqual(
|
||||||
|
sent_request.request_body.reaction_type,
|
||||||
|
{"emoji_type": "OnIt"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_received_reaction_exception_does_not_escape(self) -> None:
|
||||||
|
broken_body_type = SimpleNamespace(
|
||||||
|
builder=mock.Mock(side_effect=RuntimeError("reaction unavailable"))
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
bridge.lark.im.v1,
|
||||||
|
"CreateMessageReactionRequestBody",
|
||||||
|
broken_body_type,
|
||||||
|
create=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(bridge.logging, "exception") as log_exception,
|
||||||
|
):
|
||||||
|
result = bridge.add_received_reaction(object(), "om_original")
|
||||||
|
|
||||||
|
self.assertFalse(result)
|
||||||
|
log_exception.assert_called_once()
|
||||||
|
|
||||||
def test_candidate_continuation_phrase_is_recognized(self) -> None:
|
def test_candidate_continuation_phrase_is_recognized(self) -> None:
|
||||||
valid_phrases = (
|
valid_phrases = (
|
||||||
"还有没有其他候选型号?",
|
"还有没有其他候选型号?",
|
||||||
@@ -190,14 +282,14 @@ class TriggerGateTests(unittest.TestCase):
|
|||||||
mock.patch.object(bridge, "build_report") as build_report,
|
mock.patch.object(bridge, "build_report") as build_report,
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
bridge,
|
bridge,
|
||||||
"reply_received_acknowledgement",
|
"add_received_reaction",
|
||||||
) as acknowledge,
|
) as react,
|
||||||
mock.patch.object(bridge, "reply") as reply,
|
mock.patch.object(bridge, "reply") as reply,
|
||||||
):
|
):
|
||||||
bridge.handle_message("client", data)
|
bridge.handle_message("client", data)
|
||||||
|
|
||||||
build_report.assert_not_called()
|
build_report.assert_not_called()
|
||||||
acknowledge.assert_not_called()
|
react.assert_not_called()
|
||||||
reply.assert_not_called()
|
reply.assert_not_called()
|
||||||
|
|
||||||
def test_follow_up_continues_from_fifth_candidate_in_same_conversation(self) -> None:
|
def test_follow_up_continues_from_fifth_candidate_in_same_conversation(self) -> None:
|
||||||
@@ -219,8 +311,8 @@ class TriggerGateTests(unittest.TestCase):
|
|||||||
) as build_report,
|
) as build_report,
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
bridge,
|
bridge,
|
||||||
"reply_received_acknowledgement",
|
"add_received_reaction",
|
||||||
) as acknowledge,
|
) as react,
|
||||||
mock.patch.object(bridge, "reply") as reply,
|
mock.patch.object(bridge, "reply") as reply,
|
||||||
):
|
):
|
||||||
bridge.handle_message("client", initial_data)
|
bridge.handle_message("client", initial_data)
|
||||||
@@ -231,7 +323,7 @@ class TriggerGateTests(unittest.TestCase):
|
|||||||
[mock.call(request), mock.call(request, candidate_offset=4)],
|
[mock.call(request), mock.call(request, candidate_offset=4)],
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
acknowledge.call_args_list,
|
react.call_args_list,
|
||||||
[
|
[
|
||||||
mock.call("client", "om_initial"),
|
mock.call("client", "om_initial"),
|
||||||
mock.call("client", "om_follow_up"),
|
mock.call("client", "om_follow_up"),
|
||||||
@@ -258,8 +350,8 @@ class TriggerGateTests(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
bridge,
|
bridge,
|
||||||
"reply_received_acknowledgement",
|
"add_received_reaction",
|
||||||
) as acknowledge,
|
) as react,
|
||||||
mock.patch.object(bridge, "build_report", return_value="report") as build_report,
|
mock.patch.object(bridge, "build_report", return_value="report") as build_report,
|
||||||
mock.patch.object(bridge, "reply") as reply,
|
mock.patch.object(bridge, "reply") as reply,
|
||||||
):
|
):
|
||||||
@@ -269,7 +361,7 @@ class TriggerGateTests(unittest.TestCase):
|
|||||||
build_report.assert_called_once_with(
|
build_report.assert_called_once_with(
|
||||||
bridge.RequestInput(model="eRob70H50I-BHM-18CTC[V5]", quantity=10)
|
bridge.RequestInput(model="eRob70H50I-BHM-18CTC[V5]", quantity=10)
|
||||||
)
|
)
|
||||||
acknowledge.assert_called_once_with("client", "om_small")
|
react.assert_called_once_with("client", "om_small")
|
||||||
reply.assert_called_once_with("client", "om_small", "report")
|
reply.assert_called_once_with("client", "om_small", "report")
|
||||||
|
|
||||||
def test_expired_candidate_context_is_removed(self) -> None:
|
def test_expired_candidate_context_is_removed(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user