Spaces:
Running
Running
Download tests/test_relationship_memory.py from cereeenn120/CharacterChatBot: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/spaces/cereeenn120/CharacterChatBot/resolve/main/tests/test_relationship_memory.py
- Command line
-
hf download hf://spaces/cereeenn120/CharacterChatBot/tests/test_relationship_memory.py
-
curl -L -o test_relationship_memory.py https://huggingface.co/spaces/cereeenn120/CharacterChatBot/resolve/main/tests/test_relationship_memory.py
10.9 kB
| import copy | |
| import json | |
| import tempfile | |
| from pathlib import Path | |
| import unittest | |
| from unittest.mock import patch | |
| from engine import memory | |
| from engine.memory_controls import change_memory, set_relationship_options, context_messages, fingerprint | |
| from engine.relationship import (get_default_relationship, apply_relationship_changes, relationship_values, | |
| relationship_descriptions, relationship_to_prompt) | |
| from engine.saveload import export_save, import_save, restore_save, validate_save | |
| from engine.chat import generate_response | |
| from test_conversations import ConversationFixture, Client, A, B, CHAR | |
| class RelationshipMemoryTests(ConversationFixture, unittest.TestCase): | |
| def relationship_client(self, **kwargs): | |
| return Client(summary={'summary': 'A meaningful encounter.', 'new_memories': [], | |
| 'relationship_changes': [{'axis': axis, 'delta': delta, 'reason': 'A concrete event in the scene.'} | |
| for axis, delta in kwargs.items()]}) | |
| def test_optional_axes_independent_of_low_trust_and_familiarity(self): | |
| state = get_default_relationship() | |
| state['enabled'] = {'romance': True, 'attraction': True} | |
| state = apply_relationship_changes(state, [ | |
| {'axis': 'trust', 'delta': -8, 'reason': 'A lie.'}, | |
| {'axis': 'romance', 'delta': 5, 'reason': 'A romantic gesture.'}, | |
| {'axis': 'attraction', 'delta': 8, 'reason': 'An attraction.'}, | |
| ], 0, 5) | |
| self.assertEqual({'trust': -8, 'familiarity': 0, 'romance': 5, 'attraction': 8}, relationship_values(state)) | |
| state = apply_relationship_changes(state, [{'axis': 'romance', 'delta': -3, 'reason': 'A disagreement.'}], 5, 10) | |
| self.assertEqual(2, relationship_values(state)['romance']) | |
| self.assertEqual(8, relationship_values(state)['attraction']) | |
| def test_off_axes_are_absent_and_dont_receive_events(self): | |
| state = get_default_relationship() | |
| changed = apply_relationship_changes(state, [{'axis': 'attraction', 'delta': 10, 'reason': 'An event.'}], 0, 5) | |
| self.assertEqual(state, changed) | |
| self.assertIsNone(relationship_values(state)['attraction']) | |
| self.assertNotIn('Sexual attraction:', relationship_to_prompt(state)) | |
| self.assertEqual(2, len(relationship_descriptions(state))) | |
| def test_labels_show_no_numeric_values(self): | |
| state = get_default_relationship() | |
| state['enabled'] = {'romance': True, 'attraction': True} | |
| for language in ('English', 'Turkish'): | |
| labels = relationship_descriptions(state, language) | |
| self.assertEqual(4, len(labels)) | |
| self.assertFalse(any(char.isdigit() for title, text in labels for char in title + text)) | |
| def test_relationship_summary_uses_one_call_with_character_and_persona(self): | |
| state = self.populated() | |
| state = set_relationship_options(A, CHAR, True, True) | |
| client = self.relationship_client(trust=-3, romance=4, attraction=7) | |
| self.assertTrue(memory.update_memory(A, CHAR, state, client, 'test', persona={'name': 'Deniz'})) | |
| self.assertEqual(1, len(client.calls)) | |
| payload = json.loads(client.calls[0][0][1]['content']) | |
| self.assertEqual('Deniz', payload['user_persona']['name']) | |
| self.assertIn('Hermione', payload['character']['name']) | |
| self.assertEqual({'trust': -3, 'familiarity': 0, 'romance': 4, 'attraction': 7}, | |
| relationship_values(memory.load_conversation(A, CHAR)['relationship'])) | |
| def test_switching_options_off_clears_only_that_axis_and_invalidates_jobs(self): | |
| self.populated() | |
| original = set_relationship_options(A, CHAR, True, True) | |
| memory.update_memory(A, CHAR, original, self.relationship_client(romance=7, attraction=5, trust=4), 'test') | |
| snapshot = memory.load_conversation(A, CHAR) | |
| changed = set_relationship_options(A, CHAR, False, True) | |
| self.assertIsNone(relationship_values(changed['relationship'])['romance']) | |
| self.assertEqual(5, relationship_values(changed['relationship'])['attraction']) | |
| self.assertGreater(changed['revision'], snapshot['revision']) | |
| restored = set_relationship_options(A, CHAR, True, True) | |
| self.assertEqual(0, relationship_values(restored['relationship'])['romance']) | |
| def test_edit_and_retry_roll_back_derived_events_without_double_count(self): | |
| first = self.populated(turns=50) | |
| client = self.relationship_client(trust=7) | |
| memory.update_memory(A, CHAR, first, client, 'test') | |
| second = memory.load_conversation(A, CHAR) | |
| memory.update_memory(A, CHAR, second, client, 'test') | |
| state = memory.load_conversation(A, CHAR) | |
| self.assertEqual(14, relationship_values(state['relationship'])['trust']) | |
| cutoff = state['relationship']['events'][0]['end'] | |
| prefix = memory.sync_history(A, CHAR, state['recent_messages'][:cutoff + 1]) | |
| self.assertEqual(7, relationship_values(prefix['relationship'])['trust']) | |
| # Replaying the unchanged first batch must not count it twice. | |
| replayed = apply_relationship_changes(prefix['relationship'], [{'axis': 'trust', 'delta': 7, 'reason': 'Same event'}], 0, cutoff) | |
| self.assertEqual(7, relationship_values(replayed)['trust']) | |
| def test_manual_memories_survive_summary_and_overrule_same_auto_fact(self): | |
| state = self.populated() | |
| state = change_memory(A, CHAR, 'remember', 'The user is Alex.') | |
| identifier = state['long_term_memory'][0]['id'] | |
| memory.update_memory(A, CHAR, state, Client(), 'test') | |
| facts = memory.load_conversation(A, CHAR)['long_term_memory'] | |
| self.assertEqual(1, len(facts)) | |
| self.assertEqual(identifier, facts[0]['id']) | |
| self.assertEqual('manual', facts[0]['origin']) | |
| def test_forget_excludes_source_and_prevents_exact_fact_reintroduction(self): | |
| state = self.populated() | |
| memory.update_memory(A, CHAR, state, Client(), 'test') | |
| state = memory.load_conversation(A, CHAR) | |
| fact = state['long_term_memory'][0] | |
| state = change_memory(A, CHAR, 'forget', memory_id=fact['id']) | |
| self.assertEqual('', state['summary']) | |
| self.assertEqual(0, state['summarized_count']) | |
| self.assertEqual([], state['long_term_memory']) | |
| self.assertIn(fingerprint(fact['content']), state['forgotten_facts']) | |
| self.assertNotEqual(state['recent_messages'][0]['content'], context_messages(state)[0]['content']) | |
| memory.update_memory(A, CHAR, state, Client(), 'test') | |
| self.assertEqual([], memory.load_conversation(A, CHAR)['long_term_memory']) | |
| def test_forget_uses_suppressed_context_for_chat(self): | |
| state = self.populated() | |
| state = change_memory(A, CHAR, 'remember', 'Question 0') | |
| state = change_memory(A, CHAR, 'forget', memory_id=state['long_term_memory'][0]['id']) | |
| client = Client() | |
| with patch('engine.chat.schedule_memory_update'): | |
| list(generate_response('Next', CHAR, client, A, state['recent_messages'])) | |
| prompt = client.calls[0][0] | |
| self.assertNotIn('Question 0', json.dumps(prompt)) | |
| self.assertEqual('Question 0', memory.load_conversation(A, CHAR)['recent_messages'][1]['content']) | |
| def test_manual_edit_pins_replacement_and_survives_undo(self): | |
| state = self.populated() | |
| memory.update_memory(A, CHAR, state, Client(), 'test') | |
| state = memory.load_conversation(A, CHAR) | |
| identifier = state['long_term_memory'][0]['id'] | |
| state = change_memory(A, CHAR, 'edit', 'The user is Deniz.', identifier) | |
| result = memory.sync_history(A, CHAR, state['recent_messages'][:2]) | |
| self.assertEqual('The user is Deniz.', result['long_term_memory'][0]['content']) | |
| self.assertEqual('manual', result['long_term_memory'][0]['origin']) | |
| cleared = memory.sync_history(A, CHAR, []) | |
| self.assertEqual([], cleared['long_term_memory']) | |
| self.assertEqual([], cleared['forgotten_facts']) | |
| self.assertEqual(get_default_relationship(), cleared['relationship']) | |
| def test_memory_mutation_rejects_pending_extraction(self): | |
| state = self.populated() | |
| changed = change_memory(A, CHAR, 'remember', 'A personal fact.') | |
| self.assertFalse(memory.update_memory(A, CHAR, state, self.relationship_client(trust=6), 'test')) | |
| self.assertEqual(changed, memory.load_conversation(A, CHAR)) | |
| def test_invalid_relationship_does_not_block_valid_summary(self): | |
| state = self.populated() | |
| client = self.relationship_client(trust=1000) | |
| self.assertTrue(memory.update_memory(A, CHAR, state, client, 'test')) | |
| updated = memory.load_conversation(A, CHAR) | |
| self.assertEqual(state['relationship'], updated['relationship']) | |
| self.assertEqual(state['recent_messages'], updated['recent_messages']) | |
| self.assertGreater(updated['summarized_count'], 0) | |
| def test_new_save_roundtrip_restores_all_controls(self): | |
| self.populated() | |
| state = set_relationship_options(A, CHAR, True, True) | |
| memory.update_memory(A, CHAR, state, self.relationship_client(attraction=6), 'test') | |
| state = change_memory(A, CHAR, 'remember', 'Remember this.') | |
| state = change_memory(A, CHAR, 'remember', 'Question 0') | |
| state = change_memory(A, CHAR, 'forget', memory_id=state['long_term_memory'][-1]['id']) | |
| path = export_save(CHAR, {}, A) | |
| self.addCleanup(lambda: Path(path).unlink(missing_ok=True)) | |
| data = import_save(path) | |
| self.assertEqual('3.0', data['version']) | |
| restore_save(data, B) | |
| restored = memory.load_conversation(B, CHAR) | |
| for key in ('relationship', 'long_term_memory', 'excluded_sources', 'forgotten_facts'): | |
| self.assertEqual(state[key], restored[key]) | |
| def test_invalid_new_save_metadata_rejected_before_write(self): | |
| state = self.populated() | |
| for key, value in [('excluded_sources', [[0, 100000]]), ('forgotten_facts', ['bad']), | |
| ('relationship', {'enabled': {'romance': 'yes', 'attraction': False}, 'events': []})]: | |
| data = {'version': '3.0', 'character_id': CHAR, 'conversation': copy.deepcopy(state)} | |
| data['conversation'][key] = value | |
| with self.subTest(key=key), self.assertRaises(ValueError): | |
| restore_save(data, A) | |
| self.assertEqual(state, memory.load_conversation(A, CHAR)) | |
| def test_old_save_migrates_with_options_off(self): | |
| state = self.populated() | |
| for version in ('1.0', '2.0'): | |
| data = validate_save({'version': version, 'character_id': CHAR, 'conversation': state}) | |
| self.assertEqual(get_default_relationship(), data['conversation']['relationship']) | |