@pentatonic-ai/ai-agent-sdk 0.9.5 → 0.10.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 (131) hide show
  1. package/README.md +3 -3
  2. package/bin/cli.js +1 -1
  3. package/bin/commands/config.js +1 -1
  4. package/dist/index.cjs +39 -72
  5. package/dist/index.js +36 -69
  6. package/package.json +10 -3
  7. package/packages/doctor/src/checks/local-memory.js +2 -2
  8. package/packages/memory/README.md +2 -2
  9. package/packages/memory/openclaw-plugin/README.md +2 -2
  10. package/packages/memory/openclaw-plugin/openclaw.plugin.json +1 -1
  11. package/packages/memory/package-lock.json +49 -33
  12. package/packages/memory/package.json +4 -1
  13. package/packages/memory/src/__tests__/engine.test.js +40 -5
  14. package/packages/memory/src/engine.js +38 -3
  15. package/packages/memory/src/server.js +2 -2
  16. package/packages/memory-engine-v2/.env.example +30 -0
  17. package/packages/memory-engine-v2/README.md +125 -0
  18. package/packages/memory-engine-v2/compat/Dockerfile +11 -0
  19. package/packages/memory-engine-v2/compat/requirements.txt +6 -0
  20. package/packages/memory-engine-v2/compat/server.py +1047 -0
  21. package/packages/memory-engine-v2/docker-compose.aws.yml +78 -0
  22. package/packages/memory-engine-v2/docker-compose.yml +206 -0
  23. package/packages/memory-engine-v2/extractor-async/Dockerfile +14 -0
  24. package/packages/memory-engine-v2/extractor-async/confidence.py +62 -0
  25. package/packages/memory-engine-v2/extractor-async/noise_filter.py +144 -0
  26. package/packages/memory-engine-v2/extractor-async/requirements.txt +2 -0
  27. package/packages/memory-engine-v2/extractor-async/test_confidence.py +76 -0
  28. package/packages/memory-engine-v2/extractor-async/test_noise_filter.py +177 -0
  29. package/packages/memory-engine-v2/extractor-async/worker.py +797 -0
  30. package/packages/memory-engine-v2/extractor-sync/Dockerfile +11 -0
  31. package/packages/memory-engine-v2/extractor-sync/requirements.txt +4 -0
  32. package/packages/memory-engine-v2/extractor-sync/server.py +424 -0
  33. package/packages/memory-engine-v2/org-model/migrations/001_init.sql +390 -0
  34. package/packages/memory-engine-v2/tests/e2e_smoke.py +356 -0
  35. package/packages/memory-engine-v2/tests/fixtures/generate_synthetic_corpus.py +758 -0
  36. package/packages/memory-engine/.env.example +0 -13
  37. package/packages/memory-engine/MIGRATION.md +0 -219
  38. package/packages/memory-engine/README.md +0 -145
  39. package/packages/memory-engine/bench/README.md +0 -99
  40. package/packages/memory-engine/bench/scorecards-engine/agent-coding__pentatonic-baseline__20260427-142523.json +0 -1115
  41. package/packages/memory-engine/bench/scorecards-engine/chat-recall__pentatonic-baseline__20260427-142648.json +0 -819
  42. package/packages/memory-engine/bench/scorecards-engine/circular-economy__pentatonic-baseline__20260427-142757.json +0 -1278
  43. package/packages/memory-engine/bench/scorecards-engine/customer-support__pentatonic-baseline__20260427-142900.json +0 -1018
  44. package/packages/memory-engine/bench/scorecards-engine/marketplace-ops__pentatonic-baseline__20260427-142957.json +0 -1038
  45. package/packages/memory-engine/bench/scorecards-engine/product-catalogue__pentatonic-baseline__20260427-143122.json +0 -961
  46. package/packages/memory-engine/bench/scorecards-engine-via-docker/agent-coding__pentatonic-memory__20260427-161812.json +0 -1115
  47. package/packages/memory-engine/bench/scorecards-engine-via-docker/chat-recall__pentatonic-memory__20260427-161701.json +0 -819
  48. package/packages/memory-engine/bench/scorecards-engine-via-docker/circular-economy__pentatonic-memory__20260427-161713.json +0 -1278
  49. package/packages/memory-engine/bench/scorecards-engine-via-docker/customer-support__pentatonic-memory__20260427-161723.json +0 -1018
  50. package/packages/memory-engine/bench/scorecards-engine-via-docker/marketplace-ops__pentatonic-memory__20260427-161732.json +0 -1038
  51. package/packages/memory-engine/bench/scorecards-engine-via-docker/product-catalogue__pentatonic-memory__20260427-161741.json +0 -937
  52. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/agent-coding__pentatonic-memory__20260427-184718.json +0 -1115
  53. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/chat-recall__pentatonic-memory__20260427-184614.json +0 -819
  54. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/circular-economy__pentatonic-memory__20260427-184809.json +0 -1278
  55. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/customer-support__pentatonic-memory__20260427-184854.json +0 -1018
  56. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/marketplace-ops__pentatonic-memory__20260427-184929.json +0 -1038
  57. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/product-catalogue__pentatonic-memory__20260427-185015.json +0 -961
  58. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/agent-coding__pentatonic-memory__20260427-175252.json +0 -1115
  59. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/chat-recall__pentatonic-memory__20260427-175312.json +0 -819
  60. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/circular-economy__pentatonic-memory__20260427-175335.json +0 -1278
  61. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/customer-support__pentatonic-memory__20260427-175355.json +0 -1018
  62. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/marketplace-ops__pentatonic-memory__20260427-175413.json +0 -1038
  63. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/product-catalogue__pentatonic-memory__20260427-175430.json +0 -883
  64. package/packages/memory-engine/bench/scorecards-engine-via-shim/agent-coding__pentatonic-memory__20260427-155409.json +0 -1115
  65. package/packages/memory-engine/bench/scorecards-engine-via-shim/chat-recall__pentatonic-memory__20260427-155421.json +0 -819
  66. package/packages/memory-engine/bench/scorecards-engine-via-shim/circular-economy__pentatonic-memory__20260427-155433.json +0 -1278
  67. package/packages/memory-engine/bench/scorecards-engine-via-shim/customer-support__pentatonic-memory__20260427-155443.json +0 -1018
  68. package/packages/memory-engine/bench/scorecards-engine-via-shim/marketplace-ops__pentatonic-memory__20260427-155453.json +0 -1038
  69. package/packages/memory-engine/bench/scorecards-engine-via-shim/product-catalogue__pentatonic-memory__20260427-155503.json +0 -937
  70. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory-latest__20260427-145103.json +0 -1115
  71. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory__20260427-144909.json +0 -1115
  72. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory-latest__20260427-145153.json +0 -819
  73. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory__20260427-145120.json +0 -542
  74. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory-latest__20260427-145313.json +0 -1278
  75. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory__20260427-145207.json +0 -894
  76. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory-latest__20260427-145412.json +0 -1018
  77. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory__20260427-145327.json +0 -680
  78. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory-latest__20260427-145517.json +0 -1038
  79. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory__20260427-145422.json +0 -693
  80. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory-latest__20260427-145616.json +0 -961
  81. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory__20260427-145528.json +0 -727
  82. package/packages/memory-engine/compat/Dockerfile +0 -22
  83. package/packages/memory-engine/compat/server.py +0 -1255
  84. package/packages/memory-engine/docker-compose.test.yml +0 -59
  85. package/packages/memory-engine/docker-compose.yml +0 -240
  86. package/packages/memory-engine/engine/README.md +0 -52
  87. package/packages/memory-engine/engine/l2-hybridrag-proxy.py +0 -1543
  88. package/packages/memory-engine/engine/l5-comms-layer.py +0 -663
  89. package/packages/memory-engine/engine/l6-document-store.py +0 -1018
  90. package/packages/memory-engine/engine/services/_shared/__init__.py +0 -1
  91. package/packages/memory-engine/engine/services/_shared/embed_provider.py +0 -468
  92. package/packages/memory-engine/engine/services/l2/Dockerfile +0 -50
  93. package/packages/memory-engine/engine/services/l2/init_databases.py +0 -81
  94. package/packages/memory-engine/engine/services/l2/l2-hybridrag-proxy.py +0 -2721
  95. package/packages/memory-engine/engine/services/l5/Dockerfile +0 -11
  96. package/packages/memory-engine/engine/services/l5/l5-comms-layer.py +0 -808
  97. package/packages/memory-engine/engine/services/l6/Dockerfile +0 -30
  98. package/packages/memory-engine/engine/services/l6/l6-document-store.py +0 -1221
  99. package/packages/memory-engine/engine/services/nv-embed/Dockerfile +0 -28
  100. package/packages/memory-engine/engine/services/nv-embed/server.py +0 -152
  101. package/packages/memory-engine/pme_memory/__init__.py +0 -0
  102. package/packages/memory-engine/pme_memory/__main__.py +0 -129
  103. package/packages/memory-engine/pme_memory/artifacts.py +0 -95
  104. package/packages/memory-engine/pme_memory/embed.py +0 -74
  105. package/packages/memory-engine/pme_memory/health.py +0 -36
  106. package/packages/memory-engine/pme_memory/hygiene.py +0 -159
  107. package/packages/memory-engine/pme_memory/indexer.py +0 -200
  108. package/packages/memory-engine/pme_memory/needs.py +0 -55
  109. package/packages/memory-engine/pme_memory/provenance.py +0 -80
  110. package/packages/memory-engine/pme_memory/scoring.py +0 -168
  111. package/packages/memory-engine/pme_memory/search.py +0 -52
  112. package/packages/memory-engine/pme_memory/store.py +0 -86
  113. package/packages/memory-engine/pme_memory/synthesis.py +0 -114
  114. package/packages/memory-engine/pyproject.toml +0 -65
  115. package/packages/memory-engine/scripts/kg-extractor.py +0 -557
  116. package/packages/memory-engine/scripts/kg-preflexor-v2.py +0 -738
  117. package/packages/memory-engine/scripts/wipe-legacy-l3-entities.py +0 -128
  118. package/packages/memory-engine/tests/e2e_arena.sh +0 -259
  119. package/packages/memory-engine/tests/embed_stub/Dockerfile +0 -13
  120. package/packages/memory-engine/tests/embed_stub/server.py +0 -80
  121. package/packages/memory-engine/tests/test_aggregate.py +0 -333
  122. package/packages/memory-engine/tests/test_api_contract.sh +0 -57
  123. package/packages/memory-engine/tests/test_arena_safety.py +0 -232
  124. package/packages/memory-engine/tests/test_channel_stat_reader.py +0 -437
  125. package/packages/memory-engine/tests/test_channel_stat_rollups.py +0 -308
  126. package/packages/memory-engine/tests/test_compat_nv_embed_probe.py +0 -48
  127. package/packages/memory-engine/tests/test_embed_provider.py +0 -492
  128. package/packages/memory-engine/tests/test_l2_qmd_vec_search.py +0 -280
  129. package/packages/memory-engine/tests/test_l3_arena_isolation.py +0 -412
  130. package/packages/memory-engine/tests/test_l6_module_load.py +0 -84
  131. package/packages/memory-engine/tests/test_people_list_reader.py +0 -432
