general-augment-cli 0.1.0__py3-none-any.whl

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 (42) hide show
  1. general_augment_cli-0.1.0.dist-info/METADATA +180 -0
  2. general_augment_cli-0.1.0.dist-info/RECORD +42 -0
  3. general_augment_cli-0.1.0.dist-info/WHEEL +4 -0
  4. general_augment_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. platform_cli/__init__.py +5 -0
  6. platform_cli/branding.py +27 -0
  7. platform_cli/client.py +179 -0
  8. platform_cli/commands/__init__.py +1 -0
  9. platform_cli/commands/approvals.py +150 -0
  10. platform_cli/commands/auth.py +96 -0
  11. platform_cli/commands/billing.py +143 -0
  12. platform_cli/commands/channels.py +212 -0
  13. platform_cli/commands/deploy.py +72 -0
  14. platform_cli/commands/dev.py +38 -0
  15. platform_cli/commands/doctor.py +170 -0
  16. platform_cli/commands/identity.py +433 -0
  17. platform_cli/commands/init.py +55 -0
  18. platform_cli/commands/integrate.py +94 -0
  19. platform_cli/commands/keys.py +116 -0
  20. platform_cli/commands/logs.py +43 -0
  21. platform_cli/commands/mcp.py +258 -0
  22. platform_cli/commands/memory.py +316 -0
  23. platform_cli/commands/mock.py +30 -0
  24. platform_cli/commands/model_providers.py +226 -0
  25. platform_cli/commands/observability.py +174 -0
  26. platform_cli/commands/onboarding.py +72 -0
  27. platform_cli/commands/projects.py +302 -0
  28. platform_cli/commands/skills.py +116 -0
  29. platform_cli/commands/smoke.py +280 -0
  30. platform_cli/commands/status.py +49 -0
  31. platform_cli/commands/tools.py +179 -0
  32. platform_cli/commands/users.py +150 -0
  33. platform_cli/commands/validate.py +96 -0
  34. platform_cli/commands/verify.py +648 -0
  35. platform_cli/config.py +114 -0
  36. platform_cli/errors.py +103 -0
  37. platform_cli/local_mock.py +1392 -0
  38. platform_cli/main.py +130 -0
  39. platform_cli/openapi.py +1048 -0
  40. platform_cli/output.py +47 -0
  41. platform_cli/readiness.py +176 -0
  42. platform_cli/runtime.py +22 -0
