forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjit_ledger_user_write_emulator_test.py
More file actions
379 lines (338 loc) · 17.1 KB
/
Copy pathjit_ledger_user_write_emulator_test.py
File metadata and controls
379 lines (338 loc) · 17.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
#!/usr/bin/env python3
"""Exercise authenticated memory writes against the real Firestore emulator.
This is intentionally an API-to-apply proof. It keeps the explicit user
memory route, the batch route, and the evidence-only import route separate so
the ledger cutover cannot be proven by calling the apply adapter in isolation.
"""
from __future__ import annotations
# ruff: noqa: E402 -- emulator safety/env bootstrapping must precede backend imports.
import os
import sys
from pathlib import Path
from types import SimpleNamespace
from typing import Any
PROJECT_ID = os.environ.setdefault("GOOGLE_CLOUD_PROJECT", os.environ.get("GCLOUD_PROJECT", "demo-memory"))
os.environ.setdefault("GCLOUD_PROJECT", PROJECT_ID)
os.environ.setdefault("ENCRYPTION_SECRET", "omi_jit_user_write_emulator_key_32_bytes")
os.environ.setdefault("MEMORY_ENABLED", "on")
os.environ.setdefault("MEMORY_MODE", "read")
os.environ.setdefault("PROVIDER_MODE", "offline")
os.environ.setdefault("GOOGLE_AUTH_DISABLE_GCE_CHECK", "true")
os.environ.setdefault("GCE_METADATA_HOST", "127.0.0.1:9")
os.environ.setdefault("MEMORY_BELIEF_MODEL_ENABLED", "false")
BACKEND_DIR = Path(__file__).resolve().parents[1]
if str(BACKEND_DIR) not in sys.path:
sys.path.insert(0, str(BACKEND_DIR))
from fastapi import FastAPI
from fastapi.testclient import TestClient
from google.cloud import firestore
from database.memory_collections import MemoryCollections
from models.memory_apply import MemoryControlState, WriterMode
from utils.jit_rollout import JIT_ADMISSION_ALLOWLIST
UID = sorted(JIT_ADMISSION_ALLOWLIST)[0]
INITIAL_HEAD = "jit-user-write-emulator-head"
MANUAL_CONTENT = "[emulator] user explicitly prefers morning meetings"
REJECTED_CONTENT = "[emulator] user rejects the afternoon meeting claim"
EXTERNAL_CONTENT = "[emulator] connector observed a morning meeting"
IMPORT_EXTERNAL_ID = "jit-user-write-import-artifact"
def _assert_emulator_only() -> None:
host = (os.environ.get("FIRESTORE_EMULATOR_HOST") or "").strip()
if not host:
raise RuntimeError("FIRESTORE_EMULATOR_HOST is required; run through Firebase emulators:exec")
hostname = host.rsplit(":", 1)[0].strip("[]").lower()
if hostname not in {"127.0.0.1", "localhost", "::1"}:
raise RuntimeError(f"refusing non-loopback Firestore emulator host: {hostname}")
if not PROJECT_ID.startswith("demo-"):
raise RuntimeError(f"refusing non-demo Firestore project: {PROJECT_ID}")
def _docs(db_client: Any, collection_path: str) -> list[dict[str, Any]]:
return [snapshot.to_dict() or {} for snapshot in db_client.collection(collection_path).stream()]
def _clear_user(db_client: Any, collections: MemoryCollections) -> None:
for path in collections.all_collection_paths():
for snapshot in db_client.collection(path).stream():
snapshot.reference.delete()
db_client.document(collections.user_root).delete()
def _seed_ledger_control(db_client: Any, collections: MemoryCollections) -> None:
control = MemoryControlState(
uid=UID,
head_commit_id=INITIAL_HEAD,
account_generation=7,
source_generation=11,
writer_mode=WriterMode.ledger,
writer_epoch=1,
)
db_client.document(collections.memory_apply_control_state).set(control.model_dump(mode="json"))
def _app() -> FastAPI:
# Import after the emulator-only environment is established. The route's
# rate-limit dependency still resolves, but its authenticated UID is
# supplied by this test's FastAPI dependency override.
from routers import memories
from utils.other import endpoints as auth
app = FastAPI()
app.include_router(memories.router)
app.dependency_overrides[auth.get_current_user_uid] = lambda: UID
return app
def _expect(failures: list[str], condition: bool, message: str) -> None:
if not condition:
failures.append(message)
def main() -> int:
_assert_emulator_only()
db_client: Any = firestore.Client(project=PROJECT_ID)
collections = MemoryCollections(uid=UID)
failures: list[str] = []
_clear_user(db_client, collections)
_seed_ledger_control(db_client, collections)
try:
with TestClient(_app(), raise_server_exceptions=False) as client:
# A manual POST is an authenticated explicit user assertion. In
# ledger mode it must reach save_fact/canonical apply and return a
# durable fact rather than a pending required-processing row.
single = client.post(
"/v3/memories",
json={"content": MANUAL_CONTENT, "category": "manual"},
)
_expect(failures, single.status_code == 200, f"manual POST status={single.status_code}")
items_after_single = _docs(db_client, collections.memory_items)
_expect(failures, len(items_after_single) == 1, "manual POST did not materialize exactly one item")
if items_after_single:
item = items_after_single[0]
_expect(
failures,
item.get("ledger_schema_version") == "knowledge_ledger.v1",
"manual POST item is not a knowledge-ledger row",
)
_expect(failures, item.get("kind") == "fact", "manual POST item is not a fact")
_expect(
failures,
item.get("write_reason") == "direct_user_statement",
"manual POST item lacks direct-user write reason",
)
_expect(failures, item.get("processing_state") == "processed", "manual POST remained pending")
operations_after_single = _docs(db_client, collections.memory_operations)
_expect(
failures,
len(operations_after_single) == 1
and operations_after_single[0].get("operation_type") == "ledger_mutation",
"manual POST did not use one ledger mutation operation",
)
# A retry after the account head advances still resolves to the
# active row for the same request identity. It must not create a
# second fact merely because the first operation's response was
# lost.
retry = client.post(
"/v3/memories",
json={"content": MANUAL_CONTENT, "category": "manual"},
)
_expect(failures, retry.status_code == 200, f"active retry status={retry.status_code}")
_expect(
failures,
len(_docs(db_client, collections.memory_items)) == 1,
"active retry created a duplicate ledger row",
)
# A closed row is retained as history. A retry with its old
# content-derived identity must fail honestly rather than
# resurrecting that row. A distinct explicit action identity can
# still save the same text as a new user assertion.
from utils.memory.canonical_memory_adapter import (
close_canonical_ledger_item,
mint_direct_user_write_authority,
update_canonical_memory_review,
)
from utils.memory.knowledge_ledger import LedgerProvenance, save_fact
from models.product_memory import LedgerWriteReason
closed_item_id = items_after_single[0].get("memory_id") or items_after_single[0].get("id")
if isinstance(closed_item_id, str):
close_canonical_ledger_item(UID, closed_item_id, db_client=db_client)
closed_retry = client.post(
"/v3/memories",
json={"content": MANUAL_CONTENT, "category": "manual"},
)
_expect(
failures,
closed_retry.status_code == 503,
f"closed same-content retry should fail closed status={closed_retry.status_code}",
)
_expect(
failures,
len(_docs(db_client, collections.memory_items)) == 1,
"closed same-content retry resurrected or duplicated a row",
)
distinct_intent_id = save_fact(
UID,
MANUAL_CONTENT,
provenance=LedgerProvenance(
source_id="v3_manual:explicit-second-intent",
source_type="explicit_user_statement",
source_version="v3_memory_create.v1",
action_id="v3_manual:explicit-second-intent",
),
write_reason=LedgerWriteReason.direct_user_statement,
db_client=db_client,
_direct_user_authority=mint_direct_user_write_authority(),
)
_expect(
failures,
distinct_intent_id != closed_item_id,
"distinct explicit same-content intent reused the closed row id",
)
# Rejection is an active audit row, so status-only duplicate
# handling is insufficient. Retrying the old identity must not
# return the rejected row as if it were a successful current fact.
rejected = client.post(
"/v3/memories",
json={"content": REJECTED_CONTENT, "category": "manual"},
)
_expect(failures, rejected.status_code == 200, f"rejection fixture status={rejected.status_code}")
rejected_items = [
item for item in _docs(db_client, collections.memory_items) if item.get("content") == REJECTED_CONTENT
]
if rejected_items:
rejected_id = rejected_items[0].get("memory_id") or rejected_items[0].get("id")
if isinstance(rejected_id, str):
update_canonical_memory_review(UID, rejected_id, False, db_client=db_client)
rejected_retry = client.post(
"/v3/memories",
json={"content": REJECTED_CONTENT, "category": "manual"},
)
_expect(
failures,
rejected_retry.status_code == 503,
f"rejected same-content retry should fail closed status={rejected_retry.status_code}",
)
# A kill-switch or unavailable/unknown JIT result never grants the
# direct ledger seam. Under a ledger writer the compatibility
# fallback consequently fails closed without creating a row.
from utils.memory import memory_service as memory_service_module
original_resolver = memory_service_module.resolve_jit_rollout_sync
try:
for denial in ("kill_switch", "unknown_authority"):
memory_service_module.resolve_jit_rollout_sync = lambda *_args, **_kwargs: SimpleNamespace(
permits_work=False
)
before_denial_items = len(_docs(db_client, collections.memory_items))
denied = client.post(
"/v3/memories",
json={"content": f"[emulator] denied {denial}", "category": "manual"},
)
_expect(
failures, denied.status_code == 503, f"{denial} should fail closed status={denied.status_code}"
)
_expect(
failures,
len(_docs(db_client, collections.memory_items)) == before_denial_items,
f"{denial} created a ledger row",
)
finally:
memory_service_module.resolve_jit_rollout_sync = original_resolver
# A manual-only batch has the same explicit-user contract and must
# remain retry-stable. The mixed batch is checked before writes so
# an external connector item cannot silently gain ledger authority
# by sharing a request with a manual item.
manual_batch = client.post(
"/v3/memories/batch",
json={"memories": [{"content": "[emulator] user prefers concise agendas", "category": "manual"}]},
)
_expect(failures, manual_batch.status_code == 200, f"manual batch status={manual_batch.status_code}")
# Ledger batch validation runs before any commit. Whitespace-only
# content is accepted by the API model but rejected by LedgerWrite;
# placing it second catches the old valid-first/invalid-later
# partial-commit failure.
before_invalid_batch = len(_docs(db_client, collections.memory_items))
invalid_batch = client.post(
"/v3/memories/batch",
json={
"memories": [
{"content": "[emulator] valid batch item", "category": "manual"},
{"content": " ", "category": "manual"},
]
},
)
_expect(
failures,
invalid_batch.status_code == 503,
f"invalid direct batch should fail closed status={invalid_batch.status_code}",
)
_expect(
failures,
len(_docs(db_client, collections.memory_items)) == before_invalid_batch,
"invalid direct batch partially committed its valid first item",
)
# A forged non-capability value cannot select the direct writer or
# reach canonical apply, even when the caller supplies ledger data.
before_forged_authority = len(_docs(db_client, collections.memory_items))
try:
save_fact(
UID,
"[emulator] forged authority must not write",
provenance=LedgerProvenance(
source_id="forged-authority",
source_type="explicit_user_statement",
source_version="v3_memory_create.v1",
action_id="forged-authority",
),
write_reason=LedgerWriteReason.direct_user_statement,
db_client=db_client,
_direct_user_authority=object(),
)
except ValueError:
pass
else:
failures.append("forged direct-user authority was accepted")
_expect(
failures,
len(_docs(db_client, collections.memory_items)) == before_forged_authority,
"forged direct-user authority committed a ledger row",
)
item_count_before_mixed = len(_docs(db_client, collections.memory_items))
mixed_batch = client.post(
"/v3/memories/batch",
json={
"memories": [
{"content": "[emulator] user prefers concise agendas", "category": "manual"},
{"content": EXTERNAL_CONTENT, "category": "interesting"},
]
},
)
_expect(
failures,
mixed_batch.status_code == 503,
f"ledger mixed batch should fail closed status={mixed_batch.status_code}",
)
_expect(
failures,
len(_docs(db_client, collections.memory_items)) == item_count_before_mixed,
"ledger mixed batch partially committed before rejecting connector input",
)
# Imports are evidence ingress. They must remain accepted in
# ledger mode while creating no product memory or ledger row.
before_import_items = len(_docs(db_client, collections.memory_items))
import_response = client.post(
"/v3/memory-imports/batch",
json={
"source_type": "local_files",
"import_run_id": "jit-user-write-import-run",
"items": [{"external_id": IMPORT_EXTERNAL_ID, "title": "[emulator] imported title"}],
},
)
_expect(
failures, import_response.status_code == 200, f"evidence import status={import_response.status_code}"
)
if import_response.status_code == 200:
payload = import_response.json()
_expect(failures, payload.get("artifacts_created") == 1, "evidence import did not create one artifact")
_expect(failures, payload.get("candidates_created") == 0, "evidence import created candidates")
_expect(
failures,
len(_docs(db_client, collections.memory_items)) == before_import_items,
"evidence import created a product memory",
)
finally:
_clear_user(db_client, collections)
if failures:
print("FAIL: JIT ledger direct-user API emulator proof")
for failure in failures:
print(f"- {failure}")
return 1
print("PASS: JIT ledger direct-user POST, batch fencing, and evidence-import emulator proof")
return 0
if __name__ == "__main__":
raise SystemExit(main())