@@ -0,0 +1,356 @@
1
+ """End-to-end smoke tests for pentatonic-memory-engine v2.
2
+
3
+ Run on the engine host against compat at http://localhost:8199. Verifies:
4
+
5
+ 1. /health + /health/deep
6
+ 2. Single /store works end-to-end (compat → extractor-sync → org-model +
7
+ vector-index)
8
+ 3. /search finds the stored record, late-materialisation hydrates content
9
+ 4. Idempotency: same arena+content → same content-hash ID
10
+ 5. /store-batch works with the exact wire shape TES's
11
+ engineStoreBatch helper posts (this is the TES-cutover gate)
12
+ 6. Cross-arena isolation: search in arena A doesn't return arena B records
13
+ 7. /forget by id deletes from both stores
14
+ 8. /forget by metadata_contains (with arena filter) deletes matching set
15
+ 9. FORGET cascade: extracted entities for an event vanish when the event
16
+ is forgotten
17
+
18
+ Exits 0 on success, 1 on any failure. Prints a clear pass/fail line per
19
+ test so the SSM output is greppable.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import sys
26
+ import time
27
+ import urllib.request
28
+ import urllib.error
29
+
30
+ BASE = "http://localhost:8199"
31
+
32
+
33
+ def _http(method: str, path: str, body: dict | None = None) -> tuple[int, dict]:
34
+ data = json.dumps(body).encode() if body is not None else None
35
+ req = urllib.request.Request(
36
+ f"{BASE}{path}",
37
+ method=method,
38
+ data=data,
39
+ headers={"Content-Type": "application/json"} if body else {},
40
+ )
41
+ try:
42
+ with urllib.request.urlopen(req, timeout=30) as r:
43
+ return r.status, json.loads(r.read())
44
+ except urllib.error.HTTPError as e:
45
+ return e.code, {"_error": e.read().decode(errors="replace")[:500]}
46
+
47
+
48
+ _passes = 0
49
+ _fails: list[str] = []
50
+
51
+
52
+ def _assert(cond: bool, msg: str) -> None:
53
+ global _passes
54
+ if cond:
55
+ _passes += 1
56
+ print(f" PASS {msg}")
57
+ else:
58
+ _fails.append(msg)
59
+ print(f" FAIL {msg}")
60
+
61
+
62
+ def section(name: str) -> None:
63
+ print(f"\n=== {name} ===")
64
+
65
+
66
+ # ----------------------------------------------------------------------
67
+ # 1. Health
68
+ # ----------------------------------------------------------------------
69
+
70
+ section("health")
71
+ code, body = _http("GET", "/health")
72
+ _assert(code == 200, f"GET /health returns 200 (got {code})")
73
+ _assert(body.get("status") == "healthy", f"health.status == healthy (got {body.get('status')})")
74
+ _assert(body.get("service") == "pme2-compat", f"health.service identifies pme2-compat")
75
+
76
+ code, body = _http("GET", "/health/deep")
77
+ _assert(code == 200, f"GET /health/deep returns 200 (got {code})")
78
+ stores = body.get("stores", {})
79
+ _assert(stores.get("org_model", {}).get("status") == "ok", "health/deep: org_model ok")
80
+ _assert(stores.get("vector_index", {}).get("status") == "ok", "health/deep: vector_index ok")
81
+ _assert(stores.get("embed_gateway", {}).get("status") == "ok", "health/deep: embed_gateway ok")
82
+ _assert(stores.get("embed_gateway", {}).get("dim") == 4096, "health/deep: embed dim is 4096")
83
+
84
+
85
+ # ----------------------------------------------------------------------
86
+ # 2-4. /store + /search + idempotency
87
+ # ----------------------------------------------------------------------
88
+
89
+ section("/store + /search + idempotency")
90
+
91
+ # Use a unique arena per test run so we don't collide with anything else
92
+ # accumulating in the engine. Time-based suffix.
93
+ ARENA_A = f"e2e-test-a-{int(time.time())}"
94
+ ARENA_B = f"e2e-test-b-{int(time.time())}"
95
+
96
+ # Single store.
97
+ store_body = {
98
+ "content": "The quarterly review confirmed that the integration project is on track for Q3 delivery",
99
+ "metadata": {"arena": ARENA_A, "clientId": ARENA_A, "kind": "doc"},
100
+ }
101
+ code, resp = _http("POST", "/store", store_body)
102
+ _assert(code == 200, f"POST /store returns 200 (got {code})")
103
+ event_id_1 = resp.get("id")
104
+ _assert(isinstance(event_id_1, str) and len(event_id_1) == 32, f"store returns 32-char hex id (got {event_id_1!r})")
105
+ _assert(resp.get("layerId", "").startswith("ml_"), "store returns layerId in v1-compat shape")
106
+
107
+ # Idempotency: same arena + content → same id.
108
+ code2, resp2 = _http("POST", "/store", store_body)
109
+ _assert(code2 == 200, "second /store with same content returns 200")
110
+ _assert(resp2.get("id") == event_id_1, f"idempotent: same content → same id (got {resp2.get('id')} vs {event_id_1})")
111
+
112
+ # Different content → different id.
113
+ store_body2 = {**store_body, "content": "Different content same arena should yield a different id deterministically"}
114
+ code3, resp3 = _http("POST", "/store", store_body2)
115
+ _assert(code3 == 200, "third /store (different content) returns 200")
116
+ _assert(resp3.get("id") != event_id_1, "different content → different id")
117
+ event_id_2 = resp3.get("id")
118
+
119
+ # Search should find both.
120
+ time.sleep(1) # let Qdrant settle
121
+ search_body = {"query": "quarterly review integration project", "arena": ARENA_A, "limit": 5}
122
+ code, sresp = _http("POST", "/search", search_body)
123
+ _assert(code == 200, f"POST /search returns 200 (got {code})")
124
+ results = sresp.get("results", [])
125
+ _assert(len(results) >= 1, f"/search returns at least 1 result (got {len(results)})")
126
+ ids_found = [r["id"] for r in results]
127
+ _assert(event_id_1 in ids_found, f"/search finds first stored event ({event_id_1[:8]}...)")
128
+ _assert(all(r.get("similarity", 0) > 0 for r in results), "every result has similarity > 0")
129
+ # Late materialisation: content is hydrated from org-model
130
+ _assert(any("integration project" in r.get("content", "") for r in results),
131
+ "late materialisation: full content (not just preview) in response")
132
+
133
+
134
+ # ----------------------------------------------------------------------
135
+ # 5. TES wire shape — exact engineStoreBatch body
136
+ # ----------------------------------------------------------------------
137
+
138
+ section("TES engineStoreBatch wire shape")
139
+
140
+ # This is the exact body TES's SDK posts (see ai-agent-sdk packages/memory/src/engine.js).
141
+ # Gate: v2 must accept this without modification.
142
+ tes_arena = f"e2e-tes-{int(time.time())}"
143
+ tes_body = {
144
+ "records": [
145
+ {
146
+ "id": "stable-dedup-key-1",
147
+ "content": "First record from the simulated TES consumer drain",
148
+ "metadata": {
149
+ "arena": tes_arena,
150
+ "layer_type": "episodic",
151
+ "actor_user_id": "u-philh",
152
+ "kind": "chat",
153
+ "channel": "slack",
154
+ },
155
+ },
156
+ {
157
+ "id": "stable-dedup-key-2",
158
+ "content": "Second record, same batch, different content payload",
159
+ "metadata": {
160
+ "arena": tes_arena,
161
+ "layer_type": "episodic",
162
+ "actor_user_id": "u-philh",
163
+ "kind": "chat",
164
+ "channel": "slack",
165
+ },
166
+ },
167
+ {
168
+ # No caller-supplied id — v2 derives content-hash id.
169
+ "content": "Third record without explicit id, server derives the content-hash",
170
+ "metadata": {
171
+ "arena": tes_arena,
172
+ "layer_type": "episodic",
173
+ "actor_user_id": "u-philh",
174
+ "kind": "chat",
175
+ },
176
+ },
177
+ ],
178
+ }
179
+ code, resp = _http("POST", "/store-batch", tes_body)
180
+ _assert(code == 200, f"POST /store-batch (TES shape) returns 200 (got {code})")
181
+ _assert(resp.get("status") == "ok", f"store-batch.status == ok (got {resp.get('status')})")
182
+ _assert(resp.get("inserted") == 3, f"store-batch.inserted == 3 (got {resp.get('inserted')})")
183
+ ids = resp.get("ids", [])
184
+ _assert(len(ids) == 3, f"store-batch.ids has 3 entries (got {len(ids)})")
185
+ _assert(all(isinstance(i, str) and len(i) == 32 for i in ids), "all ids are 32-char hex content-hashes")
186
+
187
+ # Search for one of them.
188
+ time.sleep(1)
189
+ code, sresp = _http("POST", "/search", {"query": "simulated TES consumer", "arena": tes_arena, "limit": 5})
190
+ _assert(code == 200, "/search (TES arena) returns 200")
191
+ _assert(len(sresp.get("results", [])) >= 1, "TES batch records are findable via /search")
192
+
193
+
194
+ # ----------------------------------------------------------------------
195
+ # 6. Cross-arena isolation
196
+ # ----------------------------------------------------------------------
197
+
198
+ section("cross-arena isolation")
199
+
200
+ # Write something to arena B.
201
+ code, resp = _http("POST", "/store", {
202
+ "content": "Secrets that must not leak into the other tenant under any condition",
203
+ "metadata": {"arena": ARENA_B, "clientId": ARENA_B, "kind": "doc"},
204
+ })
205
+ _assert(code == 200, "store in arena B succeeds")
206
+
207
+ time.sleep(1)
208
+ # Search arena A — should NOT find the arena B record. Match is
209
+ # case-insensitive — stored content uses capital "Secrets".
210
+ code, sresp = _http("POST", "/search", {"query": "secrets must not leak", "arena": ARENA_A, "limit": 5})
211
+ _assert(code == 200, "search in arena A returns 200")
212
+ contents_a = [r.get("content", "").lower() for r in sresp.get("results", [])]
213
+ _assert(not any("secrets" in c for c in contents_a),
214
+ "isolation: arena B's content does NOT appear in arena A search")
215
+
216
+ # Search arena B — should find its own record.
217
+ code, sresp = _http("POST", "/search", {"query": "secrets must not leak", "arena": ARENA_B, "limit": 5})
218
+ contents_b = [r.get("content", "").lower() for r in sresp.get("results", [])]
219
+ _assert(any("secrets" in c for c in contents_b),
220
+ "arena B can find its own records")
221
+
222
+
223
+ # ----------------------------------------------------------------------
224
+ # 7. /forget by id
225
+ # ----------------------------------------------------------------------
226
+
227
+ section("/forget by id")
228
+
229
+ # Forget event_id_2 (the second arena-A record).
230
+ code, fresp = _http("POST", "/forget", {"id": event_id_2})
231
+ _assert(code == 200, "/forget by id returns 200")
232
+ _assert(fresp.get("deleted") == 1, f"/forget deletes exactly 1 (got {fresp.get('deleted')})")
233
+
234
+ time.sleep(1)
235
+ # Now search arena A — event_id_1 should still be there, event_id_2 should be gone.
236
+ code, sresp = _http("POST", "/search", {"query": "Different content same arena", "arena": ARENA_A, "limit": 5})
237
+ ids_after = [r["id"] for r in sresp.get("results", [])]
238
+ _assert(event_id_2 not in ids_after, f"forgotten event no longer in search results")
239
+
240
+
241
+ # ----------------------------------------------------------------------
242
+ # 8. /forget by metadata_contains
243
+ # ----------------------------------------------------------------------
244
+
245
+ section("/forget by metadata_contains")
246
+
247
+ # Forget the entire TES batch by arena filter.
248
+ code, fresp = _http("POST", "/forget", {"metadata_contains": {"arena": tes_arena}})
249
+ _assert(code == 200, "/forget by metadata_contains returns 200")
250
+ _assert(fresp.get("deleted", 0) >= 3, f"forget-by-arena deletes >= 3 (got {fresp.get('deleted')})")
251
+
252
+ time.sleep(1)
253
+ # Search should return nothing.
254
+ code, sresp = _http("POST", "/search", {"query": "simulated TES consumer", "arena": tes_arena, "limit": 5})
255
+ _assert(len(sresp.get("results", [])) == 0,
256
+ f"TES arena search after forget returns empty (got {len(sresp.get('results', []))})")
257
+
258
+
259
+ # ----------------------------------------------------------------------
260
+ # 9. /search requires arena (no implicit "general" wildcard)
261
+ # ----------------------------------------------------------------------
262
+
263
+ section("/search rejects unscoped query")
264
+
265
+ code, sresp = _http("POST", "/search", {"query": "no arena specified", "limit": 5})
266
+ _assert(code == 400, f"/search without arena returns 400 (got {code}) — v2 explicit-scope contract")
267
+
268
+
269
+ # ----------------------------------------------------------------------
270
+ # 10. TES engineSearch wire shape — arenas[] list + arena single-field
271
+ # ----------------------------------------------------------------------
272
+
273
+ section("TES engineSearch wire shape (arenas list)")
274
+
275
+ # This is what the SDK's engineSearch helper actually posts: arenas[]
276
+ # list (authoritative) + arena single-field (back-compat). v2 reads
277
+ # arenas[] when present; ignoring arena is the right behaviour.
278
+ tes_search_arena = f"e2e-tes-search-{int(time.time())}"
279
+ tes_user_arena = f"{tes_search_arena}:u-philh"
280
+ _http("POST", "/store", {
281
+ "content": "User-scoped memory only visible in the user-arena lane",
282
+ "metadata": {"arena": tes_user_arena, "clientId": tes_search_arena, "kind": "chat"},
283
+ })
284
+ _http("POST", "/store", {
285
+ "content": "Tenant-wide memory visible across the whole client",
286
+ "metadata": {"arena": tes_search_arena, "clientId": tes_search_arena, "kind": "doc"},
287
+ })
288
+ time.sleep(1)
289
+
290
+ # engineSearch with userId composes arenas=[clientId, clientId:userId] —
291
+ # caller sees their own AND tenant-wide records, never another user's.
292
+ body = {
293
+ "query": "memory tenant user-scoped visible",
294
+ "arenas": [tes_search_arena, tes_user_arena],
295
+ "arena": tes_search_arena,
296
+ "limit": 10,
297
+ "min_score": 0.001,
298
+ }
299
+ code, sresp = _http("POST", "/search", body)
300
+ _assert(code == 200, f"engineSearch wire shape returns 200 (got {code})")
301
+ ids = [r["id"] for r in sresp.get("results", [])]
302
+ arenas_seen = {r.get("metadata", {}).get("arena") for r in sresp.get("results", [])}
303
+ _assert(len(sresp.get("results", [])) >= 2, f"multi-arena search returns both records (got {len(sresp.get('results', []))})")
304
+ _assert(tes_search_arena in arenas_seen and tes_user_arena in arenas_seen,
305
+ f"both arena and user-arena records present (got {arenas_seen})")
306
+
307
+
308
+ # ----------------------------------------------------------------------
309
+ # 11. TES engineForget wire shape — top-level arena + id
310
+ # ----------------------------------------------------------------------
311
+
312
+ section("TES engineForget wire shape (top-level arena + id)")
313
+
314
+ # Plant a record to forget.
315
+ code, resp = _http("POST", "/store", {
316
+ "content": "Record specifically to be forgotten via the engineForget(id=...) wire shape",
317
+ "metadata": {"arena": ARENA_A, "clientId": ARENA_A, "kind": "doc"},
318
+ })
319
+ forget_target_id = resp["id"]
320
+
321
+ # SDK engineForget(id=X) posts {arena: clientId, id: X}. v2 should use
322
+ # id and ignore the redundant top-level arena.
323
+ code, fresp = _http("POST", "/forget", {"arena": ARENA_A, "id": forget_target_id})
324
+ _assert(code == 200, f"engineForget(id) with top-level arena returns 200 (got {code})")
325
+ _assert(fresp.get("deleted") == 1, f"deletes exactly 1 by id (got {fresp.get('deleted')})")
326
+
327
+
328
+ # ----------------------------------------------------------------------
329
+ # 12. Endpoints NOT yet implemented (gracefully missing)
330
+ # ----------------------------------------------------------------------
331
+
332
+ section("Endpoints v2 doesn't implement yet")
333
+
334
+ # personFacets calls /aggregate; peopleList calls /people-list-internal.
335
+ # v2 doesn't implement these (waiting on keystone spec + recursive-CTE
336
+ # spike). They should 404, not crash — TES resolvers have fallback
337
+ # paths that handle missing aggregate endpoints gracefully.
338
+ code, _ = _http("POST", "/aggregate", {"arena": ARENA_A, "contact_email": "a@b.com"})
339
+ _assert(code == 404, f"/aggregate returns 404 (v2 doesn't implement yet) (got {code})")
340
+ code, _ = _http("POST", "/people-list-internal", {"arenas": [ARENA_A]})
341
+ _assert(code == 404, f"/people-list-internal returns 404 (v2 doesn't implement yet) (got {code})")
342
+
343
+
344
+ # ----------------------------------------------------------------------
345
+ # Done
346
+ # ----------------------------------------------------------------------
347
+
348
+ print(f"\n{'=' * 60}")
349
+ if _fails:
350
+ print(f"FAIL — {len(_fails)} failure(s):")
351
+ for f in _fails:
352
+ print(f" - {f}")
353
+ print(f"passes: {_passes}")
354
+ sys.exit(1)
355
+ print(f"OK — all {_passes} assertions passed")
356
+ sys.exit(0)