186 lines
7.5 KiB
Python
186 lines
7.5 KiB
Python
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()
|