import unittest from unittest.mock import AsyncMock, patch from pathlib import Path import sys REPO_ROOT = Path(__file__).resolve().parents[1] # Samme flate modulstruktur som i Docker-runtime /app: sys.path.insert(0, str(REPO_ROOT / "opax-mcp")) sys.path.insert(0, str(REPO_ROOT / "emma")) from emma_adapter import CanonicalEmma from emma_identity import EMMA_IDENTITY, get_runtime_system_prompt class TestCanonicalEmmaPhase1B(unittest.IsolatedAsyncioTestCase): def test_runtime_prompt_contains_canonical_identity(self): """(1) Verifies the runtime prompt contains the core identity and rules.""" prompt = get_runtime_system_prompt() self.assertIn("Du er Emma", prompt) self.assertIn("HARD REGLER:", prompt) self.assertNotIn("gcp_project", prompt) self.assertNotIn("opax_url", prompt) async def test_adapter_injects_model_prompt_and_system_prompt(self): """(2) Verifies the adapter calls the injected function with correct args.""" mock_chat_fn = AsyncMock() model = "test-model-456" prompt = "test prompt 123" adapter = CanonicalEmma(chat_function=mock_chat_fn, model=model) await adapter.run(prompt) mock_chat_fn.assert_awaited_once() call_args = mock_chat_fn.call_args self.assertEqual(call_args.args[0], model) self.assertEqual(call_args.args[1], prompt) self.assertEqual(call_args.args[2], get_runtime_system_prompt()) async def test_adapter_returns_raw_response_unchanged(self): """(3) Verifies the adapter returns the original response from the chat function.""" mock_response = {"model": "test-model", "response": "test-response", "done": True} mock_chat_fn = AsyncMock(return_value=mock_response) adapter = CanonicalEmma(chat_function=mock_chat_fn, model="any-model") result = await adapter.run("any-prompt") self.assertEqual(result, mock_response) def test_adapter_has_no_memory_or_tool_execution(self): """(4) Verifies no memory or tool execution is implicitly activated.""" mock_chat_fn = AsyncMock() adapter = CanonicalEmma(chat_function=mock_chat_fn, model="any-model") self.assertFalse(hasattr(adapter, '_memory')) self.assertFalse(hasattr(adapter, '_tool_registry')) def test_preserves_all_canonical_rules(self): """(5) Verifies ALL canonical rules from EMMA_IDENTITY are in the prompt.""" prompt = get_runtime_system_prompt() self.assertIn("HARD REGLER:", prompt) for rule in EMMA_IDENTITY["rules"]: self.assertIn(rule, prompt) def test_policy_same_language(self): """Verifies the same-language response policy is in the prompt.""" prompt = get_runtime_system_prompt() self.assertIn( "Svar på samme språk som brukeren bruker. Svar på norsk ved norsk input og på engelsk ved engelsk input.", prompt, ) def test_policy_no_invention(self): """Verifies the policy against inventing VAUCO facts is in the prompt.""" prompt = get_runtime_system_prompt() self.assertIn( "Ikke gjett, dikt opp eller presenter generell kunnskap som VAUCO-fakta.", prompt, ) def test_policy_inactive_gitea(self): """Verifies the boundary for inactive Gitea access is in the prompt.""" prompt = get_runtime_system_prompt() self.assertIn( "Du har ikke aktiv tilgang i denne chatten til Gitea, Git, commits, branches, repositories,", prompt, ) def test_policy_inactive_memory(self): """Verifies the boundary for inactive memory/retrieval is in the prompt.""" prompt = get_runtime_system_prompt() self.assertIn( "Du har ikke aktiv tilgang i denne chatten til persistent minne, Morphic memory, tidligere", prompt, ) def test_policy_future_confirmation(self): """Verifies the requirement for future explicit confirmation is in the prompt.""" prompt = get_runtime_system_prompt() self.assertIn( "Fremtidige handlinger som endrer tilstand må presenteres konkret og kreve én eksplisitt,", prompt, ) def test_presentation_policy(self): """Verifies the user-facing capability presentation rules are in the prompt.""" normalized_prompt = " ".join(get_runtime_system_prompt().split()) self.assertIn( "Når brukeren spør hva du kan gjøre eller hvilke begrensninger du har, svar", normalized_prompt, ) self.assertIn( "Ikke gjengi eller lekke interne implementasjonsdetaljer i vanlige svar", normalized_prompt, ) self.assertIn( "filbaner, mappenavn, loggfilnavn, konfigurasjonsnavn", normalized_prompt, ) def test_unavailable_action_policy(self): """Verifies the rules for handling unavailable actions are in the prompt.""" normalized_prompt = " ".join(get_runtime_system_prompt().split()) self.assertIn( "ikke be om godkjenning som om godkjenningen alene vil utføre handlingen", normalized_prompt, ) self.assertIn( "handlingen ikke kan utføres fra denne chatten nå", normalized_prompt, ) self.assertIn( "Du kan tilby å utarbeide et utkast, et forslag eller en sjekkliste", normalized_prompt, ) self.assertIn("Ingen handling utføres.", normalized_prompt) async def test_run_without_system_context(self): """(New) Verifies run() without context passes the original system prompt.""" mock_chat_fn = AsyncMock() model = "test-model-no-context" prompt = "test prompt no context" mock_response = {"response": "ok"} mock_chat_fn.return_value = mock_response adapter = CanonicalEmma(chat_function=mock_chat_fn, model=model) result = await adapter.run(prompt, system_context=None) self.assertEqual(result, mock_response) mock_chat_fn.assert_awaited_once() call_args = mock_chat_fn.call_args self.assertEqual(len(call_args.args), 3) self.assertEqual(call_args.args[0], model) self.assertEqual(call_args.args[1], prompt) self.assertEqual(call_args.args[2], get_runtime_system_prompt()) async def test_run_with_system_context(self): """(New) Verifies run() with context appends it correctly.""" mock_chat_fn = AsyncMock() model = "test-model-with-context" prompt = "test prompt with context" bridge_context = "CURRENT CAPABILITY STATUS\n\nAvailable: Yes" mock_response = {"response": "ok"} mock_chat_fn.return_value = mock_response adapter = CanonicalEmma(chat_function=mock_chat_fn, model=model) result = await adapter.run(prompt, system_context=bridge_context) self.assertEqual(result, mock_response) mock_chat_fn.assert_awaited_once() call_args = mock_chat_fn.call_args self.assertEqual(len(call_args.args), 3) self.assertEqual(call_args.args[0], model) self.assertEqual(call_args.args[1], prompt) # User prompt is unchanged composed_prompt = call_args.args[2] canonical_prompt = get_runtime_system_prompt() self.assertTrue(composed_prompt.startswith(canonical_prompt)) self.assertIn(f"\n\n{bridge_context.strip()}", composed_prompt) if __name__ == '__main__': unittest.main()