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:
2026-03-06 22:38:06 +00:00
parent ff66bc9e1f
commit add685a326
7 changed files with 90 additions and 26 deletions

View File

@@ -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
View 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 900000900999) |
| 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

View File

@@ -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

View File

@@ -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):

View File

@@ -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"))

View File

@@ -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()

View File

@@ -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