Fix Signal messages and replies
This commit is contained in:
54
core/tests/test_command_routing_variant_ui.py
Normal file
54
core/tests/test_command_routing_variant_ui.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from core.commands.policies import ensure_variant_policies_for_profile
|
||||
from core.models import CommandProfile, User
|
||||
|
||||
|
||||
class CommandRoutingVariantUITests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="routing-user",
|
||||
email="routing@example.com",
|
||||
password="x",
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
self.profile = CommandProfile.objects.create(
|
||||
user=self.user,
|
||||
slug="bp",
|
||||
name="Business Plan",
|
||||
enabled=True,
|
||||
trigger_token="#bp#",
|
||||
reply_required=True,
|
||||
exact_match_only=True,
|
||||
)
|
||||
ensure_variant_policies_for_profile(self.profile)
|
||||
|
||||
def test_command_routing_page_shows_variant_policy_table(self):
|
||||
response = self.client.get(reverse("command_routing"))
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertContains(response, "Variant Policies")
|
||||
self.assertContains(response, "bp set range")
|
||||
self.assertContains(response, "Send status to egress")
|
||||
|
||||
def test_variant_policy_update_persists(self):
|
||||
response = self.client.post(
|
||||
reverse("command_routing"),
|
||||
{
|
||||
"action": "variant_policy_update",
|
||||
"profile_id": str(self.profile.id),
|
||||
"variant_key": "bp_set",
|
||||
"enabled": "1",
|
||||
"generation_mode": "ai",
|
||||
"send_plan_to_egress": "1",
|
||||
"send_status_to_source": "1",
|
||||
"send_status_to_egress": "1",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
row = self.profile.variant_policies.get(variant_key="bp_set")
|
||||
self.assertEqual("ai", row.generation_mode)
|
||||
self.assertTrue(row.send_status_to_egress)
|
||||
225
core/tests/test_command_variant_policy.py
Normal file
225
core/tests/test_command_variant_policy.py
Normal file
@@ -0,0 +1,225 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from core.commands.base import CommandContext
|
||||
from core.commands.handlers.bp import BPCommandHandler
|
||||
from core.commands.policies import ensure_variant_policies_for_profile
|
||||
from core.models import (
|
||||
BusinessPlanDocument,
|
||||
AI,
|
||||
ChatSession,
|
||||
CommandAction,
|
||||
CommandChannelBinding,
|
||||
CommandProfile,
|
||||
CommandVariantPolicy,
|
||||
Message,
|
||||
Person,
|
||||
PersonIdentifier,
|
||||
User,
|
||||
)
|
||||
|
||||
|
||||
class CommandVariantPolicyTests(TransactionTestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="variant-user",
|
||||
email="variant@example.com",
|
||||
password="x",
|
||||
)
|
||||
self.person = Person.objects.create(user=self.user, name="Variant Person")
|
||||
self.identifier = PersonIdentifier.objects.create(
|
||||
user=self.user,
|
||||
person=self.person,
|
||||
service="whatsapp",
|
||||
identifier="120363402761690215",
|
||||
)
|
||||
self.session = ChatSession.objects.create(user=self.user, identifier=self.identifier)
|
||||
self.profile = CommandProfile.objects.create(
|
||||
user=self.user,
|
||||
slug="bp",
|
||||
name="Business Plan",
|
||||
enabled=True,
|
||||
trigger_token="#bp#",
|
||||
reply_required=True,
|
||||
exact_match_only=True,
|
||||
visibility_mode="status_in_source",
|
||||
template_text="TEMPLATE SHOULD NOT LEAK INTO bp set",
|
||||
)
|
||||
AI.objects.create(
|
||||
user=self.user,
|
||||
base_url="https://example.invalid",
|
||||
api_key="test-key",
|
||||
model="gpt-4o-mini",
|
||||
)
|
||||
CommandAction.objects.create(
|
||||
profile=self.profile,
|
||||
action_type="extract_bp",
|
||||
enabled=True,
|
||||
position=0,
|
||||
)
|
||||
CommandAction.objects.create(
|
||||
profile=self.profile,
|
||||
action_type="save_document",
|
||||
enabled=True,
|
||||
position=1,
|
||||
)
|
||||
CommandAction.objects.create(
|
||||
profile=self.profile,
|
||||
action_type="post_result",
|
||||
enabled=True,
|
||||
position=2,
|
||||
)
|
||||
CommandChannelBinding.objects.create(
|
||||
profile=self.profile,
|
||||
direction="ingress",
|
||||
service="whatsapp",
|
||||
channel_identifier="120363402761690215",
|
||||
enabled=True,
|
||||
)
|
||||
CommandChannelBinding.objects.create(
|
||||
profile=self.profile,
|
||||
direction="egress",
|
||||
service="whatsapp",
|
||||
channel_identifier="120363402761690215",
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
def _ctx(self, trigger: Message, text: str) -> CommandContext:
|
||||
return CommandContext(
|
||||
service="whatsapp",
|
||||
channel_identifier="120363402761690215",
|
||||
message_id=str(trigger.id),
|
||||
user_id=self.user.id,
|
||||
message_text=text,
|
||||
payload={},
|
||||
)
|
||||
|
||||
def test_ensure_variant_policies_backfills_bp_defaults(self):
|
||||
rows = ensure_variant_policies_for_profile(self.profile)
|
||||
self.assertSetEqual(set(rows.keys()), {"bp", "bp_set", "bp_set_range"})
|
||||
self.assertEqual("ai", rows["bp"].generation_mode)
|
||||
self.assertEqual("verbatim", rows["bp_set"].generation_mode)
|
||||
self.assertEqual("verbatim", rows["bp_set_range"].generation_mode)
|
||||
self.assertTrue(rows["bp"].send_plan_to_egress)
|
||||
self.assertTrue(rows["bp"].send_status_to_source)
|
||||
|
||||
def test_bp_primary_can_run_in_verbatim_mode_without_ai(self):
|
||||
ensure_variant_policies_for_profile(self.profile)
|
||||
policy = CommandVariantPolicy.objects.get(profile=self.profile, variant_key="bp")
|
||||
policy.generation_mode = "verbatim"
|
||||
policy.send_plan_to_egress = False
|
||||
policy.send_status_to_source = False
|
||||
policy.send_status_to_egress = False
|
||||
policy.save()
|
||||
|
||||
anchor = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text="anchor line",
|
||||
ts=1000,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215",
|
||||
)
|
||||
trigger = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="me",
|
||||
text="#bp#",
|
||||
ts=2000,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215",
|
||||
reply_to=anchor,
|
||||
)
|
||||
|
||||
result = async_to_sync(BPCommandHandler().execute)(self._ctx(trigger, "#bp#"))
|
||||
self.assertTrue(result.ok)
|
||||
doc = BusinessPlanDocument.objects.get(trigger_message=trigger)
|
||||
self.assertEqual("anchor line\n#bp#", doc.content_markdown)
|
||||
|
||||
def test_bp_set_ai_mode_ignores_template(self):
|
||||
ensure_variant_policies_for_profile(self.profile)
|
||||
policy = CommandVariantPolicy.objects.get(profile=self.profile, variant_key="bp_set")
|
||||
policy.generation_mode = "ai"
|
||||
policy.send_plan_to_egress = False
|
||||
policy.send_status_to_source = False
|
||||
policy.send_status_to_egress = False
|
||||
policy.save()
|
||||
|
||||
trigger = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="me",
|
||||
text="#bp set# text to transform",
|
||||
ts=1000,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215",
|
||||
)
|
||||
|
||||
with patch(
|
||||
"core.commands.handlers.bp.ai_runner.run_prompt",
|
||||
new=AsyncMock(return_value="AI RESULT"),
|
||||
) as mocked:
|
||||
result = async_to_sync(BPCommandHandler().execute)(
|
||||
self._ctx(trigger, trigger.text)
|
||||
)
|
||||
|
||||
self.assertTrue(result.ok)
|
||||
doc = BusinessPlanDocument.objects.get(trigger_message=trigger)
|
||||
self.assertEqual("AI RESULT", doc.content_markdown)
|
||||
call_args = mocked.await_args.args
|
||||
prompt_payload = call_args[0]
|
||||
self.assertNotIn("TEMPLATE SHOULD NOT LEAK", str(prompt_payload))
|
||||
|
||||
def test_delivery_flags_control_source_and_egress_status(self):
|
||||
ensure_variant_policies_for_profile(self.profile)
|
||||
policy = CommandVariantPolicy.objects.get(
|
||||
profile=self.profile,
|
||||
variant_key="bp_set_range",
|
||||
)
|
||||
policy.generation_mode = "verbatim"
|
||||
policy.store_document = False
|
||||
policy.send_plan_to_egress = False
|
||||
policy.send_status_to_source = True
|
||||
policy.send_status_to_egress = True
|
||||
policy.save()
|
||||
|
||||
anchor = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text="line one",
|
||||
ts=1000,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215",
|
||||
)
|
||||
trigger = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="me",
|
||||
text="#bp set range#",
|
||||
ts=2000,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215",
|
||||
reply_to=anchor,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"core.commands.handlers.bp.post_status_in_source",
|
||||
new=AsyncMock(return_value=True),
|
||||
) as source_status, patch(
|
||||
"core.commands.handlers.bp.post_to_channel_binding",
|
||||
new=AsyncMock(return_value=True),
|
||||
) as binding_send:
|
||||
result = async_to_sync(BPCommandHandler().execute)(
|
||||
self._ctx(trigger, trigger.text)
|
||||
)
|
||||
|
||||
self.assertTrue(result.ok)
|
||||
source_status.assert_awaited()
|
||||
self.assertEqual(1, binding_send.await_count)
|
||||
self.assertFalse(BusinessPlanDocument.objects.filter(trigger_message=trigger).exists())
|
||||
@@ -6,6 +6,7 @@ from django.test import TestCase
|
||||
from core.commands.base import CommandContext
|
||||
from core.commands.engine import _matches_trigger, process_inbound_message
|
||||
from core.messaging.reply_sync import extract_reply_ref, resolve_reply_target
|
||||
from core.views.compose import _command_options_for_channel
|
||||
from core.models import (
|
||||
ChatSession,
|
||||
CommandChannelBinding,
|
||||
@@ -123,6 +124,27 @@ class Phase1ReplyResolutionTests(TestCase):
|
||||
self.assertEqual("signal-msg-quoted", result.get("reply_source_message_id"))
|
||||
self.assertEqual("signal", result.get("reply_source_service"))
|
||||
|
||||
def test_extract_reply_ref_signal_target_sent_timestamp_variant(self):
|
||||
result = extract_reply_ref(
|
||||
"signal",
|
||||
{
|
||||
"envelope": {
|
||||
"dataMessage": {
|
||||
"quote": {
|
||||
"targetSentTimestamp": 1772545268786,
|
||||
"authorNumber": "+15550000001",
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
"1772545268786",
|
||||
result.get("reply_source_message_id"),
|
||||
)
|
||||
self.assertEqual("signal", result.get("reply_source_service"))
|
||||
self.assertEqual("+15550000001", result.get("reply_source_chat_id"))
|
||||
|
||||
def test_extract_reply_ref_whatsapp(self):
|
||||
result = extract_reply_ref(
|
||||
"whatsapp",
|
||||
@@ -272,3 +294,23 @@ class Phase1CommandEngineTests(TestCase):
|
||||
self.assertEqual(1, len(results))
|
||||
self.assertEqual("skipped", results[0].status)
|
||||
self.assertEqual("reply_required", results[0].error)
|
||||
|
||||
def test_compose_command_options_show_bp_subcommands(self):
|
||||
self.profile.channel_bindings.all().delete()
|
||||
CommandChannelBinding.objects.create(
|
||||
profile=self.profile,
|
||||
direction="ingress",
|
||||
service="whatsapp",
|
||||
channel_identifier="120363402761690215@g.us",
|
||||
enabled=True,
|
||||
)
|
||||
options = _command_options_for_channel(
|
||||
self.user,
|
||||
"whatsapp",
|
||||
"120363402761690215@g.us",
|
||||
)
|
||||
names = [str(row.get("name") or "").strip().lower() for row in options]
|
||||
self.assertIn("bp", names)
|
||||
self.assertIn("bp set", names)
|
||||
self.assertIn("bp set range", names)
|
||||
self.assertNotIn("announce task ids", names)
|
||||
|
||||
@@ -16,6 +16,7 @@ from core.models import (
|
||||
TaskProject,
|
||||
User,
|
||||
Message,
|
||||
Chat,
|
||||
)
|
||||
from core.tasks.engine import process_inbound_task_intelligence
|
||||
|
||||
@@ -136,3 +137,63 @@ class TaskEngineTests(TestCase):
|
||||
self.assertTrue(
|
||||
DerivedTaskEvent.objects.filter(task=task, event_type="completion_marked").exists()
|
||||
)
|
||||
|
||||
def test_matches_whatsapp_private_channel_variants(self):
|
||||
ChatTaskSource.objects.create(
|
||||
user=self.user,
|
||||
service="whatsapp",
|
||||
channel_identifier="447700900123@s.whatsapp.net",
|
||||
project=self.project,
|
||||
enabled=True,
|
||||
)
|
||||
m = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text="task: update private chat mapping",
|
||||
ts=1200,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="447700900123",
|
||||
)
|
||||
async_to_sync(process_inbound_task_intelligence)(m)
|
||||
self.assertTrue(
|
||||
DerivedTask.objects.filter(origin_message=m).exists(),
|
||||
"Expected private WhatsApp bare identifier to match @s.whatsapp.net mapping.",
|
||||
)
|
||||
|
||||
def test_matches_signal_uuid_to_number_companion_mapping(self):
|
||||
signal_person = Person.objects.create(user=self.user, name="Signal Task Person")
|
||||
signal_identifier = PersonIdentifier.objects.create(
|
||||
user=self.user,
|
||||
person=signal_person,
|
||||
service="signal",
|
||||
identifier="+447700900555",
|
||||
)
|
||||
signal_session = ChatSession.objects.create(user=self.user, identifier=signal_identifier)
|
||||
ChatTaskSource.objects.create(
|
||||
user=self.user,
|
||||
service="signal",
|
||||
channel_identifier="+447700900555",
|
||||
project=self.project,
|
||||
enabled=True,
|
||||
)
|
||||
Chat.objects.create(
|
||||
source_uuid="54cb8dbe-4c5f-4ef9-9f3d-4a9b37fd15d9",
|
||||
source_number="+447700900555",
|
||||
source_name="Signal Peer",
|
||||
account="+447700900000",
|
||||
)
|
||||
m = Message.objects.create(
|
||||
user=self.user,
|
||||
session=signal_session,
|
||||
sender_uuid="peer",
|
||||
text="task: check signal private mapping",
|
||||
ts=1300,
|
||||
source_service="signal",
|
||||
source_chat_id="54cb8dbe-4c5f-4ef9-9f3d-4a9b37fd15d9",
|
||||
)
|
||||
async_to_sync(process_inbound_task_intelligence)(m)
|
||||
self.assertTrue(
|
||||
DerivedTask.objects.filter(origin_message=m).exists(),
|
||||
"Expected Signal UUID source chat to match source mapping by companion number.",
|
||||
)
|
||||
|
||||
45
core/tests/test_signal_relink.py
Normal file
45
core/tests/test_signal_relink.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
from core.models import User
|
||||
|
||||
|
||||
class SignalRelinkTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_superuser(
|
||||
username="signal-admin",
|
||||
email="signal-admin@example.com",
|
||||
password="x",
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
@patch("core.views.signal.transport.list_accounts")
|
||||
def test_signal_accounts_view_shows_relink_action(self, mock_list_accounts):
|
||||
mock_list_accounts.return_value = ["+447000000001"]
|
||||
response = self.client.get(reverse("signal_accounts", kwargs={"type": "page"}))
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertContains(response, "Relink")
|
||||
self.assertContains(response, "/services/signal/")
|
||||
self.assertContains(response, "/unlink/+447000000001/")
|
||||
|
||||
@patch("core.views.signal.transport.list_accounts")
|
||||
@patch("core.views.signal.transport.unlink_account")
|
||||
def test_signal_account_unlink_calls_transport_and_renders_panel(
|
||||
self,
|
||||
mock_unlink_account,
|
||||
mock_list_accounts,
|
||||
):
|
||||
mock_list_accounts.side_effect = [
|
||||
["+447000000001"],
|
||||
[],
|
||||
]
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"signal_account_unlink",
|
||||
kwargs={"type": "page", "account": "+447000000001"},
|
||||
)
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
mock_unlink_account.assert_called_once_with("signal", "+447000000001")
|
||||
223
core/tests/test_signal_reply_send.py
Normal file
223
core/tests/test_signal_reply_send.py
Normal file
@@ -0,0 +1,223 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import Mock
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.conf import settings
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
|
||||
from core.clients import transport
|
||||
from core.clients.signal import SignalClient
|
||||
from core.models import ChatSession, Message, Person, PersonIdentifier, User
|
||||
from core.views.compose import _build_signal_reply_metadata
|
||||
|
||||
|
||||
class SignalReplyMetadataTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="signal-reply-meta-user",
|
||||
email="signal-reply-meta@example.com",
|
||||
password="x",
|
||||
)
|
||||
self.person = Person.objects.create(user=self.user, name="Signal Reply")
|
||||
self.identifier = PersonIdentifier.objects.create(
|
||||
user=self.user,
|
||||
person=self.person,
|
||||
service="signal",
|
||||
identifier="+15550001000",
|
||||
)
|
||||
self.session = ChatSession.objects.create(
|
||||
user=self.user,
|
||||
identifier=self.identifier,
|
||||
)
|
||||
|
||||
def test_build_signal_reply_metadata_uses_signal_source(self):
|
||||
incoming = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="+15550001000",
|
||||
text="quoted body",
|
||||
ts=1772538353497,
|
||||
source_service="signal",
|
||||
source_message_id="1772538353497",
|
||||
source_chat_id="+15550001000",
|
||||
)
|
||||
payload = _build_signal_reply_metadata(incoming, "+15550001000")
|
||||
self.assertEqual(1772538353497, payload.get("quote_timestamp"))
|
||||
self.assertEqual("+15550001000", payload.get("quote_author"))
|
||||
self.assertEqual("quoted body", payload.get("quote_text"))
|
||||
|
||||
def test_build_signal_reply_metadata_uses_chat_number_when_sender_is_uuid(self):
|
||||
incoming = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="756078fd-d447-426d-a620-581a86d64f51",
|
||||
text="quoted body",
|
||||
ts=1772538353497,
|
||||
source_service="signal",
|
||||
source_message_id="1772538353497",
|
||||
source_chat_id="+15550001000",
|
||||
)
|
||||
payload = _build_signal_reply_metadata(incoming, "+15550001000")
|
||||
self.assertEqual("+15550001000", payload.get("quote_author"))
|
||||
|
||||
def test_build_signal_reply_metadata_uses_local_sender_for_own_messages(self):
|
||||
outgoing = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="",
|
||||
custom_author="USER",
|
||||
text="my previous message",
|
||||
ts=1772538353900,
|
||||
source_service="web",
|
||||
source_message_id="1772538353900",
|
||||
source_chat_id="+15550001000",
|
||||
)
|
||||
payload = _build_signal_reply_metadata(outgoing, "+15550001000")
|
||||
expected_author = str(getattr(settings, "SIGNAL_NUMBER", "") or "").strip()
|
||||
if expected_author:
|
||||
self.assertEqual(expected_author, payload.get("quote_author"))
|
||||
else:
|
||||
self.assertEqual("+15550001000", payload.get("quote_author"))
|
||||
|
||||
|
||||
class SignalTransportSendTests(TestCase):
|
||||
def test_transport_passes_reply_metadata_to_signal_api(self):
|
||||
with patch(
|
||||
"core.clients.transport.prepare_outbound_attachments",
|
||||
new=AsyncMock(return_value=[]),
|
||||
), patch(
|
||||
"core.clients.transport.signalapi.send_message_raw",
|
||||
new=AsyncMock(return_value=1772538354000),
|
||||
) as mocked_send:
|
||||
result = async_to_sync(transport.send_message_raw)(
|
||||
"signal",
|
||||
"+15550001000",
|
||||
text="reply payload",
|
||||
attachments=[],
|
||||
metadata={
|
||||
"quote_timestamp": 1772538353497,
|
||||
"quote_author": "+15550001000",
|
||||
"quote_text": "quoted body",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(1772538354000, result)
|
||||
mocked_send.assert_awaited_once_with(
|
||||
"+15550001000",
|
||||
"reply payload",
|
||||
[],
|
||||
metadata={
|
||||
"quote_timestamp": 1772538353497,
|
||||
"quote_author": "+15550001000",
|
||||
"quote_text": "quoted body",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class SignalInboundReplyLinkTests(TransactionTestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="signal-inbound-user",
|
||||
email="signal-inbound@example.com",
|
||||
password="x",
|
||||
)
|
||||
self.person = Person.objects.create(user=self.user, name="Signal Inbound")
|
||||
self.identifier = PersonIdentifier.objects.create(
|
||||
user=self.user,
|
||||
person=self.person,
|
||||
service="signal",
|
||||
identifier="+15550002000",
|
||||
)
|
||||
self.session = ChatSession.objects.create(
|
||||
user=self.user,
|
||||
identifier=self.identifier,
|
||||
)
|
||||
self.anchor = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="+15550002000",
|
||||
text="anchor inbound",
|
||||
ts=1772545458187,
|
||||
source_service="signal",
|
||||
source_message_id="1772545458187",
|
||||
source_chat_id="+15550002000",
|
||||
)
|
||||
|
||||
def test_process_raw_inbound_event_links_signal_reply(self):
|
||||
fake_ur = Mock()
|
||||
fake_ur.message_received = AsyncMock(return_value=None)
|
||||
client = SignalClient.__new__(SignalClient)
|
||||
client.service = "signal"
|
||||
client.ur = fake_ur
|
||||
client.log = Mock()
|
||||
client.client = Mock()
|
||||
client.client.bot_uuid = ""
|
||||
client.client.phone_number = ""
|
||||
client._resolve_signal_identifiers = AsyncMock(return_value=[self.identifier])
|
||||
client._auto_link_single_user_signal_identifier = AsyncMock(return_value=[])
|
||||
|
||||
payload = {
|
||||
"envelope": {
|
||||
"sourceNumber": "+15550002000",
|
||||
"sourceUuid": "756078fd-d447-426d-a620-581a86d64f51",
|
||||
"timestamp": 1772545462051,
|
||||
"dataMessage": {
|
||||
"message": "reply inbound s3",
|
||||
"quote": {
|
||||
"targetSentTimestamp": 1772545458187,
|
||||
"authorNumber": "+15550002000",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
async_to_sync(client._process_raw_inbound_event)(json.dumps(payload))
|
||||
|
||||
created = Message.objects.filter(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
text="reply inbound s3",
|
||||
).order_by("-ts").first()
|
||||
self.assertIsNotNone(created)
|
||||
self.assertEqual(self.anchor.id, created.reply_to_id)
|
||||
self.assertEqual("1772545458187", created.reply_source_message_id)
|
||||
|
||||
def test_process_raw_inbound_event_applies_reaction(self):
|
||||
fake_ur = Mock()
|
||||
fake_ur.message_received = AsyncMock(return_value=None)
|
||||
fake_ur.xmpp = Mock()
|
||||
fake_ur.xmpp.client = Mock()
|
||||
fake_ur.xmpp.client.apply_external_reaction = AsyncMock(return_value=None)
|
||||
client = SignalClient.__new__(SignalClient)
|
||||
client.service = "signal"
|
||||
client.ur = fake_ur
|
||||
client.log = Mock()
|
||||
client.client = Mock()
|
||||
client.client.bot_uuid = ""
|
||||
client.client.phone_number = ""
|
||||
client._resolve_signal_identifiers = AsyncMock(return_value=[self.identifier])
|
||||
client._auto_link_single_user_signal_identifier = AsyncMock(return_value=[])
|
||||
|
||||
payload = {
|
||||
"envelope": {
|
||||
"sourceNumber": "+15550002000",
|
||||
"sourceUuid": "756078fd-d447-426d-a620-581a86d64f51",
|
||||
"timestamp": 1772545463000,
|
||||
"dataMessage": {
|
||||
"reaction": {
|
||||
"emoji": "❤️",
|
||||
"targetSentTimestamp": 1772545458187,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
async_to_sync(client._process_raw_inbound_event)(json.dumps(payload))
|
||||
|
||||
self.anchor.refresh_from_db()
|
||||
reactions = list((self.anchor.receipt_payload or {}).get("reactions") or [])
|
||||
self.assertTrue(
|
||||
any(str(row.get("emoji") or "") == "❤️" for row in reactions),
|
||||
"Expected Signal heart reaction to be applied to anchor receipt payload.",
|
||||
)
|
||||
43
core/tests/test_signal_unlink_fallback.py
Normal file
43
core/tests/test_signal_unlink_fallback.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from core.clients import transport
|
||||
|
||||
|
||||
class SignalUnlinkFallbackTests(TestCase):
|
||||
@patch("core.clients.transport._wipe_signal_cli_local_state")
|
||||
@patch("requests.delete")
|
||||
def test_signal_unlink_uses_rest_delete_when_available(
|
||||
self,
|
||||
mock_delete,
|
||||
mock_wipe,
|
||||
):
|
||||
ok_response = Mock()
|
||||
ok_response.ok = True
|
||||
mock_delete.return_value = ok_response
|
||||
|
||||
result = transport.unlink_account("signal", "+447700900000")
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(mock_delete.called)
|
||||
mock_wipe.assert_not_called()
|
||||
|
||||
@patch("core.clients.transport._wipe_signal_cli_local_state")
|
||||
@patch("requests.delete")
|
||||
def test_signal_unlink_falls_back_to_local_wipe(
|
||||
self,
|
||||
mock_delete,
|
||||
mock_wipe,
|
||||
):
|
||||
bad_response = Mock()
|
||||
bad_response.ok = False
|
||||
mock_delete.return_value = bad_response
|
||||
mock_wipe.return_value = True
|
||||
|
||||
result = transport.unlink_account("signal", "+447700900000")
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(2, mock_delete.call_count)
|
||||
mock_wipe.assert_called_once()
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from core.models import (
|
||||
@@ -12,12 +13,13 @@ from core.models import (
|
||||
Message,
|
||||
Person,
|
||||
PersonIdentifier,
|
||||
TaskCompletionPattern,
|
||||
TaskProject,
|
||||
User,
|
||||
)
|
||||
from core.tasks.engine import process_inbound_task_intelligence
|
||||
from core.views.compose import _command_options_for_channel, _toggle_task_announce_for_channel
|
||||
from core.views.tasks import _apply_safe_defaults_for_user
|
||||
from core.views.tasks import _apply_safe_defaults_for_user, _ensure_default_completion_patterns
|
||||
|
||||
|
||||
class TaskSettingsBackfillTests(TestCase):
|
||||
@@ -65,6 +67,13 @@ class TaskSettingsBackfillTests(TestCase):
|
||||
self.assertEqual("strict", self.source.settings.get("match_mode"))
|
||||
self.assertTrue(bool(self.source.settings.get("require_prefix")))
|
||||
|
||||
def test_default_completion_phrases_seeded(self):
|
||||
_ensure_default_completion_patterns(self.user)
|
||||
phrases = set(
|
||||
TaskCompletionPattern.objects.filter(user=self.user).values_list("phrase", flat=True)
|
||||
)
|
||||
self.assertTrue({"done", "completed", "fixed"}.issubset(phrases))
|
||||
|
||||
|
||||
class TaskAnnounceToggleTests(TestCase):
|
||||
def setUp(self):
|
||||
@@ -98,14 +107,16 @@ class TaskAnnounceToggleTests(TestCase):
|
||||
self.source.refresh_from_db()
|
||||
self.assertTrue(bool(self.source.settings.get("announce_task_id")))
|
||||
|
||||
def test_command_options_include_task_announce_state(self):
|
||||
def test_command_options_include_bp_subcommands(self):
|
||||
options = _command_options_for_channel(
|
||||
self.user,
|
||||
"whatsapp",
|
||||
"120363402761690215",
|
||||
)
|
||||
row = [opt for opt in options if opt.get("slug") == "task_announce"][0]
|
||||
self.assertFalse(bool(row.get("enabled_here")))
|
||||
names = [str(row.get("name") or "").strip().lower() for row in options]
|
||||
self.assertIn("bp", names)
|
||||
self.assertIn("bp set", names)
|
||||
self.assertIn("bp set range", names)
|
||||
|
||||
|
||||
@override_settings(TASK_DERIVATION_USE_AI=False)
|
||||
@@ -161,3 +172,34 @@ class TaskAnnounceRuntimeTests(TestCase):
|
||||
async_to_sync(process_inbound_task_intelligence)(self._msg("task: rotate secrets"))
|
||||
self.assertTrue(DerivedTask.objects.exists())
|
||||
mocked_send.assert_awaited()
|
||||
|
||||
|
||||
class TaskSettingsViewActionsTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user("task-settings-user", "ts@example.com", "x")
|
||||
self.client.force_login(self.user)
|
||||
self.project = TaskProject.objects.create(user=self.user, name="Project A")
|
||||
self.source = ChatTaskSource.objects.create(
|
||||
user=self.user,
|
||||
service="whatsapp",
|
||||
channel_identifier="120363402761690215@g.us",
|
||||
project=self.project,
|
||||
settings={"match_mode": "strict"},
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
def test_source_delete_removes_mapping(self):
|
||||
response = self.client.post(
|
||||
reverse("tasks_settings"),
|
||||
{
|
||||
"action": "source_delete",
|
||||
"source_id": str(self.source.id),
|
||||
"prefill_service": "whatsapp",
|
||||
"prefill_identifier": "120363402761690215@g.us",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertFalse(
|
||||
ChatTaskSource.objects.filter(id=self.source.id, user=self.user).exists()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user