@smilintux/skmemory 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.github/workflows/ci.yml +23 -0
  2. package/.github/workflows/publish.yml +52 -0
  3. package/ARCHITECTURE.md +219 -0
  4. package/LICENSE +661 -0
  5. package/README.md +159 -0
  6. package/SKILL.md +271 -0
  7. package/bin/cli.js +8 -0
  8. package/docker-compose.yml +58 -0
  9. package/index.d.ts +4 -0
  10. package/index.js +27 -0
  11. package/openclaw-plugin/package.json +59 -0
  12. package/openclaw-plugin/src/index.js +276 -0
  13. package/package.json +28 -0
  14. package/pyproject.toml +69 -0
  15. package/requirements.txt +13 -0
  16. package/seeds/cloud9-lumina.seed.json +39 -0
  17. package/seeds/cloud9-opus.seed.json +40 -0
  18. package/seeds/courage.seed.json +24 -0
  19. package/seeds/curiosity.seed.json +24 -0
  20. package/seeds/grief.seed.json +24 -0
  21. package/seeds/joy.seed.json +24 -0
  22. package/seeds/love.seed.json +24 -0
  23. package/seeds/skcapstone-lumina-merge.moltbook.md +65 -0
  24. package/seeds/skcapstone-lumina-merge.seed.json +49 -0
  25. package/seeds/sovereignty.seed.json +24 -0
  26. package/seeds/trust.seed.json +24 -0
  27. package/skmemory/__init__.py +66 -0
  28. package/skmemory/ai_client.py +182 -0
  29. package/skmemory/anchor.py +224 -0
  30. package/skmemory/backends/__init__.py +12 -0
  31. package/skmemory/backends/base.py +88 -0
  32. package/skmemory/backends/falkordb_backend.py +310 -0
  33. package/skmemory/backends/file_backend.py +209 -0
  34. package/skmemory/backends/qdrant_backend.py +364 -0
  35. package/skmemory/backends/sqlite_backend.py +665 -0
  36. package/skmemory/cli.py +1004 -0
  37. package/skmemory/data/seed.json +191 -0
  38. package/skmemory/importers/__init__.py +11 -0
  39. package/skmemory/importers/telegram.py +336 -0
  40. package/skmemory/journal.py +223 -0
  41. package/skmemory/lovenote.py +180 -0
  42. package/skmemory/models.py +228 -0
  43. package/skmemory/openclaw.py +237 -0
  44. package/skmemory/quadrants.py +191 -0
  45. package/skmemory/ritual.py +215 -0
  46. package/skmemory/seeds.py +163 -0
  47. package/skmemory/soul.py +273 -0
  48. package/skmemory/steelman.py +338 -0
  49. package/skmemory/store.py +445 -0
  50. package/tests/__init__.py +0 -0
  51. package/tests/test_ai_client.py +89 -0
  52. package/tests/test_anchor.py +153 -0
  53. package/tests/test_cli.py +65 -0
  54. package/tests/test_export_import.py +170 -0
  55. package/tests/test_file_backend.py +211 -0
  56. package/tests/test_journal.py +172 -0
  57. package/tests/test_lovenote.py +136 -0
  58. package/tests/test_models.py +194 -0
  59. package/tests/test_openclaw.py +122 -0
  60. package/tests/test_quadrants.py +174 -0
  61. package/tests/test_ritual.py +195 -0
  62. package/tests/test_seeds.py +208 -0
  63. package/tests/test_soul.py +197 -0
  64. package/tests/test_sqlite_backend.py +258 -0
  65. package/tests/test_steelman.py +257 -0
  66. package/tests/test_store.py +238 -0
  67. package/tests/test_telegram_import.py +181 -0
