@pentatonic-ai/ai-agent-sdk 0.9.6 → 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.
- package/README.md +3 -3
- package/bin/cli.js +1 -1
- package/bin/commands/config.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/packages/doctor/src/checks/local-memory.js +2 -2
- package/packages/memory/README.md +2 -2
- package/packages/memory/openclaw-plugin/README.md +2 -2
- package/packages/memory/openclaw-plugin/openclaw.plugin.json +1 -1
- package/packages/memory/src/server.js +2 -2
- package/packages/memory-engine-v2/.env.example +30 -0
- package/packages/memory-engine-v2/README.md +125 -0
- package/packages/memory-engine-v2/compat/Dockerfile +11 -0
- package/packages/memory-engine-v2/compat/requirements.txt +6 -0
- package/packages/memory-engine-v2/compat/server.py +1047 -0
- package/packages/memory-engine-v2/docker-compose.aws.yml +78 -0
- package/packages/memory-engine-v2/docker-compose.yml +206 -0
- package/packages/memory-engine-v2/extractor-async/Dockerfile +14 -0
- package/packages/memory-engine-v2/extractor-async/confidence.py +62 -0
- package/packages/memory-engine-v2/extractor-async/noise_filter.py +144 -0
- package/packages/memory-engine-v2/extractor-async/requirements.txt +2 -0
- package/packages/memory-engine-v2/extractor-async/test_confidence.py +76 -0
- package/packages/memory-engine-v2/extractor-async/test_noise_filter.py +177 -0
- package/packages/memory-engine-v2/extractor-async/worker.py +797 -0
- package/packages/memory-engine-v2/extractor-sync/Dockerfile +11 -0
- package/packages/memory-engine-v2/extractor-sync/requirements.txt +4 -0
- package/packages/memory-engine-v2/extractor-sync/server.py +424 -0
- package/packages/memory-engine-v2/org-model/migrations/001_init.sql +390 -0
- package/packages/memory-engine-v2/tests/e2e_smoke.py +356 -0
- package/packages/memory-engine-v2/tests/fixtures/generate_synthetic_corpus.py +758 -0
- package/packages/memory-engine/.env.example +0 -13
- package/packages/memory-engine/MIGRATION.md +0 -219
- package/packages/memory-engine/README.md +0 -145
- package/packages/memory-engine/bench/README.md +0 -99
- package/packages/memory-engine/bench/scorecards-engine/agent-coding__pentatonic-baseline__20260427-142523.json +0 -1115
- package/packages/memory-engine/bench/scorecards-engine/chat-recall__pentatonic-baseline__20260427-142648.json +0 -819
- package/packages/memory-engine/bench/scorecards-engine/circular-economy__pentatonic-baseline__20260427-142757.json +0 -1278
- package/packages/memory-engine/bench/scorecards-engine/customer-support__pentatonic-baseline__20260427-142900.json +0 -1018
- package/packages/memory-engine/bench/scorecards-engine/marketplace-ops__pentatonic-baseline__20260427-142957.json +0 -1038
- package/packages/memory-engine/bench/scorecards-engine/product-catalogue__pentatonic-baseline__20260427-143122.json +0 -961
- package/packages/memory-engine/bench/scorecards-engine-via-docker/agent-coding__pentatonic-memory__20260427-161812.json +0 -1115
- package/packages/memory-engine/bench/scorecards-engine-via-docker/chat-recall__pentatonic-memory__20260427-161701.json +0 -819
- package/packages/memory-engine/bench/scorecards-engine-via-docker/circular-economy__pentatonic-memory__20260427-161713.json +0 -1278
- package/packages/memory-engine/bench/scorecards-engine-via-docker/customer-support__pentatonic-memory__20260427-161723.json +0 -1018
- package/packages/memory-engine/bench/scorecards-engine-via-docker/marketplace-ops__pentatonic-memory__20260427-161732.json +0 -1038
- package/packages/memory-engine/bench/scorecards-engine-via-docker/product-catalogue__pentatonic-memory__20260427-161741.json +0 -937
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/agent-coding__pentatonic-memory__20260427-184718.json +0 -1115
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/chat-recall__pentatonic-memory__20260427-184614.json +0 -819
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/circular-economy__pentatonic-memory__20260427-184809.json +0 -1278
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/customer-support__pentatonic-memory__20260427-184854.json +0 -1018
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/marketplace-ops__pentatonic-memory__20260427-184929.json +0 -1038
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/product-catalogue__pentatonic-memory__20260427-185015.json +0 -961
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/agent-coding__pentatonic-memory__20260427-175252.json +0 -1115
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/chat-recall__pentatonic-memory__20260427-175312.json +0 -819
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/circular-economy__pentatonic-memory__20260427-175335.json +0 -1278
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/customer-support__pentatonic-memory__20260427-175355.json +0 -1018
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/marketplace-ops__pentatonic-memory__20260427-175413.json +0 -1038
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/product-catalogue__pentatonic-memory__20260427-175430.json +0 -883
- package/packages/memory-engine/bench/scorecards-engine-via-shim/agent-coding__pentatonic-memory__20260427-155409.json +0 -1115
- package/packages/memory-engine/bench/scorecards-engine-via-shim/chat-recall__pentatonic-memory__20260427-155421.json +0 -819
- package/packages/memory-engine/bench/scorecards-engine-via-shim/circular-economy__pentatonic-memory__20260427-155433.json +0 -1278
- package/packages/memory-engine/bench/scorecards-engine-via-shim/customer-support__pentatonic-memory__20260427-155443.json +0 -1018
- package/packages/memory-engine/bench/scorecards-engine-via-shim/marketplace-ops__pentatonic-memory__20260427-155453.json +0 -1038
- package/packages/memory-engine/bench/scorecards-engine-via-shim/product-catalogue__pentatonic-memory__20260427-155503.json +0 -937
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory-latest__20260427-145103.json +0 -1115
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory__20260427-144909.json +0 -1115
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory-latest__20260427-145153.json +0 -819
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory__20260427-145120.json +0 -542
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory-latest__20260427-145313.json +0 -1278
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory__20260427-145207.json +0 -894
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory-latest__20260427-145412.json +0 -1018
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory__20260427-145327.json +0 -680
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory-latest__20260427-145517.json +0 -1038
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory__20260427-145422.json +0 -693
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory-latest__20260427-145616.json +0 -961
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory__20260427-145528.json +0 -727
- package/packages/memory-engine/compat/Dockerfile +0 -22
- package/packages/memory-engine/compat/server.py +0 -1255
- package/packages/memory-engine/docker-compose.test.yml +0 -59
- package/packages/memory-engine/docker-compose.yml +0 -255
- package/packages/memory-engine/engine/README.md +0 -52
- package/packages/memory-engine/engine/l2-hybridrag-proxy.py +0 -1543
- package/packages/memory-engine/engine/l5-comms-layer.py +0 -663
- package/packages/memory-engine/engine/l6-document-store.py +0 -1018
- package/packages/memory-engine/engine/services/_shared/__init__.py +0 -1
- package/packages/memory-engine/engine/services/_shared/embed_provider.py +0 -562
- package/packages/memory-engine/engine/services/l2/Dockerfile +0 -50
- package/packages/memory-engine/engine/services/l2/init_databases.py +0 -81
- package/packages/memory-engine/engine/services/l2/l2-hybridrag-proxy.py +0 -2721
- package/packages/memory-engine/engine/services/l5/Dockerfile +0 -11
- package/packages/memory-engine/engine/services/l5/l5-comms-layer.py +0 -808
- package/packages/memory-engine/engine/services/l6/Dockerfile +0 -30
- package/packages/memory-engine/engine/services/l6/l6-document-store.py +0 -1221
- package/packages/memory-engine/engine/services/nv-embed/Dockerfile +0 -28
- package/packages/memory-engine/engine/services/nv-embed/server.py +0 -152
- package/packages/memory-engine/pme_memory/__init__.py +0 -0
- package/packages/memory-engine/pme_memory/__main__.py +0 -129
- package/packages/memory-engine/pme_memory/artifacts.py +0 -95
- package/packages/memory-engine/pme_memory/embed.py +0 -74
- package/packages/memory-engine/pme_memory/health.py +0 -36
- package/packages/memory-engine/pme_memory/hygiene.py +0 -159
- package/packages/memory-engine/pme_memory/indexer.py +0 -200
- package/packages/memory-engine/pme_memory/needs.py +0 -55
- package/packages/memory-engine/pme_memory/provenance.py +0 -80
- package/packages/memory-engine/pme_memory/scoring.py +0 -168
- package/packages/memory-engine/pme_memory/search.py +0 -52
- package/packages/memory-engine/pme_memory/store.py +0 -86
- package/packages/memory-engine/pme_memory/synthesis.py +0 -114
- package/packages/memory-engine/pyproject.toml +0 -65
- package/packages/memory-engine/scripts/kg-extractor.py +0 -557
- package/packages/memory-engine/scripts/kg-preflexor-v2.py +0 -738
- package/packages/memory-engine/scripts/wipe-legacy-l3-entities.py +0 -128
- package/packages/memory-engine/tests/e2e_arena.sh +0 -259
- package/packages/memory-engine/tests/embed_stub/Dockerfile +0 -13
- package/packages/memory-engine/tests/embed_stub/server.py +0 -80
- package/packages/memory-engine/tests/test_aggregate.py +0 -333
- package/packages/memory-engine/tests/test_api_contract.sh +0 -57
- package/packages/memory-engine/tests/test_arena_safety.py +0 -232
- package/packages/memory-engine/tests/test_channel_stat_reader.py +0 -437
- package/packages/memory-engine/tests/test_channel_stat_rollups.py +0 -308
- package/packages/memory-engine/tests/test_compat_nv_embed_probe.py +0 -48
- package/packages/memory-engine/tests/test_embed_provider.py +0 -693
- package/packages/memory-engine/tests/test_l2_qmd_vec_search.py +0 -280
- package/packages/memory-engine/tests/test_l3_arena_isolation.py +0 -412
- package/packages/memory-engine/tests/test_l6_module_load.py +0 -84
- 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)
|