failproofai 1.0.12-beta.1 → 1.0.12

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 (236) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +3 -3
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  30. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +3 -3
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  36. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  37. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  38. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  41. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  42. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  55. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__167mipqmjprq5._.js +3 -0
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0b78b0j8iyv6l._.js → [root-of-the-server]__1vofkb2oq7p7e._.js} +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/{_1a3tubds7s7ow._.js → _0ouu886szh6yj._.js} +4 -4
  75. package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/_1d98fyfrcaqhy._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1i8oid1gc7s1f.js → node_modules_html-to-image_es_index_0tqc05n832v7a.js} +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589gp4p60j._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.js +1 -1
  90. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  91. package/.next/standalone/.next/server/pages/404.html +1 -1
  92. package/.next/standalone/.next/server/pages/500.html +1 -1
  93. package/.next/standalone/.next/server/preview-props.json +3 -3
  94. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  95. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  96. package/.next/standalone/.next/static/chunks/{1b32-afqacaen.js → 0el9n3h9_zp5m.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/0k7g_8x3o6gd-.js +1 -0
  98. package/.next/standalone/.next/static/chunks/{42e8zab314654.js → 0umzct31duk5g.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{24w3x2da9sk49.js → 0wobqgosirs8p.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/12rug3jilwqzs.js +1 -0
  101. package/.next/standalone/.next/static/chunks/{2-tf6_7u623kd.js → 1kduj74-715jh.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/2701amhrbsbot.js +1 -0
  103. package/.next/standalone/.next/static/chunks/{18433qz-8njv3.js → 27cavoc8paelg.js} +1 -1
  104. package/.next/standalone/.next/static/chunks/{0_zfad4co-t2z.js → 2ve23a7keupjw.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/3bv6xaoitjpnh.js +6 -0
  106. package/.next/standalone/app/actions/update-jev-config.ts +5 -3
  107. package/.next/standalone/app/settings/jev-panel.tsx +1 -1
  108. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +28 -18
  109. package/.next/standalone/fp-cloud-cli/README.md +127 -9
  110. package/.next/standalone/fp-cloud-cli/fp_cli/app.py +15 -1
  111. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +199 -34
  112. package/.next/standalone/fp-cloud-cli/fp_cli/cloud_output.py +523 -0
  113. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_cloud.py +45 -0
  114. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +3 -1
  115. package/.next/standalone/fp-cloud-cli/fp_cli/commands/account_cmds.py +171 -0
  116. package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +80 -7
  117. package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +23 -0
  118. package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +2 -0
  119. package/.next/standalone/fp-cloud-cli/fp_cli/commands/contracts_cmds.py +67 -0
  120. package/.next/standalone/fp-cloud-cli/fp_cli/commands/dashboards_cmds.py +159 -0
  121. package/.next/standalone/fp-cloud-cli/fp_cli/commands/evaluators_cmds.py +380 -0
  122. package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +14 -1
  123. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +12 -216
  124. package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +82 -6
  125. package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +4 -1
  126. package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +17 -0
  127. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +143 -1
  128. package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +70 -0
  129. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sentiment_cmds.py +49 -0
  130. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +95 -1
  131. package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +2 -0
  132. package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +21 -1
  133. package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +14 -0
  134. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +7 -53
  135. package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +3 -0
  136. package/.next/standalone/fp-cloud-cli/fp_cli/evaluator_spec.py +266 -0
  137. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +189 -152
  138. package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +21 -24
  139. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +124 -23
  140. package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +2 -1
  141. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +173 -22
  142. package/.next/standalone/fp-cloud-cli/tests/fake_cloud_smoke.py +312 -0
  143. package/.next/standalone/fp-cloud-cli/tests/test_cloud_account.py +125 -0
  144. package/.next/standalone/fp-cloud-cli/tests/test_cloud_agent.py +253 -0
  145. package/.next/standalone/fp-cloud-cli/tests/test_cloud_human_views.py +287 -0
  146. package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +32 -197
  147. package/.next/standalone/fp-cloud-cli/tests/test_cloud_observability.py +116 -0
  148. package/.next/standalone/fp-cloud-cli/tests/test_cloud_transport.py +141 -0
  149. package/.next/standalone/fp-cloud-cli/tests/test_commands.py +3 -3
  150. package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +3 -3
  151. package/.next/standalone/fp-cloud-cli/tests/test_evaluator_authoring.py +449 -0
  152. package/.next/standalone/fp-cloud-cli/tests/test_facets.py +1 -1
  153. package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +15 -0
  154. package/.next/standalone/fp-cloud-cli/tests/test_operator.py +1 -1
  155. package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +1 -1
  156. package/.next/standalone/fp-cloud-cli/tests/test_output.py +37 -37
  157. package/.next/standalone/fp-cloud-cli/tests/test_round4_behaviour.py +144 -0
  158. package/.next/standalone/fp-cloud-cli/tests/test_screen_language.py +80 -0
  159. package/.next/standalone/fp-cloud-cli/tests/test_session_reevaluate.py +33 -0
  160. package/.next/standalone/fp-cloud-cli/tests/test_staging_parity.py +198 -0
  161. package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +1 -1
  162. package/.next/standalone/hermes-plugin/__init__.py +11 -0
  163. package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
  164. package/.next/standalone/package.json +11 -10
  165. package/.next/standalone/sdk/python/uv.lock +31 -8
  166. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +68 -176
  167. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +2 -2
  168. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -4
  169. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +1 -1
  170. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +7 -7
  171. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +1 -1
  172. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +7 -7
  173. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +1 -1
  174. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +98 -115
  175. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +3 -3
  176. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +4 -4
  177. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +1 -1
  178. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -4
  179. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +1 -1
  180. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +149 -171
  181. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +2 -2
  182. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +4 -4
  183. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +1 -1
  184. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +25 -25
  185. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +1 -1
  186. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +7 -7
  187. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +1 -1
  188. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +10 -10
  189. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +1 -1
  190. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +7 -7
  191. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +1 -1
  192. package/.next/standalone/server.js +1 -1
  193. package/dist/cli.mjs +1760 -638
  194. package/dist/worker.mjs +426 -76
  195. package/hermes-plugin/__init__.py +11 -0
  196. package/hermes-plugin/plugin.yaml +1 -1
  197. package/package.json +11 -10
  198. package/src/audit/open-browser.ts +24 -7
  199. package/src/hooks/agent-roster.ts +9 -1
  200. package/src/hooks/agent-skill.ts +206 -0
  201. package/src/hooks/cloud-connection.ts +27 -51
  202. package/src/hooks/cloud-introspect.ts +5 -2
  203. package/src/hooks/cloud-login.ts +248 -0
  204. package/src/hooks/cloud-managed-policies.ts +54 -4
  205. package/src/hooks/codex-system-hooks.ts +10 -4
  206. package/src/hooks/config-status.ts +9 -3
  207. package/src/hooks/configure-wizard.ts +356 -32
  208. package/src/hooks/delivery-health.ts +47 -2
  209. package/src/hooks/effective-reviewers.ts +14 -0
  210. package/src/hooks/fp-config.ts +19 -0
  211. package/src/hooks/fp-home.ts +1 -1
  212. package/src/hooks/handler.ts +43 -5
  213. package/src/hooks/hook-activity-store.ts +7 -1
  214. package/src/hooks/integrations.ts +18 -3
  215. package/src/hooks/jev-activity.ts +117 -1
  216. package/src/hooks/jev-cli.ts +7 -9
  217. package/src/hooks/manager.ts +80 -46
  218. package/src/hooks/policy-evaluator.ts +17 -0
  219. package/src/hooks/policy-reviewability.ts +4 -2
  220. package/src/hooks/semantic/cloud-jev.ts +219 -28
  221. package/src/hooks/semantic/combine.ts +26 -1
  222. package/src/hooks/semantic/intent.ts +127 -24
  223. package/src/hooks/semantic/jev-client.ts +1 -1
  224. package/src/hooks/semantic/jev-config.ts +6 -27
  225. package/src/hooks/semantic/jev-review.ts +111 -4
  226. package/src/hooks/tui.ts +280 -0
  227. package/src/hooks/uninstall-cli.ts +12 -1
  228. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0588st-tpiuia._.js +0 -3
  229. package/.next/standalone/.next/server/chunks/ssr/_128ebxr83nk6g._.js +0 -3
  230. package/.next/standalone/.next/static/chunks/0jutamwqz8pg5.js +0 -1
  231. package/.next/standalone/.next/static/chunks/114hbolvrv94e.js +0 -1
  232. package/.next/standalone/.next/static/chunks/1p-hsi58i-6h0.js +0 -6
  233. package/.next/standalone/.next/static/chunks/3-pku9qwv1yfu.js +0 -1
  234. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_buildManifest.js +0 -0
  235. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_clientMiddlewareManifest.js +0 -0
  236. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_ssgManifest.js +0 -0
@@ -0,0 +1,312 @@
1
+ """User-level CLI smoke against a local Cloud fake, including paused SSE.
2
+
3
+ Run after building/installing the wheel:
4
+ python tests/fake_cloud_smoke.py --fp /tmp/fp-smoke/bin/fp --artifacts /job/artifacts
5
+
6
+ The fake is intentionally server-shaped and does not import the CLI's client.
7
+ It exercises actual subprocesses, cookie login, bearer routing and an interactive
8
+ approval through a pseudo-terminal. Never connect this script to a live Cloud.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+ import pty
17
+ import re
18
+ import select
19
+ import subprocess
20
+ import tempfile
21
+ import threading
22
+ import time
23
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
24
+ from pathlib import Path
25
+
26
+
27
+ class Cloud:
28
+ definitions = []
29
+ dashboards = []
30
+ tiles = []
31
+ requests = []
32
+ answers = []
33
+ pending = {}
34
+ unavailable = False
35
+ source = 'EvalResult(score=Score(1.0 if session.count("error") == 0 else 0.0), reasoning="Error-free session")'
36
+
37
+
38
+ class Handler(BaseHTTPRequestHandler):
39
+ protocol_version = "HTTP/1.1"
40
+
41
+ def log_message(self, *args):
42
+ pass
43
+
44
+ def json_response(self, data, status=200, headers=None):
45
+ raw = json.dumps(data).encode()
46
+ self.send_response(status)
47
+ self.send_header("Content-Type", "application/json")
48
+ self.send_header("Content-Length", str(len(raw)))
49
+ for key, value in (headers or {}).items():
50
+ self.send_header(key, value)
51
+ self.end_headers()
52
+ self.wfile.write(raw)
53
+
54
+ def stream(self, events):
55
+ self.send_response(200)
56
+ self.send_header("Content-Type", "text/event-stream")
57
+ self.send_header("Connection", "close")
58
+ self.end_headers()
59
+ for event in events:
60
+ self.wfile.write(("data: " + json.dumps(event) + "\n\n").encode())
61
+ self.wfile.flush()
62
+ if event["type"] == "ask-user":
63
+ if not Cloud.pending[event["callId"]].wait(10):
64
+ raise RuntimeError("CLI did not answer the pending approval")
65
+ self.close_connection = True
66
+
67
+ def do_GET(self):
68
+ self.dispatch("GET")
69
+
70
+ def do_POST(self):
71
+ self.dispatch("POST")
72
+
73
+ def do_PATCH(self):
74
+ self.dispatch("PATCH")
75
+
76
+ def do_PUT(self):
77
+ self.dispatch("PUT")
78
+
79
+ def dispatch(self, method):
80
+ raw = self.rfile.read(int(self.headers.get("Content-Length", 0)))
81
+ body = json.loads(raw) if raw else None
82
+ path = self.path.split("?", 1)[0]
83
+ key_mode = path.startswith("/v1/")
84
+ route = re.sub(r"^/(api|v1)", "", path)
85
+ Cloud.requests.append({"method": method, "path": path, "body": body, "auth": "key" if key_mode else "session"})
86
+ if route == "/auth/otp/request":
87
+ return self.json_response({"ok": True})
88
+ if route == "/auth/otp/verify":
89
+ assert body["code"] == "123456"
90
+ return self.json_response({"user": {"id": "u1", "email": "smoke@example.test"}, "expires_in_secs": 3600},
91
+ headers={"Set-Cookie": "ae_session=fake-session; Path=/"})
92
+ if key_mode:
93
+ assert self.headers.get("Authorization") == "Bearer fake-key"
94
+ assert not self.headers.get("Cookie")
95
+ else:
96
+ assert "ae_session=fake-session" in self.headers.get("Cookie", "")
97
+ assert not self.headers.get("Authorization")
98
+ if Cloud.unavailable:
99
+ return self.json_response({"code": "unknown_route", "error": "old Cloud route absent"}, 404)
100
+ if route == "/auth/session":
101
+ return self.json_response({"id": "u1", "email": "smoke@example.test", "memberships": [
102
+ {"org_id": "o1", "org_slug": "smoke", "org_name": "Smoke", "permissions": ["evaluations:trigger", "events:read", "queries:run", "agent:use"], "permission_set": "admin"}]})
103
+ if route == "/access-granters":
104
+ return self.json_response([])
105
+ if route == "/agent/compose-eval":
106
+ return self.json_response({"eval_key": "error_free", "display_name": "Error free", "result_kind": "score", "labels": ["reliability"],
107
+ "condition_source": "", "evaluator_source": Cloud.source, "reasoning": "Drafted from intent", "kind": "code"})
108
+ if route == "/evaluator-definitions/dry-run":
109
+ assert len(body["session_ids"]) <= 10
110
+ runs = [
111
+ {"ok": sid != "failed-session", "session_id": sid, "mode": "run",
112
+ "result": {"score": {"value": 1.0}, "reasoning": "No errors found"} if sid != "failed-session" else None,
113
+ "error_message": "Missing result score" if sid == "failed-session" else None}
114
+ for sid in body["session_ids"]
115
+ ]
116
+ failed = sum(not run["ok"] for run in runs)
117
+ return self.json_response({"runs": runs,
118
+ "summary": {"failed": failed, "succeeded": len(runs) - failed, "skipped": 0}})
119
+ if route == "/evaluations/backfill":
120
+ assert body == {
121
+ "from": "2026-10-01T00:00:00Z", "to": "2026-10-08T00:00:00Z",
122
+ "eval_key": "error_free", "dry_run": True,
123
+ }
124
+ return self.json_response({"queued": False, "dry_run": True})
125
+ if route == "/sessions/missing/re-evaluate":
126
+ return self.json_response({"error": "session not found"}, 404)
127
+ if route == "/evaluator-definitions":
128
+ if method == "GET":
129
+ return self.json_response(Cloud.definitions)
130
+ assert body["eval_key"] and body["display_name"] and body["eval_version"]
131
+ assert isinstance(body["evaluator_source"], str)
132
+ if body["evaluator_source"].startswith("{"):
133
+ envelope = json.loads(body["evaluator_source"])
134
+ assert envelope["kind"] in ("judge", "jev")
135
+ assert body["result_kind"] == "score"
136
+ row = {**body, "id": f"d{len(Cloud.definitions) + 1}", "enabled": True}
137
+ Cloud.definitions.append(row)
138
+ return self.json_response(row, 201)
139
+ if route.startswith("/evaluator-definitions/") and method == "PATCH":
140
+ row = next(r for r in Cloud.definitions if r["id"] == route.rsplit("/", 1)[1])
141
+ assert set(body) <= {"enabled", "labels", "display_name"}
142
+ row.update(body)
143
+ return self.json_response(row)
144
+ if route == "/dashboards":
145
+ if method == "GET":
146
+ return self.json_response({"dashboards": Cloud.dashboards})
147
+ assert isinstance(body["definition"], dict)
148
+ row = {**body, "id": "board1"}
149
+ Cloud.dashboards.append(row)
150
+ return self.json_response(row, 201)
151
+ if route == "/dashboards/board1/tiles":
152
+ if method == "GET":
153
+ return self.json_response({"tiles": Cloud.tiles})
154
+ assert body["query_id"] == "q1" and body["chart_type"] == "bar"
155
+ assert all(k in body for k in ("pos_x", "pos_y", "pos_w", "pos_h"))
156
+ row = {**body, "id": "tile1"}
157
+ Cloud.tiles.append(row)
158
+ return self.json_response(row, 201)
159
+ if route.startswith("/events/sentiment"):
160
+ return self.json_response({"positive": 4, "negative": 1, "neutral": 2})
161
+ if route == "/queries/run":
162
+ assert body["sql"].lower().startswith("select")
163
+ return self.json_response({"columns": [{"name": "agent", "type": "String"}, {"name": "events", "type": "UInt64"}],
164
+ "rows": [["agent-smoke", 42]], "truncated": False, "elapsed_ms": 1})
165
+ if route == "/agent/chat":
166
+ call_id = f"ask_{len(Cloud.answers) + 1}"
167
+ Cloud.pending[call_id] = threading.Event()
168
+ return self.stream([
169
+ {"type": "ui-action", "action": "open_artifact", "artifact": {"title": "Events by agent", "sql": "SELECT agent_id, count() FROM events GROUP BY agent_id"}},
170
+ {"type": "ask-user", "kind": "approval", "callId": call_id, "question": "Approve disabling the test alert?", "details": "Change: alert test-alert enabled=false", "allowText": False},
171
+ {"type": "text-delta", "text": "The requested summary is ready."},
172
+ {"type": "done"},
173
+ ])
174
+ if route == "/agent/answer":
175
+ assert body["decision"] in ("approve", "reject")
176
+ Cloud.answers.append(body)
177
+ Cloud.pending[body["callId"]].set()
178
+ return self.json_response({}, 200)
179
+ if route == "/agent/conversations" and method == "POST":
180
+ return self.json_response({"id": "chat-smoke-001"})
181
+ if route == "/agent/conversations/chat-smoke-001/messages":
182
+ assert body["messages"][-1]["content"]["artifacts"]
183
+ return self.json_response({})
184
+ if route == "/agent/home-brief":
185
+ return self.json_response({"greeting": "Hello", "summary": "One agent needs attention.",
186
+ "blocks": [{"key": "traffic", "kind": "table", "title": "Events by agent", "sql": "SELECT 42"}]})
187
+ return self.json_response({"error": "unexpected fake route"}, 404)
188
+
189
+
190
+ def main():
191
+ parser = argparse.ArgumentParser()
192
+ parser.add_argument("--fp", required=True)
193
+ parser.add_argument("--artifacts", required=True)
194
+ args = parser.parse_args()
195
+ artifacts = Path(args.artifacts)
196
+ artifacts.mkdir(parents=True, exist_ok=True)
197
+ server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
198
+ threading.Thread(target=server.serve_forever, daemon=True).start()
199
+ base = f"http://127.0.0.1:{server.server_port}"
200
+ transcript = []
201
+ with tempfile.TemporaryDirectory(prefix="fp-user-smoke-") as tmp:
202
+ env = {**os.environ, "FP_HOME": tmp, "FP_ANALYTICS_DISABLED": "1",
203
+ "COLUMNS": os.environ.get("FP_SMOKE_COLUMNS", "140"), "NO_COLOR": "1"}
204
+ for var in ("FP_TOKEN", "FP_API_KEY", "FP_ORG", "FP_JSON", "FP_DASHBOARD_URL"):
205
+ env.pop(var, None)
206
+ fp = [args.fp, "--base-url", base]
207
+
208
+ def run(argv, *, input=None, key=False, json_mode=True, expected=0):
209
+ cmd = fp + (["--api-key", "fake-key"] if key else []) + (["--json"] if json_mode else []) + argv
210
+ completed = subprocess.run(cmd, input=input, text=True, capture_output=True, env=env, cwd=tmp, timeout=20)
211
+ transcript.append("$ fp " + " ".join(argv) + (" [key mode]" if key else " [session mode]") + "\n" + completed.stdout + completed.stderr + f"\nexit {completed.returncode}\n")
212
+ assert completed.returncode == expected, transcript[-1]
213
+ return json.loads(completed.stdout) if json_mode and completed.stdout else completed.stdout
214
+
215
+ run(["login", "--email", "smoke@example.test"], input="123456\n", json_mode=False)
216
+ spec = str(Path(tmp) / "evaluator.json")
217
+ run(["evaluators", "draft", "Measure error-free sessions with code", "--kind", "code", "--out", spec])
218
+ run(["evaluators", "create", "--file", spec, "--dry-run-on", "run-001"])
219
+ run(["evaluators", "dry-run", "--file", spec, "--session", "run-001"], key=True)
220
+ run(["evaluators", "deploy", "error_free", "--file", spec, "--version", "2"], key=True)
221
+ assert not Cloud.definitions[0]["enabled"] and Cloud.definitions[1]["enabled"]
222
+ run(["evaluators", "list", "--all"], key=True)
223
+ human_list = run(["evaluators", "list", "--all"], key=True, json_mode=False)
224
+ assert "EVALUATORS" in human_list and "error_free" in human_list and "[]" not in human_list
225
+ run(["evaluators", "disable", "error_free@2"], key=True)
226
+ run(["evaluators", "backfill", "--from", "2026-10-01T02:00:00+02:00",
227
+ "--to", "2026-10-07T20:00:00-04:00", "--key", "error_free", "--dry-run"], key=True)
228
+ run(["evaluators", "create", "--key", "relevance", "--name", "Relevance", "--judge", "--criteria", "Grade relevance"], key=True)
229
+ run(["evaluators", "dry-run", "--judge", "--criteria", "Grade relevance"], key=True, expected=2)
230
+ for step in ("score 1 if correct", "give 0 when incorrect", "pass/fail", "yes or no"):
231
+ run(["evaluators", "create", "--key", "binary", "--name", "Binary", "--judge",
232
+ "--criteria", "Grade relevance", "--step", step], key=True, expected=2)
233
+ run(["evaluators", "create", "--key", "resolution", "--name", "Resolution", "--jev", "noul", "--instructions", "Was the request resolved?", "--true", "Resolved", "--false", "Unresolved"], key=True)
234
+ before = len(Cloud.definitions)
235
+ failed = run(["evaluators", "create", "--key", "preview_failure", "--name", "Preview failure",
236
+ "--code", "-", "--dry-run-on", "failed-session"],
237
+ input=Cloud.source, key=True, json_mode=False, expected=1)
238
+ assert "failed-session · failed" in failed and "Missing result score" in failed
239
+ assert len(Cloud.definitions) == before
240
+ forced = run(["evaluators", "create", "--key", "preview_failure", "--name", "Preview failure",
241
+ "--code", "-", "--dry-run-on", "failed-session", "--force"],
242
+ input=Cloud.source, key=True, json_mode=False)
243
+ assert len(Cloud.definitions) == before + 1
244
+ assert forced.index("Missing result score") < forced.index("EVALUATOR CREATED")
245
+ run(["dashboards", "create", "--name", "Quality"], key=True)
246
+ run(["dashboards", "tiles", "board1", "add", "--query", "q1", "--chart", "bar", "--title", "Traffic"], key=True)
247
+ run(["dashboards", "tiles", "board1", "list"])
248
+ run(["sentiment", "summary"], key=True)
249
+ answer = run(["agent", "ask", "Count events"])
250
+ assert answer["artifacts"][0]["result"]["rows"] == [["agent-smoke", 42]]
251
+ assert Cloud.answers[-1]["decision"] == "reject"
252
+ run(["agent", "ask", "Count events", "--approve"])
253
+ assert Cloud.answers[-1]["decision"] == "approve"
254
+ run(["agent", "brief", "--range", "7d"], json_mode=False)
255
+ run(["evaluators", "draft", "Grade quality"], key=True, expected=2)
256
+ denial = run(["agent", "ask", "Hello"], key=True, expected=2)
257
+ assert denial["error"] == "`fp agent ask` needs a signed-in session: run `fp login`"
258
+ for argv in (["plan"], ["plan", "claim", "offer"]):
259
+ denied = run(argv, key=True, expected=2)
260
+ assert "needs a signed-in session" in denied["error"]
261
+ missing = run(["sessions", "re-evaluate", "missing"], key=True, expected=6)
262
+ assert missing["error"] == "session not found"
263
+ assert "not available" not in missing["error"]
264
+ help_text = run(["help"], json_mode=False)
265
+ for group in ("evaluators", "dashboards", "sentiment", "roles", "plan", "otel", "slack"):
266
+ assert group in help_text
267
+ Cloud.unavailable = True
268
+ unavailable = run(["sentiment", "summary"], key=True, expected=6)
269
+ assert "not available on this FailproofAI Cloud yet" in unavailable["error"]
270
+ Cloud.unavailable = False
271
+
272
+ # Real terminal approval, with the stream paused until /agent/answer.
273
+ master, slave = pty.openpty()
274
+ proc = subprocess.Popen(fp + ["agent", "ask", "Count events"], stdin=slave, stdout=slave, stderr=slave, env=env, cwd=tmp)
275
+ os.close(slave)
276
+ output = b""
277
+ sent = False
278
+ deadline = time.monotonic() + 20
279
+ while time.monotonic() < deadline:
280
+ ready, _, _ = select.select([master], [], [], 0.1)
281
+ if ready:
282
+ try:
283
+ data = os.read(master, 65536)
284
+ except OSError:
285
+ break
286
+ if not data:
287
+ break
288
+ output += data
289
+ if b"Approve this change" in output and not sent:
290
+ os.write(master, b"y\n")
291
+ sent = True
292
+ if proc.poll() is not None and not ready:
293
+ break
294
+ if proc.poll() is None:
295
+ proc.kill()
296
+ proc.wait(timeout=5)
297
+ os.close(master)
298
+ transcript.append("$ fp agent ask 'Count events' [interactive approval]\n" + output.decode(errors="replace") + f"\nexit {proc.returncode}\n")
299
+ assert proc.returncode == 0 and sent, transcript[-1]
300
+ assert Cloud.answers[-1]["decision"] == "approve"
301
+ # CI may force colour even with NO_COLOR, inserting ANSI spans inside
302
+ # the wordmark. Compare visible text without weakening the order check.
303
+ rendered = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", output.decode(errors="replace"))
304
+ assert rendered.index("failproof ai") < rendered.index("EVENTS BY AGENT")
305
+ server.shutdown()
306
+ (artifacts / "fp-user-smoke.txt").write_text("\n".join(transcript), encoding="utf-8")
307
+ (artifacts / "fp-fake-cloud-requests.json").write_text(json.dumps(Cloud.requests, indent=2), encoding="utf-8")
308
+ print(f"User smoke passed: {len(transcript)} CLI invocations; cookie/key auth and paused SSE approvals verified.")
309
+
310
+
311
+ if __name__ == "__main__":
312
+ main()
@@ -0,0 +1,125 @@
1
+ import json
2
+
3
+ import pytest
4
+ import respx
5
+
6
+ from fp_cli.app import app
7
+
8
+ BASE = "http://dash.test"
9
+ CONTRACT = {"agent_id": "agent-1", "body": "# Purpose\nResolve requests", "enabled": True}
10
+ CASES = [
11
+ (["roles", "list"], "GET", "/permission-sets", []),
12
+ (["roles", "show", "reviewer"], "GET", "/permission-sets/reviewer", {"name": "reviewer", "permissions": ["events:read"]}),
13
+ (["roles", "create", "--name", "reviewer", "--permission", "events:read"], "POST", "/permission-sets", {"name": "reviewer"}),
14
+ (["roles", "update", "reviewer", "--add", "queries:run"], "PUT", "/permission-sets/reviewer", {"name": "reviewer"}),
15
+ (["roles", "delete", "reviewer"], "DELETE", "/permission-sets/reviewer", {"deleted": True}),
16
+ (["users", "delete", "u1"], "DELETE", "/users/u1", {"deleted": True}),
17
+ (["users", "defaults"], "GET", "/users/defaults", {}),
18
+ (["usage", "windows"], "GET", "/usage/windows", []),
19
+ (["plan"], "GET", "/offers/me", []),
20
+ (["plan", "claim", "team_offer"], "POST", "/offers/claim", {"claimed": True}),
21
+ (["settings", "context-windows", "list"], "GET", "/settings/model-context-windows", {}),
22
+ (["settings", "context-windows", "set", "provider/model", "64000"], "PUT", "/settings/model-context-windows", {}),
23
+ (["settings", "context-windows", "delete", "provider/model"], "DELETE", "/settings/model-context-windows", {}),
24
+ (["otel", "status"], "GET", "/otel/status", {}),
25
+ (["otel", "enable"], "PUT", "/settings/otel.ingest_enabled", {"value": True}),
26
+ (["otel", "disable"], "PUT", "/settings/otel.ingest_enabled", {"value": False}),
27
+ (["alerts", "recipients"], "GET", "/alerts/recipients", []),
28
+ (["alerts", "issues", "a1"], "GET", "/alerts/a1/issues", []),
29
+ (["alerts", "issues", "a1", "--create", "--title", "Failure"], "POST", "/alerts/a1/issues", {"id": "i1"}),
30
+ (["audits", "contracts", "list"], "GET", "/audits/contracts", []),
31
+ (["audits", "contracts", "show", "c1"], "GET", "/audits/contracts/c1", CONTRACT),
32
+ (["audits", "contracts", "create", "--agent-id", "agent-1", "--body", "# Purpose\nResolve requests"], "POST", "/audits/contracts", CONTRACT),
33
+ (["audits", "contracts", "update", "c1", "--body", "# New purpose"], "PUT", "/audits/contracts/c1", CONTRACT),
34
+ ]
35
+ SESSION = [
36
+ (["orgs", "create", "--name", "My Team"], "POST", "/orgs", {"id": "o1"}),
37
+ (["slack", "status"], "GET", "/integrations/slack", {"connected": True}),
38
+ (["slack", "disconnect"], "DELETE", "/integrations/slack", {"disconnected": True}),
39
+ ]
40
+
41
+
42
+ @pytest.mark.parametrize("key_mode", [False, True])
43
+ @pytest.mark.parametrize("argv,method,path,response", CASES)
44
+ def test_commands(logged_in, runner, key_mode, argv, method, path, response):
45
+ if not key_mode and argv[:2] == ["usage", "windows"]:
46
+ with respx.mock as mock:
47
+ result = runner.invoke(app, ["--json", *argv])
48
+ assert not mock.calls
49
+ assert result.exit_code == 2
50
+ assert "needs an API key" in result.stdout
51
+ return
52
+ if key_mode and argv[0] == "plan":
53
+ with respx.mock as mock:
54
+ result = runner.invoke(app, ["--json", "--api-key", "key", *argv])
55
+ assert not mock.calls
56
+ assert result.exit_code == 2
57
+ assert "needs a signed-in session" in result.stdout
58
+ return
59
+ prefix = "/v1" if key_mode else "/api"
60
+ with respx.mock(assert_all_called=False) as mock:
61
+ mock.get(f"{BASE}{prefix}/permission-sets/reviewer").respond(200, json={"name": "reviewer", "permissions": ["events:read"], "description": "Keep"})
62
+ mock.get(f"{BASE}{prefix}/audits/contracts/c1").respond(200, json=CONTRACT)
63
+ mock.get(f"{BASE}{prefix}/team-trial/me").respond(200, json={})
64
+ route = mock.request(method, f"{BASE}{prefix}{path}").respond(200, json=response)
65
+ result = runner.invoke(app, ["--json", *(["--api-key", "key"] if key_mode else []), *argv])
66
+ assert result.exit_code == 0, result.output
67
+ assert route.called
68
+ json.loads(result.stdout)
69
+ if argv[:2] == ["roles", "create"]:
70
+ assert json.loads(route.calls[0].request.content) == {"name": "reviewer", "permissions": ["events:read"]}
71
+ if argv[:2] == ["roles", "update"]:
72
+ assert json.loads(route.calls[0].request.content) == {"permissions": ["events:read", "queries:run"], "description": "Keep"}
73
+ if argv[:2] == ["plan", "claim"]:
74
+ assert json.loads(route.calls[0].request.content) == {"offer_key": "team_offer"}
75
+ if argv[:3] == ["settings", "context-windows", "set"]:
76
+ assert json.loads(route.calls[0].request.content) == {"model_id": "provider/model", "context_window": 64000}
77
+ if argv[:3] == ["settings", "context-windows", "delete"]:
78
+ assert route.calls[0].request.url.params["model"] == "provider/model"
79
+ if argv[:3] == ["audits", "contracts", "update"]:
80
+ assert json.loads(route.calls[0].request.content) == {**CONTRACT, "body": "# New purpose"}
81
+
82
+
83
+ @pytest.mark.parametrize("argv,method,path,response", SESSION)
84
+ def test_session_commands(logged_in, runner, argv, method, path, response):
85
+ with respx.mock as mock:
86
+ route = mock.request(method, f"{BASE}/api{path}").respond(200, json=response)
87
+ result = runner.invoke(app, ["--json", *argv])
88
+ assert result.exit_code == 0, result.output
89
+ if path == "/orgs":
90
+ assert json.loads(route.calls[0].request.content) == {"name": "My Team", "slug": "my-team"}
91
+ with respx.mock as mock:
92
+ result = runner.invoke(app, ["--json", "--api-key", "key", *argv])
93
+ assert not mock.calls
94
+ assert result.exit_code == 2
95
+ assert "needs a signed-in session" in result.stdout
96
+
97
+
98
+ @pytest.mark.parametrize("argv,method,path,response", CASES + SESSION)
99
+ def test_404(logged_in, runner, argv, method, path, response):
100
+ key = ["--api-key", "key"] if argv[:2] == ["usage", "windows"] else []
101
+ with respx.mock as mock:
102
+ mock.route().respond(404, json={"error": "unknown route"})
103
+ result = runner.invoke(app, ["--json", *key, *argv])
104
+ assert result.exit_code == 6, result.output
105
+ assert "not available on this FailproofAI Cloud yet" in result.stdout
106
+
107
+
108
+ @pytest.mark.parametrize("argv", [
109
+ ["roles", "create", "--name", "", "--permission", "events:read"],
110
+ ["roles", "create", "--name", "x", "--permission", "unknown:read"],
111
+ ["roles", "update", "x"],
112
+ ["roles", "update", "x", "--permission", "events:read", "--add", "queries:run"],
113
+ ["settings", "context-windows", "set", "model", "-1"],
114
+ ["settings", "context-windows", "set", "model", "1000001"],
115
+ ["alerts", "issues", "a1", "--create"],
116
+ ["alerts", "issues", "a1", "--title", "No create"],
117
+ ["audits", "contracts", "create", "--agent-id", "a", "--body", ""],
118
+ ["audits", "contracts", "update", "c1"],
119
+ ["orgs", "create", "--name", ""],
120
+ ])
121
+ def test_validation(logged_in, runner, argv):
122
+ with respx.mock as mock:
123
+ result = runner.invoke(app, ["--json", *argv])
124
+ assert not mock.calls
125
+ assert result.exit_code == 2, result.output