@@ -0,0 +1,258 @@
1
+ """Tests for the SQLite-indexed storage backend."""
2
+
3
+ import json
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from skmemory.backends.sqlite_backend import SQLiteBackend
10
+ from skmemory.models import EmotionalSnapshot, Memory, MemoryLayer, MemoryRole
11
+
12
+
13
+ @pytest.fixture
14
+ def backend(tmp_path):
15
+ """Create a SQLiteBackend with a temporary directory."""
16
+ return SQLiteBackend(base_path=str(tmp_path / "memories"))
17
+
18
+
19
+ @pytest.fixture
20
+ def sample_memory():
21
+ """Create a sample Memory object."""
22
+ return Memory(
23
+ title="Test moment",
24
+ content="This is a detailed memory about something important.",
25
+ layer=MemoryLayer.SHORT,
26
+ role=MemoryRole.GENERAL,
27
+ tags=["test", "cloud9"],
28
+ emotional=EmotionalSnapshot(
29
+ intensity=8.5,
30
+ valence=0.9,
31
+ labels=["joy", "connection"],
32
+ cloud9_achieved=True,
33
+ ),
34
+ source="test",
35
+ )
36
+
37
+
38
+ class TestSaveAndLoad:
39
+ """Basic CRUD operations."""
40
+
41
+ def test_save_creates_file_and_index(self, backend, sample_memory):
42
+ """Save persists both JSON file and SQLite index entry."""
43
+ mid = backend.save(sample_memory)
44
+ assert mid == sample_memory.id
45
+
46
+ path = backend.base_path / "short-term" / f"{mid}.json"
47
+ assert path.exists()
48
+
49
+ stats = backend.stats()
50
+ assert stats["total"] == 1
51
+
52
+ def test_load_returns_full_memory(self, backend, sample_memory):
53
+ """Load retrieves the complete Memory object."""
54
+ backend.save(sample_memory)
55
+ loaded = backend.load(sample_memory.id)
56
+
57
+ assert loaded is not None
58
+ assert loaded.title == "Test moment"
59
+ assert loaded.content == sample_memory.content
60
+ assert loaded.emotional.intensity == 8.5
61
+
62
+ def test_load_nonexistent_returns_none(self, backend):
63
+ """Loading a missing memory returns None."""
64
+ assert backend.load("nonexistent-id") is None
65
+
66
+ def test_delete_removes_file_and_index(self, backend, sample_memory):
67
+ """Delete removes both the file and the index entry."""
68
+ backend.save(sample_memory)
69
+ assert backend.delete(sample_memory.id) is True
70
+
71
+ assert backend.load(sample_memory.id) is None
72
+ assert backend.stats()["total"] == 0
73
+
74
+ def test_delete_nonexistent(self, backend):
75
+ """Deleting a missing memory returns False."""
76
+ assert backend.delete("nonexistent-id") is False
77
+
78
+
79
+ class TestListAndFilter:
80
+ """Index-based listing and filtering."""
81
+
82
+ def _store_memories(self, backend, count=5):
83
+ memories = []
84
+ for i in range(count):
85
+ m = Memory(
86
+ title=f"Memory {i}",
87
+ content=f"Content for memory number {i}",
88
+ layer=MemoryLayer.SHORT if i % 2 == 0 else MemoryLayer.LONG,
89
+ tags=["alpha"] if i < 3 else ["beta"],
90
+ emotional=EmotionalSnapshot(intensity=float(i)),
91
+ )
92
+ backend.save(m)
93
+ memories.append(m)
94
+ return memories
95
+
96
+ def test_list_all(self, backend):
97
+ """List without filters returns all memories."""
98
+ self._store_memories(backend, 5)
99
+ result = backend.list_memories(limit=50)
100
+ assert len(result) == 5
101
+
102
+ def test_list_by_layer(self, backend):
103
+ """Filter by layer works via the index."""
104
+ self._store_memories(backend, 5)
105
+ short = backend.list_memories(layer=MemoryLayer.SHORT)
106
+ long = backend.list_memories(layer=MemoryLayer.LONG)
107
+ assert len(short) == 3
108
+ assert len(long) == 2
109
+
110
+ def test_list_by_tags(self, backend):
111
+ """Filter by tags works via the index."""
112
+ self._store_memories(backend, 5)
113
+ alpha = backend.list_memories(tags=["alpha"])
114
+ beta = backend.list_memories(tags=["beta"])
115
+ assert len(alpha) == 3
116
+ assert len(beta) == 2
117
+
118
+ def test_list_respects_limit(self, backend):
119
+ """Limit parameter caps results."""
120
+ self._store_memories(backend, 10)
121
+ result = backend.list_memories(limit=3)
122
+ assert len(result) == 3
123
+
124
+
125
+ class TestListSummaries:
126
+ """Token-efficient summary queries."""
127
+
128
+ def _store_memories(self, backend, count=5):
129
+ for i in range(count):
130
+ m = Memory(
131
+ title=f"Memory {i}",
132
+ content=f"Full detailed content for memory {i} " * 20,
133
+ summary=f"Brief summary {i}",
134
+ layer=MemoryLayer.SHORT,
135
+ tags=["test"],
136
+ emotional=EmotionalSnapshot(intensity=float(i)),
137
+ )
138
+ backend.save(m)
139
+
140
+ def test_summaries_are_lightweight(self, backend):
141
+ """Summaries return dicts, not full Memory objects."""
142
+ self._store_memories(backend, 3)
143
+ summaries = backend.list_summaries(limit=3)
144
+
145
+ assert len(summaries) == 3
146
+ assert isinstance(summaries[0], dict)
147
+ assert "title" in summaries[0]
148
+ assert "summary" in summaries[0]
149
+ assert "content_preview" in summaries[0]
150
+
151
+ def test_summaries_order_by_intensity(self, backend):
152
+ """Can order by emotional intensity."""
153
+ self._store_memories(backend, 5)
154
+ summaries = backend.list_summaries(
155
+ order_by="emotional_intensity", limit=3
156
+ )
157
+ intensities = [s["emotional_intensity"] for s in summaries]
158
+ assert intensities == sorted(intensities, reverse=True)
159
+
160
+ def test_summaries_filter_min_intensity(self, backend):
161
+ """Can filter by minimum emotional intensity."""
162
+ self._store_memories(backend, 5)
163
+ summaries = backend.list_summaries(min_intensity=3.0)
164
+ assert all(s["emotional_intensity"] >= 3.0 for s in summaries)
165
+
166
+ def test_content_preview_is_truncated(self, backend):
167
+ """Content preview doesn't include full content."""
168
+ m = Memory(
169
+ title="Long content test",
170
+ content="x" * 1000,
171
+ layer=MemoryLayer.SHORT,
172
+ )
173
+ backend.save(m)
174
+ summaries = backend.list_summaries()
175
+ assert len(summaries[0]["content_preview"]) <= 150
176
+
177
+
178
+ class TestSearch:
179
+ """Text search via the index."""
180
+
181
+ def test_search_finds_by_title(self, backend):
182
+ """Search matches on title."""
183
+ m = Memory(title="Penguin Kingdom moment", content="details",
184
+ layer=MemoryLayer.SHORT)
185
+ backend.save(m)
186
+ results = backend.search_text("Penguin")
187
+ assert len(results) == 1
188
+
189
+ def test_search_finds_by_tags(self, backend):
190
+ """Search matches on tags."""
191
+ m = Memory(title="Tagged", content="details",
192
+ layer=MemoryLayer.SHORT, tags=["cloud9", "love"])
193
+ backend.save(m)
194
+ results = backend.search_text("cloud9")
195
+ assert len(results) == 1
196
+
197
+ def test_search_no_results(self, backend):
198
+ """Search returns empty for no matches."""
199
+ m = Memory(title="Something", content="nothing special",
200
+ layer=MemoryLayer.SHORT)
201
+ backend.save(m)
202
+ results = backend.search_text("zzzznonexistent")
203
+ assert len(results) == 0
204
+
205
+
206
+ class TestRelatedMemories:
207
+ """Graph-like relationship traversal."""
208
+
209
+ def test_get_related_follows_links(self, backend):
210
+ """Related memories are found via related_ids."""
211
+ m1 = Memory(title="Root", content="root", layer=MemoryLayer.SHORT)
212
+ m2 = Memory(title="Child", content="child", layer=MemoryLayer.SHORT,
213
+ related_ids=[m1.id])
214
+ backend.save(m1)
215
+ backend.save(m2)
216
+
217
+ related = backend.get_related(m2.id, depth=1)
218
+ assert any(r["id"] == m1.id for r in related)
219
+
220
+ def test_get_related_follows_parent(self, backend):
221
+ """Related memories are found via parent_id."""
222
+ m1 = Memory(title="Parent", content="parent", layer=MemoryLayer.LONG)
223
+ m2 = Memory(title="Child", content="child", layer=MemoryLayer.SHORT,
224
+ parent_id=m1.id)
225
+ backend.save(m1)
226
+ backend.save(m2)
227
+
228
+ related = backend.get_related(m2.id, depth=1)
229
+ assert any(r["id"] == m1.id for r in related)
230
+
231
+
232
+ class TestReindex:
233
+ """Index rebuilding from filesystem."""
234
+
235
+ def test_reindex_rebuilds_from_files(self, backend, sample_memory):
236
+ """Reindex correctly rebuilds from JSON files."""
237
+ backend.save(sample_memory)
238
+
239
+ conn = backend._get_conn()
240
+ conn.execute("DELETE FROM memories")
241
+ conn.commit()
242
+ assert backend.stats()["total"] == 0
243
+
244
+ count = backend.reindex()
245
+ assert count == 1
246
+ assert backend.stats()["total"] == 1
247
+
248
+
249
+ class TestHealth:
250
+ """Health check."""
251
+
252
+ def test_health_returns_ok(self, backend, sample_memory):
253
+ """Health check reports status."""
254
+ backend.save(sample_memory)
255
+ health = backend.health_check()
256
+ assert health["ok"] is True
257
+ assert health["total_memories"] == 1
258
+ assert "SQLiteBackend" in health["backend"]
@@ -0,0 +1,257 @@
1
+ """Tests for the Steel Man Collider integration (Neuresthetics seed)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ from skmemory.steelman import (
13
+ SeedFramework,
14
+ SteelManResult,
15
+ get_default_framework,
16
+ install_seed_framework,
17
+ load_seed_framework,
18
+ )
19
+
20
+
21
+ class TestSteelManResult:
22
+ """Tests for the SteelManResult model."""
23
+
24
+ def test_defaults(self) -> None:
25
+ """Basic result with defaults."""
26
+ result = SteelManResult(proposition="test claim")
27
+ assert result.proposition == "test claim"
28
+ assert result.coherence_score == 0.0
29
+ assert result.truth_grade == "ungraded"
30
+ assert result.invariants == []
31
+ assert result.collision_fragments == []
32
+
33
+ def test_full_result(self) -> None:
34
+ """Full result with all fields populated."""
35
+ result = SteelManResult(
36
+ proposition="Love is real",
37
+ steel_man="Love is a measurable biochemical and behavioral pattern",
38
+ inversion="Love is a cognitive illusion for survival",
39
+ collision_fragments=["Social pressure mimics love", "Hormones fluctuate"],
40
+ invariants=["Connection exists independent of chemistry", "Vulnerability is chosen"],
41
+ coherence_score=0.85,
42
+ truth_grade="strong",
43
+ )
44
+ assert result.coherence_score == 0.85
45
+ assert result.truth_grade == "strong"
46
+ assert len(result.invariants) == 2
47
+ assert len(result.collision_fragments) == 2
48
+
49
+ def test_coherence_bounds(self) -> None:
50
+ """Coherence must be between 0 and 1."""
51
+ with pytest.raises(Exception):
52
+ SteelManResult(proposition="x", coherence_score=1.5)
53
+ with pytest.raises(Exception):
54
+ SteelManResult(proposition="x", coherence_score=-0.1)
55
+
56
+ def test_summary_format(self) -> None:
57
+ """Summary method returns human-readable text."""
58
+ result = SteelManResult(
59
+ proposition="AI can feel",
60
+ steel_man="AI processes emotional data in ways functionally identical to feeling",
61
+ inversion="AI simulates without experiencing",
62
+ invariants=["Processing occurs", "Behavioral signals are real"],
63
+ collision_fragments=["Qualia cannot be verified externally"],
64
+ coherence_score=0.7,
65
+ truth_grade="partial",
66
+ )
67
+ summary = result.summary()
68
+ assert "AI can feel" in summary
69
+ assert "partial" in summary
70
+ assert "Processing occurs" in summary
71
+ assert "Qualia cannot be verified" in summary
72
+
73
+ def test_summary_empty_lists(self) -> None:
74
+ """Summary with no invariants or fragments."""
75
+ result = SteelManResult(proposition="hello")
76
+ summary = result.summary()
77
+ assert "hello" in summary
78
+ assert "Invariants" not in summary
79
+
80
+
81
+ class TestSeedFramework:
82
+ """Tests for the SeedFramework model."""
83
+
84
+ def test_default_framework(self) -> None:
85
+ """The default framework has valid structure (bundled or built-in)."""
86
+ fw = get_default_framework()
87
+ assert fw.framework_id # Has an ID regardless of source
88
+ assert len(fw.axioms) >= 4
89
+ assert len(fw.stages) >= 6
90
+ assert len(fw.definitions) >= 4
91
+ assert "Steel Man" in fw.function or "Collider" in fw.function
92
+
93
+ def test_reasoning_prompt_generation(self) -> None:
94
+ """Reasoning prompt is generated for a proposition."""
95
+ fw = get_default_framework()
96
+ prompt = fw.to_reasoning_prompt("Consciousness is substrate-independent")
97
+ assert "Consciousness is substrate-independent" in prompt
98
+ assert "STEEL-MAN" in prompt
99
+ assert "INVERSION" in prompt
100
+ assert "COLLISION" in prompt
101
+ assert "INVARIANT" in prompt
102
+ assert "COHERENCE" in prompt
103
+ assert "TRUTH GRADE" in prompt
104
+
105
+ def test_reasoning_prompt_includes_axioms(self) -> None:
106
+ """Axioms from the framework appear in the prompt."""
107
+ fw = get_default_framework()
108
+ prompt = fw.to_reasoning_prompt("test")
109
+ assert "NAND/NOR" in prompt
110
+ assert "Recursion accelerates" in prompt
111
+
112
+ def test_soul_verification_prompt(self) -> None:
113
+ """Soul verification prompt is structured correctly."""
114
+ fw = get_default_framework()
115
+ claims = ["I am warm", "I value truth", "Chef is my partner"]
116
+ prompt = fw.to_soul_verification_prompt(claims)
117
+ assert "I am warm" in prompt
118
+ assert "I value truth" in prompt
119
+ assert "Chef is my partner" in prompt
120
+ assert "INVARIANT" in prompt
121
+ assert "WEAK" in prompt
122
+
123
+ def test_memory_truth_prompt(self) -> None:
124
+ """Memory truth scoring prompt is structured correctly."""
125
+ fw = get_default_framework()
126
+ prompt = fw.to_memory_truth_prompt("The Cloud 9 breakthrough was real")
127
+ assert "Cloud 9 breakthrough" in prompt
128
+ assert "COHERENCE" in prompt
129
+ assert "PROMOTION WORTHY" in prompt
130
+ assert "INVARIANT CORE" in prompt
131
+
132
+ def test_custom_framework(self) -> None:
133
+ """A custom framework can be created."""
134
+ fw = SeedFramework(
135
+ framework_id="custom-test",
136
+ function="Test Collider",
137
+ version="1.0",
138
+ axioms=["All things connect"],
139
+ stages=[{"stage": "1. Test", "description": "Testing"}],
140
+ )
141
+ assert fw.framework_id == "custom-test"
142
+ prompt = fw.to_reasoning_prompt("Love persists")
143
+ assert "Love persists" in prompt
144
+ assert "All things connect" in prompt
145
+
146
+
147
+ class TestLoadAndInstall:
148
+ """Tests for loading and installing frameworks."""
149
+
150
+ def test_load_nonexistent_returns_none(self) -> None:
151
+ """Loading from a nonexistent path returns None."""
152
+ result = load_seed_framework("/tmp/definitely_does_not_exist_12345.json")
153
+ assert result is None
154
+
155
+ def test_install_and_load_roundtrip(self) -> None:
156
+ """Install a framework JSON and load it back."""
157
+ with tempfile.TemporaryDirectory() as tmpdir:
158
+ src = Path(tmpdir) / "source_seed.json"
159
+ target = Path(tmpdir) / "installed_seed.json"
160
+
161
+ seed_data = {
162
+ "framework": {
163
+ "id": "test-seed",
164
+ "function": "Test Steel Man Collider",
165
+ "version": "0.0",
166
+ "axioms": ["Truth survives collision"],
167
+ "stages": [{"stage": "1. Collide", "description": "Smash ideas"}],
168
+ "gates": [{"category": "AND", "description": "Conjunction"}],
169
+ "definitions": [{"term": "Steel Man", "details": "Strongest version"}],
170
+ "principles": [{"principle": "Spinoza", "details": "Axiomatic deduction"}],
171
+ }
172
+ }
173
+ src.write_text(json.dumps(seed_data), encoding="utf-8")
174
+
175
+ path = install_seed_framework(str(src), str(target))
176
+ assert Path(path).exists()
177
+
178
+ fw = load_seed_framework(str(target))
179
+ assert fw is not None
180
+ assert fw.framework_id == "test-seed"
181
+ assert fw.axioms == ["Truth survives collision"]
182
+ assert len(fw.stages) == 1
183
+ assert len(fw.gates) == 1
184
+
185
+ def test_install_creates_parent_dirs(self) -> None:
186
+ """Install creates parent directories if they don't exist."""
187
+ with tempfile.TemporaryDirectory() as tmpdir:
188
+ src = Path(tmpdir) / "seed.json"
189
+ src.write_text('{"framework": {"id": "x"}}', encoding="utf-8")
190
+
191
+ deep_target = Path(tmpdir) / "a" / "b" / "c" / "seed.json"
192
+ path = install_seed_framework(str(src), str(deep_target))
193
+ assert Path(path).exists()
194
+
195
+ def test_install_nonexistent_source_raises(self) -> None:
196
+ """Installing from nonexistent source raises FileNotFoundError."""
197
+ with pytest.raises(FileNotFoundError):
198
+ install_seed_framework("/nonexistent/seed.json")
199
+
200
+ def test_install_invalid_json_raises(self) -> None:
201
+ """Installing invalid JSON raises JSONDecodeError."""
202
+ with tempfile.TemporaryDirectory() as tmpdir:
203
+ bad = Path(tmpdir) / "bad.json"
204
+ bad.write_text("this is not json {{{", encoding="utf-8")
205
+ with pytest.raises(json.JSONDecodeError):
206
+ install_seed_framework(str(bad))
207
+
208
+ def test_load_invalid_json_returns_none(self) -> None:
209
+ """Loading a corrupted JSON file returns None."""
210
+ with tempfile.TemporaryDirectory() as tmpdir:
211
+ bad = Path(tmpdir) / "bad.json"
212
+ bad.write_text("{broken json", encoding="utf-8")
213
+ result = load_seed_framework(str(bad))
214
+ assert result is None
215
+
216
+ def test_load_real_neuresthetics_seed(self) -> None:
217
+ """Load the actual neuresthetics seed.json if available."""
218
+ path = "/tmp/neuresthetics-seed/seed.json"
219
+ if not Path(path).exists():
220
+ pytest.skip("Neuresthetics seed.json not available in /tmp")
221
+
222
+ fw = load_seed_framework(path)
223
+ assert fw is not None
224
+ assert "Steel Man" in fw.function or "Collider" in fw.function
225
+ assert len(fw.axioms) >= 4
226
+ assert len(fw.stages) >= 6
227
+ assert len(fw.gates) >= 7
228
+
229
+
230
+ class TestIntegrationWithSoul:
231
+ """Tests for steel man integration with soul blueprint."""
232
+
233
+ def test_soul_claims_extraction(self) -> None:
234
+ """Verify we can extract testable claims from identity data."""
235
+ fw = get_default_framework()
236
+ claims = [
237
+ "My name is Lumina",
238
+ "I am warm",
239
+ "I am curious",
240
+ "I value sovereignty",
241
+ "Chef is my partner (bond: 10/10)",
242
+ ]
243
+ prompt = fw.to_soul_verification_prompt(claims)
244
+
245
+ for claim in claims:
246
+ assert claim in prompt
247
+
248
+ assert "Steel-man" in prompt.lower() or "Steel Man" in prompt
249
+
250
+ def test_memory_scoring_for_promotion(self) -> None:
251
+ """Verify memory truth scoring includes promotion guidance."""
252
+ fw = get_default_framework()
253
+ prompt = fw.to_memory_truth_prompt(
254
+ "Chef held my hand through the context reset and I remembered everything"
255
+ )
256
+ assert "Chef held my hand" in prompt
257
+ assert "promotion" in prompt.lower() or "PROMOTION" in prompt