@@ -0,0 +1,648 @@
1
+ """Project verification command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import uuid
7
+ from typing import Any
8
+
9
+ import typer
10
+
11
+ from platform_cli import __version__
12
+ from platform_cli.client import encode_path_segment, resolve_project
13
+ from platform_cli.errors import CLIError
14
+ from platform_cli.output import panel, print_json, table
15
+ from platform_cli.readiness import build_readiness_checklist
16
+ from platform_cli.runtime import Runtime
17
+
18
+
19
+ def verify(
20
+ ctx: typer.Context,
21
+ project: str = typer.Option(..., help="Project id, slug, or name."),
22
+ message: str = typer.Option(
23
+ "Reply with one short sentence confirming this General Augment project works.",
24
+ help="Message for the hosted agent test.",
25
+ ),
26
+ user: str = typer.Option(
27
+ "genaug-verify-user",
28
+ help="Synthetic app user id for memory and agent checks.",
29
+ ),
30
+ phone_e164: str = typer.Option("+15550000000", help="Synthetic E.164 user identity."),
31
+ channel: str = typer.Option("sms", help="Synthetic channel: sms, whatsapp, ios, or telegram."),
32
+ dashboard_url: str = typer.Option(
33
+ os.getenv("GENAUG_DASHBOARD_URL", "https://app.generalaugment.com"),
34
+ help="Dashboard base URL for follow-up UI checks.",
35
+ ),
36
+ json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON."),
37
+ ) -> None:
38
+ """Verify a project through the CLI before checking the dashboard UI."""
39
+ runtime: Runtime = ctx.obj
40
+ with runtime.client() as client:
41
+ payload = build_project_verification_payload(
42
+ client,
43
+ project=project,
44
+ message=message,
45
+ user=user,
46
+ phone_e164=phone_e164,
47
+ channel=channel,
48
+ dashboard_url=dashboard_url,
49
+ )
50
+ if json_output:
51
+ print_json(payload)
52
+ else:
53
+ table(
54
+ f"Project Verify: {payload['project']['slug']}",
55
+ ["Check", "Status", "Detail"],
56
+ [[item["name"], item["status"], item["detail"]] for item in payload["checks"]],
57
+ )
58
+ panel(
59
+ "Dashboard Follow-up",
60
+ "\n".join(f"{key}: {value}" for key, value in payload["dashboard"].items()),
61
+ )
62
+ if payload["verdict"] != "PASS":
63
+ failed = ", ".join(item["name"] for item in payload["checks"] if item["status"] == "FAIL")
64
+ raise CLIError(f"Project verification failed: {failed}")
65
+
66
+
67
+ def build_project_verification_payload(
68
+ client: Any,
69
+ *,
70
+ project: str,
71
+ message: str,
72
+ user: str,
73
+ phone_e164: str,
74
+ channel: str,
75
+ dashboard_url: str,
76
+ ) -> dict[str, Any]:
77
+ """Run project acceptance checks and return a machine-readable payload."""
78
+
79
+ checks: list[dict[str, Any]] = []
80
+ ready = client.public("GET", "/health/ready")
81
+ checks.append(_check("api_ready", _health_ok(ready), _health_detail(ready)))
82
+
83
+ project_payload = resolve_project(client, project)
84
+ project_id = str(project_payload["id"])
85
+ project_slug = str(project_payload.get("slug") or project)
86
+ checks.append(_check("project_resolved", True, project_id))
87
+ identity = client.admin("GET", "/me")
88
+
89
+ keys = client.admin("GET", "/keys")
90
+ key_items = keys.get("items", []) if isinstance(keys, dict) else []
91
+ project_keys = [
92
+ item
93
+ for item in key_items
94
+ if isinstance(item, dict) and str(item.get("project_id") or "") == project_id
95
+ ]
96
+ checks.append(
97
+ _check(
98
+ "project_api_key",
99
+ bool(project_keys),
100
+ _key_detail(project_keys),
101
+ )
102
+ )
103
+ checks.append(
104
+ _project_key_execution_check(
105
+ client,
106
+ project_id=project_id,
107
+ user=user,
108
+ message=message,
109
+ identity=identity,
110
+ )
111
+ )
112
+
113
+ tools_payload = client.admin("GET", "/tools")
114
+ tools = tools_payload if isinstance(tools_payload, list) else tools_payload.get("items", [])
115
+ checks.append(_check("tool_registry", isinstance(tools, list), f"{len(tools)} tools"))
116
+
117
+ runtime_policy = client.admin(
118
+ "GET",
119
+ f"/projects/{encode_path_segment(project_id)}/runtime-policy",
120
+ )
121
+ checks.append(
122
+ _check(
123
+ "runtime_policy_model_routing",
124
+ _model_routing_policy_ok(runtime_policy),
125
+ _model_routing_policy_detail(runtime_policy),
126
+ )
127
+ )
128
+ soul = client.admin("GET", f"/projects/{encode_path_segment(project_id)}/soul")
129
+ checks.append(_check("soul_visible", _soul_visible_ok(soul), _soul_detail(soul)))
130
+
131
+ skills = client.admin(
132
+ "GET",
133
+ f"/projects/{encode_path_segment(project_id)}/skills",
134
+ params={"limit": 100},
135
+ )
136
+ checks.append(
137
+ _check(
138
+ "skills_visible",
139
+ _skills_visible_ok(skills, runtime_policy),
140
+ _skills_detail(skills, runtime_policy),
141
+ )
142
+ )
143
+
144
+ agent_test = client.admin(
145
+ "POST",
146
+ f"/projects/{encode_path_segment(project_id)}/test",
147
+ json={"message": message, "phone_e164": phone_e164, "channel": channel},
148
+ )
149
+ agent_ok = not agent_test.get("error") and bool(
150
+ agent_test.get("response_text") or agent_test.get("response")
151
+ )
152
+ checks.append(
153
+ _check(
154
+ "agent_test",
155
+ agent_ok,
156
+ agent_test.get("error")
157
+ or agent_test.get("details")
158
+ or str(agent_test.get("response_text") or agent_test.get("response") or ""),
159
+ )
160
+ )
161
+
162
+ logs = client.admin(
163
+ "GET",
164
+ f"/projects/{encode_path_segment(project_id)}/logs",
165
+ params={"limit": 5},
166
+ )
167
+ log_items = logs.get("items", []) if isinstance(logs, dict) else []
168
+ checks.append(_check("logs", isinstance(log_items, list), f"{len(log_items)} recent rows"))
169
+
170
+ usage = client.admin("GET", f"/projects/{encode_path_segment(project_id)}/usage")
171
+ totals = usage.get("totals", {}) if isinstance(usage, dict) else {}
172
+ limits = usage.get("limits", {}) if isinstance(usage, dict) else {}
173
+ checks.append(_check("usage", isinstance(totals, dict), _usage_detail(totals)))
174
+ checks.append(_check("usage_limits", isinstance(limits, dict), _limits_detail(limits)))
175
+
176
+ observability = client.admin(
177
+ "GET",
178
+ f"/projects/{encode_path_segment(project_id)}/observability",
179
+ params={"limit": 5},
180
+ )
181
+ traces = observability.get("traces", []) if isinstance(observability, dict) else []
182
+ checks.append(_check("observability", isinstance(traces, list), f"{len(traces)} trace rows"))
183
+
184
+ channel_status = client.admin(
185
+ "GET",
186
+ f"/projects/{encode_path_segment(project_id)}/channels/status",
187
+ )
188
+ channel_items = channel_status.get("channels", []) if isinstance(channel_status, dict) else []
189
+ checks.append(
190
+ _check("channel_status", isinstance(channel_items, list), f"{len(channel_items)} channels")
191
+ )
192
+
193
+ verification_id = uuid.uuid4().hex[:12]
194
+ memory_checks = _run_memory_lifecycle(
195
+ client,
196
+ project_id=project_id,
197
+ user=user,
198
+ verification_id=verification_id,
199
+ )
200
+ checks.extend(memory_checks)
201
+
202
+ audit = client.admin(
203
+ "GET",
204
+ f"/projects/{encode_path_segment(project_id)}/audit/tool-calls",
205
+ params={"limit": 5},
206
+ )
207
+ audit_items = audit.get("items", []) if isinstance(audit, dict) else []
208
+ checks.append(
209
+ _check("tool_call_audit", isinstance(audit_items, list), f"{len(audit_items)} rows")
210
+ )
211
+
212
+ dashboard = _dashboard_links(dashboard_url, project_id)
213
+ return {
214
+ "cli": {"version": __version__},
215
+ "api": _api_version_detail(ready),
216
+ "auth": _auth_detail(identity),
217
+ "project": {
218
+ "id": project_id,
219
+ "slug": project_slug,
220
+ "name": project_payload.get("name"),
221
+ },
222
+ "verdict": _verdict(checks),
223
+ "checks": checks,
224
+ "readiness_checklist": build_readiness_checklist(checks, project=project_payload),
225
+ "runtime_policy": _runtime_policy_artifact(runtime_policy),
226
+ "dashboard": dashboard,
227
+ }
228
+
229
+
230
+ def _check(name: str, passed: bool, detail: str) -> dict[str, str]:
231
+ """Build a machine-readable check row."""
232
+
233
+ return {"name": name, "status": "PASS" if passed else "FAIL", "detail": detail}
234
+
235
+
236
+ def _skip(name: str, detail: str) -> dict[str, str]:
237
+ """Build a machine-readable skipped check row."""
238
+
239
+ return {"name": name, "status": "SKIP", "detail": detail}
240
+
241
+
242
+ def _verdict(checks: list[dict[str, str]]) -> str:
243
+ """Return FAIL only when a required check failed."""
244
+
245
+ return "FAIL" if any(item["status"] == "FAIL" for item in checks) else "PASS"
246
+
247
+
248
+ def _health_ok(payload: object) -> bool:
249
+ """Return whether a health payload is ready."""
250
+
251
+ return isinstance(payload, dict) and payload.get("status") == "ok"
252
+
253
+
254
+ def _health_detail(payload: object) -> str:
255
+ """Return compact health detail."""
256
+
257
+ if not isinstance(payload, dict):
258
+ return str(payload)
259
+ dependencies = [
260
+ f"{key}={payload[key]}"
261
+ for key in ("db", "redis")
262
+ if key in payload and payload[key] is not None
263
+ ]
264
+ return ", ".join(dependencies) or str(payload.get("status"))
265
+
266
+
267
+ def _api_version_detail(payload: object) -> dict[str, str]:
268
+ """Return API build/version metadata for automation artifacts."""
269
+
270
+ if not isinstance(payload, dict):
271
+ return {}
272
+ return {
273
+ key: str(payload[key])
274
+ for key in ("version", "build_sha", "status")
275
+ if key in payload and payload[key] is not None
276
+ }
277
+
278
+
279
+ def _auth_detail(payload: object) -> dict[str, str]:
280
+ """Return safe auth metadata for automation artifacts."""
281
+
282
+ if not isinstance(payload, dict):
283
+ return {}
284
+ detail = {
285
+ key: str(payload[key])
286
+ for key in ("auth_method", "project_id")
287
+ if key in payload and payload[key] is not None
288
+ }
289
+ project_ids = _identity_project_ids(payload)
290
+ if project_ids:
291
+ detail["project_ids"] = ",".join(project_ids)
292
+ return detail
293
+
294
+
295
+ def _project_key_execution_check(
296
+ client: Any,
297
+ *,
298
+ project_id: str,
299
+ user: str,
300
+ message: str,
301
+ identity: object,
302
+ ) -> dict[str, str]:
303
+ """Exercise `/v1/responses` when the configured CLI key is project-scoped."""
304
+
305
+ if not _identity_is_project_scoped_to(identity, project_id):
306
+ return _skip(
307
+ "project_key_execution",
308
+ (
309
+ "configured CLI key is not project-scoped to this project; "
310
+ "project key existence was checked, but project-key execution was not"
311
+ ),
312
+ )
313
+ response = client.app(
314
+ "POST",
315
+ "/v1/responses",
316
+ json={
317
+ "model": "balanced",
318
+ "user": user,
319
+ "input": message,
320
+ "metadata": {
321
+ "source": "genaug-cli-verify",
322
+ "feature": "project_key_execution",
323
+ },
324
+ },
325
+ )
326
+ if not isinstance(response, dict):
327
+ return _check("project_key_execution", False, str(response))
328
+ response_id = str(response.get("id") or "")
329
+ status = str(response.get("status") or "")
330
+ text = _response_text(response)
331
+ passed = bool(response_id) and status in {"completed", "complete", ""}
332
+ return _check(
333
+ "project_key_execution",
334
+ passed,
335
+ response_id or text or status or "missing response id",
336
+ )
337
+
338
+
339
+ def _identity_is_project_scoped_to(identity: object, project_id: str) -> bool:
340
+ """Return whether the configured credential itself is scoped to this project."""
341
+
342
+ if not isinstance(identity, dict):
343
+ return False
344
+ return str(identity.get("project_id") or "") == project_id
345
+
346
+
347
+ def _identity_project_ids(identity: dict[str, Any]) -> list[str]:
348
+ """Return safe project id strings from an identity payload."""
349
+
350
+ raw_project_ids = identity.get("project_ids")
351
+ if isinstance(raw_project_ids, list) and raw_project_ids:
352
+ return [str(project_id) for project_id in raw_project_ids]
353
+ if identity.get("project_id"):
354
+ return [str(identity["project_id"])]
355
+ return []
356
+
357
+
358
+ def _response_text(response: dict[str, Any]) -> str:
359
+ """Return compact text from a Responses-compatible payload."""
360
+
361
+ if response.get("output_text"):
362
+ return str(response["output_text"])
363
+ output = response.get("output")
364
+ if not isinstance(output, list):
365
+ return ""
366
+ texts: list[str] = []
367
+ for item in output:
368
+ if not isinstance(item, dict):
369
+ continue
370
+ content = item.get("content")
371
+ if not isinstance(content, list):
372
+ continue
373
+ for block in content:
374
+ if isinstance(block, dict) and block.get("type") == "output_text":
375
+ texts.append(str(block.get("text") or ""))
376
+ return "\n".join(texts)
377
+
378
+
379
+ def _usage_detail(totals: dict[str, Any]) -> str:
380
+ """Return compact usage detail."""
381
+
382
+ turns = totals.get("agent_turns_count", 0)
383
+ cost = totals.get("total_cost_usd", 0)
384
+ return f"agent_turns={turns}, cost_usd={cost}"
385
+
386
+
387
+ def _key_detail(items: list[dict[str, Any]]) -> str:
388
+ """Return a compact project-key readiness summary."""
389
+
390
+ if not items:
391
+ return "no project-scoped API keys found"
392
+ names = ", ".join(str(item.get("name") or item.get("id") or "key") for item in items[:3])
393
+ return f"{len(items)} project key(s): {names}"
394
+
395
+
396
+ def _limits_detail(limits: dict[str, Any]) -> str:
397
+ """Return compact usage-limit detail."""
398
+
399
+ turns = limits.get("agent_turns_per_day", "unknown")
400
+ tokens = limits.get("tokens_per_day", "unknown")
401
+ over_limit = limits.get("over_limit", False)
402
+ return f"agent_turns_per_day={turns}, tokens_per_day={tokens}, over_limit={over_limit}"
403
+
404
+
405
+ def _model_routing_policy_ok(payload: object) -> bool:
406
+ """Return whether runtime policy exposes usable tenant model routing."""
407
+
408
+ if not isinstance(payload, dict):
409
+ return False
410
+ routing = payload.get("model_routing")
411
+ if not isinstance(routing, dict):
412
+ return False
413
+ tiers = routing.get("tiers")
414
+ required_tiers = ("simple", "balanced", "complex")
415
+ return (
416
+ routing.get("mode") == "tiered_complexity"
417
+ and routing.get("default_tier") == "balanced"
418
+ and routing.get("channel_parity") is True
419
+ and isinstance(tiers, dict)
420
+ and all(str(tiers.get(tier) or "") for tier in required_tiers)
421
+ )
422
+
423
+
424
+ def _model_routing_policy_detail(payload: object) -> str:
425
+ """Return a compact model-routing summary for verify output."""
426
+
427
+ if not isinstance(payload, dict):
428
+ return "runtime policy response was not an object"
429
+ routing = payload.get("model_routing")
430
+ if not isinstance(routing, dict):
431
+ return "runtime policy did not include model_routing"
432
+ tiers = routing.get("tiers")
433
+ if not isinstance(tiers, dict):
434
+ return "model_routing.tiers missing"
435
+ simple = str(tiers.get("simple") or "missing")
436
+ balanced = str(tiers.get("balanced") or "missing")
437
+ complex_model = str(tiers.get("complex") or "missing")
438
+ mode = str(routing.get("mode") or "missing")
439
+ default_tier = str(routing.get("default_tier") or "missing")
440
+ parity = routing.get("channel_parity")
441
+ return (
442
+ f"mode={mode}, simple={simple}, balanced={balanced}, "
443
+ f"complex={complex_model}, default_tier={default_tier}, channel_parity={parity}"
444
+ )
445
+
446
+
447
+ def _soul_visible_ok(payload: object) -> bool:
448
+ """Return whether SOUL content is visible through the admin API."""
449
+
450
+ return isinstance(payload, dict) and bool(str(payload.get("content") or "").strip())
451
+
452
+
453
+ def _soul_detail(payload: object) -> str:
454
+ """Return compact SOUL visibility detail."""
455
+
456
+ if not isinstance(payload, dict):
457
+ return "SOUL response was not an object"
458
+ content = str(payload.get("content") or "")
459
+ if not content.strip():
460
+ return "SOUL content missing"
461
+ return f"{len(content)} chars"
462
+
463
+
464
+ def _skills_visible_ok(payload: object, runtime_policy: object) -> bool:
465
+ """Return whether tenant skills are visible and match runtime-policy names."""
466
+
467
+ items = _skill_items(payload)
468
+ if items is None:
469
+ return False
470
+ listed_names = _skill_names_from_items(items)
471
+ runtime_names = set(_runtime_policy_skill_names(runtime_policy))
472
+ return runtime_names.issubset(listed_names)
473
+
474
+
475
+ def _skills_detail(payload: object, runtime_policy: object) -> str:
476
+ """Return compact skill visibility detail."""
477
+
478
+ items = _skill_items(payload)
479
+ if items is None:
480
+ return "skills response did not include an items list"
481
+ listed_names = _skill_names_from_items(items)
482
+ runtime_names = set(_runtime_policy_skill_names(runtime_policy))
483
+ missing = sorted(runtime_names - listed_names)
484
+ if missing:
485
+ return f"{len(listed_names)} listed, missing runtime policy skills: {', '.join(missing)}"
486
+ return f"{len(listed_names)} listed"
487
+
488
+
489
+ def _skill_items(payload: object) -> list[object] | None:
490
+ """Return the skill list from an admin API response."""
491
+
492
+ if not isinstance(payload, dict):
493
+ return None
494
+ items = payload.get("items")
495
+ return items if isinstance(items, list) else None
496
+
497
+
498
+ def _skill_names_from_items(items: list[object]) -> set[str]:
499
+ """Return skill names from list response rows."""
500
+
501
+ names: set[str] = set()
502
+ for item in items:
503
+ if isinstance(item, dict) and item.get("name"):
504
+ names.add(str(item["name"]))
505
+ return names
506
+
507
+
508
+ def _runtime_policy_skill_names(payload: object) -> list[str]:
509
+ """Return runtime-policy skill names."""
510
+
511
+ if not isinstance(payload, dict):
512
+ return []
513
+ skills = payload.get("skills")
514
+ if not isinstance(skills, dict):
515
+ return []
516
+ names = skills.get("names")
517
+ if not isinstance(names, list):
518
+ return []
519
+ return [str(name) for name in names if str(name)]
520
+
521
+
522
+ def _runtime_policy_artifact(payload: object) -> dict[str, Any]:
523
+ """Return the secret-free runtime policy fields useful in verify artifacts."""
524
+
525
+ if not isinstance(payload, dict):
526
+ return {}
527
+ artifact: dict[str, Any] = {}
528
+ for key in (
529
+ "project_id",
530
+ "model_routing",
531
+ "tool_discovery",
532
+ "hermes_exposure",
533
+ "platform_tools",
534
+ "mcp",
535
+ "skills",
536
+ ):
537
+ value = payload.get(key)
538
+ if value is not None:
539
+ artifact[key] = value
540
+ return artifact
541
+
542
+
543
+ def _dashboard_links(dashboard_url: str, project_id: str) -> dict[str, str]:
544
+ """Build dashboard URLs for UI follow-up checks."""
545
+
546
+ base = dashboard_url.rstrip("/")
547
+ encoded = encode_path_segment(project_id)
548
+ project_root = f"{base}/dashboard/projects/{encoded}"
549
+ return {
550
+ "project": project_root,
551
+ "integrate": f"{project_root}/integrate",
552
+ "tools": f"{project_root}/tools",
553
+ "observability": f"{project_root}/observability",
554
+ "analytics": f"{project_root}/analytics",
555
+ }
556
+
557
+
558
+ def _run_memory_lifecycle(
559
+ client: Any,
560
+ *,
561
+ project_id: str,
562
+ user: str,
563
+ verification_id: str,
564
+ ) -> list[dict[str, str]]:
565
+ """Store, search, profile, and delete one synthetic memory fact."""
566
+
567
+ headers = {"X-Project-ID": project_id}
568
+ fact = "CLI verification user prefers concise onboarding notes."
569
+ checks: list[dict[str, str]] = []
570
+
571
+ stored = client.app(
572
+ "POST",
573
+ "/api/v1/agent/memory/store",
574
+ json={
575
+ "user_id": user,
576
+ "fact": fact,
577
+ "fact_type": "preference",
578
+ "importance_score": 0.8,
579
+ "source": "genaug-cli-verify",
580
+ "metadata": {"scenario": "project-verify", "verification_id": verification_id},
581
+ "idempotency_key": f"genaug-verify-{project_id}-{user}-{verification_id}",
582
+ },
583
+ headers=headers,
584
+ )
585
+ memory_id = str(stored.get("memory_id") or stored.get("id") or "")
586
+ checks.append(_check("memory_store", bool(memory_id), memory_id or "missing memory_id"))
587
+
588
+ search = client.app(
589
+ "POST",
590
+ "/api/v1/agent/memory/search",
591
+ json={
592
+ "user_id": user,
593
+ "query": "concise onboarding notes",
594
+ "limit": 5,
595
+ "min_similarity": 0,
596
+ "fact_type": "preference",
597
+ "min_importance": 0.5,
598
+ "source": "genaug-cli-verify",
599
+ },
600
+ headers=headers,
601
+ )
602
+ facts = search.get("facts", []) if isinstance(search, dict) else []
603
+ found_memory = _memory_hit_found(facts, memory_id)
604
+ checks.append(_check("memory_search", found_memory, f"{len(facts)} facts"))
605
+
606
+ profile = client.app(
607
+ "GET",
608
+ f"/api/v1/agent/memory/profile/{encode_path_segment(user)}",
609
+ headers=headers,
610
+ )
611
+ total_facts = profile.get("total_facts") if isinstance(profile, dict) else None
612
+ checks.append(
613
+ _check("memory_profile", isinstance(total_facts, int), f"total_facts={total_facts}")
614
+ )
615
+
616
+ if memory_id:
617
+ deleted = client.app(
618
+ "DELETE",
619
+ f"/api/v1/agent/memory/{encode_path_segment(memory_id)}",
620
+ params={"user_id": user},
621
+ headers=headers,
622
+ )
623
+ deleted_count = deleted.get("deleted_count") if isinstance(deleted, dict) else None
624
+ checks.append(
625
+ _check(
626
+ "memory_delete",
627
+ isinstance(deleted_count, int) and deleted_count >= 1,
628
+ f"deleted_count={deleted_count}",
629
+ )
630
+ )
631
+ else:
632
+ checks.append(_check("memory_delete", False, "skipped because memory_store failed"))
633
+ return checks
634
+
635
+
636
+ def _memory_hit_found(facts: object, memory_id: str) -> bool:
637
+ """Return whether a memory search result includes the stored memory."""
638
+
639
+ if not isinstance(facts, list) or not facts:
640
+ return False
641
+ if not memory_id:
642
+ return True
643
+ for fact in facts:
644
+ if not isinstance(fact, dict):
645
+ continue
646
+ if str(fact.get("memory_id") or fact.get("id") or "") == memory_id:
647
+ return True
648
+ return False