From add685a326b9fde2949435b05772adfc88678b7f Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Fri, 6 Mar 2026 22:38:06 +0000 Subject: [PATCH] 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 --- .pre-commit-config.yaml | 10 +-- CLAUDE.md | 27 ++++++++ ...test_presence_query_and_compose_context.py | 6 +- core/tests/test_repeat_answer_and_tasks.py | 61 ++++++++++++++----- core/tests/test_signal_send_normalization.py | 8 ++- core/tests/test_signal_unlink_fallback.py | 1 - stack.env.example | 3 + 7 files changed, 90 insertions(+), 26 deletions(-) create mode 100644 CLAUDE.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23c9d5b..a684f10 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,22 +1,22 @@ repos: - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 26.3.0 hooks: - id: black exclude: ^core/migrations - repo: https://github.com/PyCQA/isort - rev: 5.11.5 + rev: 8.0.1 hooks: - id: isort args: ["--profile", "black"] - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 7.3.0 hooks: - id: flake8 args: [--max-line-length=88] exclude: ^core/migrations - repo: https://github.com/rtts/djhtml - rev: v2.0.0 + rev: 3.0.10 hooks: - id: djhtml args: [-t 2] @@ -25,6 +25,6 @@ repos: - id: djjs exclude: ^core/static/js # slow - repo: https://github.com/sirwart/ripsecrets.git - rev: v0.1.5 + rev: v0.1.11 hooks: - id: ripsecrets diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d4dfbc3 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/core/tests/test_presence_query_and_compose_context.py b/core/tests/test_presence_query_and_compose_context.py index 5f2bd77..5fd9bf6 100644 --- a/core/tests/test_presence_query_and_compose_context.py +++ b/core/tests/test_presence_query_and_compose_context.py @@ -3,7 +3,11 @@ from __future__ import annotations from django.test import TestCase 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.views.compose import _compose_availability_payload, _context_base diff --git a/core/tests/test_repeat_answer_and_tasks.py b/core/tests/test_repeat_answer_and_tasks.py index bb025aa..7ae3253 100644 --- a/core/tests/test_repeat_answer_and_tasks.py +++ b/core/tests/test_repeat_answer_and_tasks.py @@ -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.models import ( AnswerSuggestionEvent, + Chat, ChatSession, ChatTaskSource, DerivedTask, DerivedTaskEvent, + Message, Person, PersonIdentifier, TaskCompletionPattern, TaskEpic, TaskProject, User, - Message, - Chat, ) from core.tasks.engine import process_inbound_task_intelligence @@ -34,7 +34,9 @@ class RepeatAnswerTests(TestCase): service="whatsapp", 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): q1 = Message.objects.create( @@ -71,7 +73,9 @@ class RepeatAnswerTests(TestCase): self.assertIsNotNone(suggestion) self.assertIn("deploy", suggestion.answer_text.lower()) 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", 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") ChatTaskSource.objects.create( user=self.user, @@ -95,7 +101,9 @@ class TaskEngineTests(TestCase): project=self.project, 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): m = Message.objects.create( @@ -111,7 +119,9 @@ class TaskEngineTests(TestCase): task = DerivedTask.objects.get(origin_message=m) self.assertEqual("open", task.status_snapshot) 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): seed = Message.objects.create( @@ -138,7 +148,9 @@ class TaskEngineTests(TestCase): task.refresh_from_db() self.assertEqual("completed", task.status_snapshot) 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): @@ -172,7 +184,9 @@ class TaskEngineTests(TestCase): service="signal", 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( user=self.user, service="signal", @@ -308,7 +322,9 @@ class TaskEngineTests(TestCase): ) async_to_sync(process_inbound_task_intelligence)(cmd) 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("#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)(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( user=self.user, session=self.session, @@ -352,7 +370,9 @@ class TaskEngineTests(TestCase): ) self.assertEqual(1, len(remaining)) 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)) @patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock) @@ -368,9 +388,14 @@ class TaskEngineTests(TestCase): source_chat_id="120363402761690215@g.us", ) 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( - 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.", ) @@ -386,8 +411,12 @@ class TaskEngineTests(TestCase): source_chat_id="120363402761690215@g.us", ) async_to_sync(process_inbound_task_intelligence)(msg) - self.assertTrue(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( + 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)) def test_task_with_epic_token_assigns_epic(self): diff --git a/core/tests/test_signal_send_normalization.py b/core/tests/test_signal_send_normalization.py index 4d38633..6077350 100644 --- a/core/tests/test_signal_send_normalization.py +++ b/core/tests/test_signal_send_normalization.py @@ -1,8 +1,9 @@ from __future__ import annotations +from unittest.mock import patch + from asgiref.sync import async_to_sync from django.test import TestCase -from unittest.mock import patch from core.clients import signalapi @@ -43,7 +44,9 @@ class _FakeClientSession: class SignalSendNormalizationTests(TestCase): 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( "+447700900000", signalapi.normalize_signal_recipient("+44 7700-900000") ) @@ -71,4 +74,3 @@ class SignalSendNormalizationTests(TestCase): self.assertGreaterEqual(len(_FakeClientSession.posted_payloads), 1) first_payload = _FakeClientSession.posted_payloads[0] self.assertEqual(["+447700900000"], first_payload.get("recipients")) - diff --git a/core/tests/test_signal_unlink_fallback.py b/core/tests/test_signal_unlink_fallback.py index 404fefb..01e9128 100644 --- a/core/tests/test_signal_unlink_fallback.py +++ b/core/tests/test_signal_unlink_fallback.py @@ -40,4 +40,3 @@ class SignalUnlinkFallbackTests(TestCase): self.assertTrue(result) self.assertEqual(2, mock_delete.call_count) mock_wipe.assert_called_once() - diff --git a/stack.env.example b/stack.env.example index 459b0fa..ea3897a 100644 --- a/stack.env.example +++ b/stack.env.example @@ -43,6 +43,9 @@ QUADLET_PROSODY_CERTS_DIR=./.podman/gia_prosody_certs QUADLET_PROSODY_DATA_DIR=./.podman/gia_prosody_data 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_SEARCH_BACKEND=django MANTICORE_HTTP_URL=http://localhost:9308