Remove real contact numbers from tests and update tooling
- Replace real phone numbers in tests with Ofcom-reserved fictitious numbers (447700900xxx range) throughout test suite - Add SIGNAL_NUMBER to stack.env.example documenting required env var - Update pre-commit hooks to latest versions (black 26.3.0, isort 8.0.1, flake8 7.3.0, djhtml 3.0.10, ripsecrets v0.1.11) - Add CLAUDE.md with rule prohibiting real contact identifiers in code Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,22 +1,22 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.1.0
|
rev: 26.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
exclude: ^core/migrations
|
exclude: ^core/migrations
|
||||||
- repo: https://github.com/PyCQA/isort
|
- repo: https://github.com/PyCQA/isort
|
||||||
rev: 5.11.5
|
rev: 8.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
args: ["--profile", "black"]
|
args: ["--profile", "black"]
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 6.0.0
|
rev: 7.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
args: [--max-line-length=88]
|
args: [--max-line-length=88]
|
||||||
exclude: ^core/migrations
|
exclude: ^core/migrations
|
||||||
- repo: https://github.com/rtts/djhtml
|
- repo: https://github.com/rtts/djhtml
|
||||||
rev: v2.0.0
|
rev: 3.0.10
|
||||||
hooks:
|
hooks:
|
||||||
- id: djhtml
|
- id: djhtml
|
||||||
args: [-t 2]
|
args: [-t 2]
|
||||||
@@ -25,6 +25,6 @@ repos:
|
|||||||
- id: djjs
|
- id: djjs
|
||||||
exclude: ^core/static/js # slow
|
exclude: ^core/static/js # slow
|
||||||
- repo: https://github.com/sirwart/ripsecrets.git
|
- repo: https://github.com/sirwart/ripsecrets.git
|
||||||
rev: v0.1.5
|
rev: v0.1.11
|
||||||
hooks:
|
hooks:
|
||||||
- id: ripsecrets
|
- id: ripsecrets
|
||||||
|
|||||||
27
CLAUDE.md
Normal file
27
CLAUDE.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# GIA — Claude Code Rules
|
||||||
|
|
||||||
|
## Privacy: No Real Contact Data in Code
|
||||||
|
|
||||||
|
**NEVER use real contact identifiers in tests, fixtures, seeds, or any committed file.**
|
||||||
|
|
||||||
|
Real contact data includes: phone numbers, JIDs, email addresses, usernames, or any identifier belonging to an actual person in the user's contacts.
|
||||||
|
|
||||||
|
### Use fictitious data instead
|
||||||
|
|
||||||
|
| Type | Safe fictitious examples |
|
||||||
|
|---|---|
|
||||||
|
| UK mobile (E.164) | `+447700900001`, `+447700900002` (Ofcom-reserved range 07700 900000–900999) |
|
||||||
|
| UK mobile (no +) | `447700900001`, `447700900002` |
|
||||||
|
| US phone | `+15550001234`, `+15550009999` (555-0xxx NANP reserved range) |
|
||||||
|
| Email | `test@example.com`, `user@example.invalid` |
|
||||||
|
| WhatsApp JID | `447700900001@s.whatsapp.net`, `447700900001@g.us` |
|
||||||
|
|
||||||
|
### Why this matters
|
||||||
|
|
||||||
|
AI coding tools (Copilot, Claude) will reuse any values they see in context. A real number placed in a test becomes training signal and will be suggested in future completions — potentially leaking it further.
|
||||||
|
|
||||||
|
### Quick check
|
||||||
|
|
||||||
|
Before committing test files, verify no identifier matches a real person:
|
||||||
|
- No number outside the reserved fictitious ranges above
|
||||||
|
- No name that corresponds to a real contact used as a literal identifier
|
||||||
@@ -3,7 +3,11 @@ from __future__ import annotations
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from core.models import Person, PersonIdentifier, User
|
from core.models import Person, PersonIdentifier, User
|
||||||
from core.presence import AvailabilitySignal, latest_state_for_people, record_native_signal
|
from core.presence import (
|
||||||
|
AvailabilitySignal,
|
||||||
|
latest_state_for_people,
|
||||||
|
record_native_signal,
|
||||||
|
)
|
||||||
from core.presence.inference import now_ms
|
from core.presence.inference import now_ms
|
||||||
from core.views.compose import _compose_availability_payload, _context_base
|
from core.views.compose import _compose_availability_payload, _context_base
|
||||||
|
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ from django.test import TestCase, override_settings
|
|||||||
from core.assist.repeat_answer import find_repeat_answer, learn_from_message
|
from core.assist.repeat_answer import find_repeat_answer, learn_from_message
|
||||||
from core.models import (
|
from core.models import (
|
||||||
AnswerSuggestionEvent,
|
AnswerSuggestionEvent,
|
||||||
|
Chat,
|
||||||
ChatSession,
|
ChatSession,
|
||||||
ChatTaskSource,
|
ChatTaskSource,
|
||||||
DerivedTask,
|
DerivedTask,
|
||||||
DerivedTaskEvent,
|
DerivedTaskEvent,
|
||||||
|
Message,
|
||||||
Person,
|
Person,
|
||||||
PersonIdentifier,
|
PersonIdentifier,
|
||||||
TaskCompletionPattern,
|
TaskCompletionPattern,
|
||||||
TaskEpic,
|
TaskEpic,
|
||||||
TaskProject,
|
TaskProject,
|
||||||
User,
|
User,
|
||||||
Message,
|
|
||||||
Chat,
|
|
||||||
)
|
)
|
||||||
from core.tasks.engine import process_inbound_task_intelligence
|
from core.tasks.engine import process_inbound_task_intelligence
|
||||||
|
|
||||||
@@ -34,7 +34,9 @@ class RepeatAnswerTests(TestCase):
|
|||||||
service="whatsapp",
|
service="whatsapp",
|
||||||
identifier="120363402761690215@g.us",
|
identifier="120363402761690215@g.us",
|
||||||
)
|
)
|
||||||
self.session = ChatSession.objects.create(user=self.user, identifier=self.identifier)
|
self.session = ChatSession.objects.create(
|
||||||
|
user=self.user, identifier=self.identifier
|
||||||
|
)
|
||||||
|
|
||||||
def test_suggest_only_for_repeated_group_question(self):
|
def test_suggest_only_for_repeated_group_question(self):
|
||||||
q1 = Message.objects.create(
|
q1 = Message.objects.create(
|
||||||
@@ -71,7 +73,9 @@ class RepeatAnswerTests(TestCase):
|
|||||||
self.assertIsNotNone(suggestion)
|
self.assertIsNotNone(suggestion)
|
||||||
self.assertIn("deploy", suggestion.answer_text.lower())
|
self.assertIn("deploy", suggestion.answer_text.lower())
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
AnswerSuggestionEvent.objects.filter(message=q2, status="suggested").exists()
|
AnswerSuggestionEvent.objects.filter(
|
||||||
|
message=q2, status="suggested"
|
||||||
|
).exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -86,7 +90,9 @@ class TaskEngineTests(TestCase):
|
|||||||
service="whatsapp",
|
service="whatsapp",
|
||||||
identifier="120363402761690215@g.us",
|
identifier="120363402761690215@g.us",
|
||||||
)
|
)
|
||||||
self.session = ChatSession.objects.create(user=self.user, identifier=self.identifier)
|
self.session = ChatSession.objects.create(
|
||||||
|
user=self.user, identifier=self.identifier
|
||||||
|
)
|
||||||
self.project = TaskProject.objects.create(user=self.user, name="Ops")
|
self.project = TaskProject.objects.create(user=self.user, name="Ops")
|
||||||
ChatTaskSource.objects.create(
|
ChatTaskSource.objects.create(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
@@ -95,7 +101,9 @@ class TaskEngineTests(TestCase):
|
|||||||
project=self.project,
|
project=self.project,
|
||||||
enabled=True,
|
enabled=True,
|
||||||
)
|
)
|
||||||
TaskCompletionPattern.objects.create(user=self.user, phrase="done", enabled=True)
|
TaskCompletionPattern.objects.create(
|
||||||
|
user=self.user, phrase="done", enabled=True
|
||||||
|
)
|
||||||
|
|
||||||
def test_creates_derived_task_on_task_like_message(self):
|
def test_creates_derived_task_on_task_like_message(self):
|
||||||
m = Message.objects.create(
|
m = Message.objects.create(
|
||||||
@@ -111,7 +119,9 @@ class TaskEngineTests(TestCase):
|
|||||||
task = DerivedTask.objects.get(origin_message=m)
|
task = DerivedTask.objects.get(origin_message=m)
|
||||||
self.assertEqual("open", task.status_snapshot)
|
self.assertEqual("open", task.status_snapshot)
|
||||||
self.assertTrue(task.reference_code)
|
self.assertTrue(task.reference_code)
|
||||||
self.assertTrue(DerivedTaskEvent.objects.filter(task=task, event_type="created").exists())
|
self.assertTrue(
|
||||||
|
DerivedTaskEvent.objects.filter(task=task, event_type="created").exists()
|
||||||
|
)
|
||||||
|
|
||||||
def test_marks_completion_from_regex_marker(self):
|
def test_marks_completion_from_regex_marker(self):
|
||||||
seed = Message.objects.create(
|
seed = Message.objects.create(
|
||||||
@@ -138,7 +148,9 @@ class TaskEngineTests(TestCase):
|
|||||||
task.refresh_from_db()
|
task.refresh_from_db()
|
||||||
self.assertEqual("completed", task.status_snapshot)
|
self.assertEqual("completed", task.status_snapshot)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
DerivedTaskEvent.objects.filter(task=task, event_type="completion_marked").exists()
|
DerivedTaskEvent.objects.filter(
|
||||||
|
task=task, event_type="completion_marked"
|
||||||
|
).exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_matches_whatsapp_private_channel_variants(self):
|
def test_matches_whatsapp_private_channel_variants(self):
|
||||||
@@ -172,7 +184,9 @@ class TaskEngineTests(TestCase):
|
|||||||
service="signal",
|
service="signal",
|
||||||
identifier="+447700900555",
|
identifier="+447700900555",
|
||||||
)
|
)
|
||||||
signal_session = ChatSession.objects.create(user=self.user, identifier=signal_identifier)
|
signal_session = ChatSession.objects.create(
|
||||||
|
user=self.user, identifier=signal_identifier
|
||||||
|
)
|
||||||
ChatTaskSource.objects.create(
|
ChatTaskSource.objects.create(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
service="signal",
|
service="signal",
|
||||||
@@ -308,7 +322,9 @@ class TaskEngineTests(TestCase):
|
|||||||
)
|
)
|
||||||
async_to_sync(process_inbound_task_intelligence)(cmd)
|
async_to_sync(process_inbound_task_intelligence)(cmd)
|
||||||
self.assertTrue(mocked_send.await_count >= 1)
|
self.assertTrue(mocked_send.await_count >= 1)
|
||||||
list_payloads = [str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list]
|
list_payloads = [
|
||||||
|
str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list
|
||||||
|
]
|
||||||
self.assertTrue(any("open tasks" in row.lower() for row in list_payloads))
|
self.assertTrue(any("open tasks" in row.lower() for row in list_payloads))
|
||||||
self.assertTrue(any("#1" in row for row in list_payloads))
|
self.assertTrue(any("#1" in row for row in list_payloads))
|
||||||
|
|
||||||
@@ -334,7 +350,9 @@ class TaskEngineTests(TestCase):
|
|||||||
)
|
)
|
||||||
async_to_sync(process_inbound_task_intelligence)(m1)
|
async_to_sync(process_inbound_task_intelligence)(m1)
|
||||||
async_to_sync(process_inbound_task_intelligence)(m2)
|
async_to_sync(process_inbound_task_intelligence)(m2)
|
||||||
self.assertEqual(2, DerivedTask.objects.filter(user=self.user, project=self.project).count())
|
self.assertEqual(
|
||||||
|
2, DerivedTask.objects.filter(user=self.user, project=self.project).count()
|
||||||
|
)
|
||||||
cmd = Message.objects.create(
|
cmd = Message.objects.create(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
session=self.session,
|
session=self.session,
|
||||||
@@ -352,7 +370,9 @@ class TaskEngineTests(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(1, len(remaining))
|
self.assertEqual(1, len(remaining))
|
||||||
self.assertEqual("one", remaining[0])
|
self.assertEqual("one", remaining[0])
|
||||||
payloads = [str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list]
|
payloads = [
|
||||||
|
str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list
|
||||||
|
]
|
||||||
self.assertTrue(any("removed #2" in row.lower() for row in payloads))
|
self.assertTrue(any("removed #2" in row.lower() for row in payloads))
|
||||||
|
|
||||||
@patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock)
|
@patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock)
|
||||||
@@ -368,9 +388,14 @@ class TaskEngineTests(TestCase):
|
|||||||
source_chat_id="120363402761690215@g.us",
|
source_chat_id="120363402761690215@g.us",
|
||||||
)
|
)
|
||||||
async_to_sync(process_inbound_task_intelligence)(m)
|
async_to_sync(process_inbound_task_intelligence)(m)
|
||||||
payloads = [str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list]
|
payloads = [
|
||||||
|
str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list
|
||||||
|
]
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
any(".l list tasks" in row.lower() and ".undo" in row.lower() for row in payloads),
|
any(
|
||||||
|
".l list tasks" in row.lower() and ".undo" in row.lower()
|
||||||
|
for row in payloads
|
||||||
|
),
|
||||||
"Expected periodic reminder to mention both .l and .undo.",
|
"Expected periodic reminder to mention both .l and .undo.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -386,8 +411,12 @@ class TaskEngineTests(TestCase):
|
|||||||
source_chat_id="120363402761690215@g.us",
|
source_chat_id="120363402761690215@g.us",
|
||||||
)
|
)
|
||||||
async_to_sync(process_inbound_task_intelligence)(msg)
|
async_to_sync(process_inbound_task_intelligence)(msg)
|
||||||
self.assertTrue(TaskEpic.objects.filter(project=self.project, name="Security").exists())
|
self.assertTrue(
|
||||||
payloads = [str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list]
|
TaskEpic.objects.filter(project=self.project, name="Security").exists()
|
||||||
|
)
|
||||||
|
payloads = [
|
||||||
|
str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list
|
||||||
|
]
|
||||||
self.assertTrue(any("whatsapp usage" in row.lower() for row in payloads))
|
self.assertTrue(any("whatsapp usage" in row.lower() for row in payloads))
|
||||||
|
|
||||||
def test_task_with_epic_token_assigns_epic(self):
|
def test_task_with_epic_token_assigns_epic(self):
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from core.clients import signalapi
|
from core.clients import signalapi
|
||||||
|
|
||||||
@@ -43,7 +44,9 @@ class _FakeClientSession:
|
|||||||
|
|
||||||
class SignalSendNormalizationTests(TestCase):
|
class SignalSendNormalizationTests(TestCase):
|
||||||
def test_normalize_signal_recipient_phone_and_uuid(self):
|
def test_normalize_signal_recipient_phone_and_uuid(self):
|
||||||
self.assertEqual("+447700900000", signalapi.normalize_signal_recipient("447700900000"))
|
self.assertEqual(
|
||||||
|
"+447700900000", signalapi.normalize_signal_recipient("447700900000")
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"+447700900000", signalapi.normalize_signal_recipient("+44 7700-900000")
|
"+447700900000", signalapi.normalize_signal_recipient("+44 7700-900000")
|
||||||
)
|
)
|
||||||
@@ -71,4 +74,3 @@ class SignalSendNormalizationTests(TestCase):
|
|||||||
self.assertGreaterEqual(len(_FakeClientSession.posted_payloads), 1)
|
self.assertGreaterEqual(len(_FakeClientSession.posted_payloads), 1)
|
||||||
first_payload = _FakeClientSession.posted_payloads[0]
|
first_payload = _FakeClientSession.posted_payloads[0]
|
||||||
self.assertEqual(["+447700900000"], first_payload.get("recipients"))
|
self.assertEqual(["+447700900000"], first_payload.get("recipients"))
|
||||||
|
|
||||||
|
|||||||
@@ -40,4 +40,3 @@ class SignalUnlinkFallbackTests(TestCase):
|
|||||||
self.assertTrue(result)
|
self.assertTrue(result)
|
||||||
self.assertEqual(2, mock_delete.call_count)
|
self.assertEqual(2, mock_delete.call_count)
|
||||||
mock_wipe.assert_called_once()
|
mock_wipe.assert_called_once()
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ QUADLET_PROSODY_CERTS_DIR=./.podman/gia_prosody_certs
|
|||||||
QUADLET_PROSODY_DATA_DIR=./.podman/gia_prosody_data
|
QUADLET_PROSODY_DATA_DIR=./.podman/gia_prosody_data
|
||||||
QUADLET_PROSODY_LOGS_DIR=./.podman/gia_prosody_logs
|
QUADLET_PROSODY_LOGS_DIR=./.podman/gia_prosody_logs
|
||||||
|
|
||||||
|
# Signal CLI account number (E.164 format, e.g. +447700900000). Required for Signal integration.
|
||||||
|
SIGNAL_NUMBER=
|
||||||
|
|
||||||
# Memory/wiki search backend foundation
|
# Memory/wiki search backend foundation
|
||||||
MEMORY_SEARCH_BACKEND=django
|
MEMORY_SEARCH_BACKEND=django
|
||||||
MANTICORE_HTTP_URL=http://localhost:9308
|
MANTICORE_HTTP_URL=http://localhost:9308
|
||||||
|
|||||||
Reference in New Issue
Block a user