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,253 @@
1
+ import json
2
+ import re
3
+ from io import StringIO
4
+
5
+ import httpx
6
+ import pytest
7
+ import respx
8
+ from rich.console import Console
9
+
10
+ from fp_cli import output
11
+ from fp_cli.app import app
12
+
13
+ BASE = "http://dash.test"
14
+ RESULT = {"columns": [{"name": "total", "type": "UInt64"}], "rows": [[42]]}
15
+
16
+
17
+ def sse(events):
18
+ return "".join("data: " + json.dumps(e) + "\n\n" for e in events)
19
+
20
+
21
+ @pytest.mark.parametrize("json_mode,approve,tty,input_text,decision", [
22
+ (True, False, False, "", "reject"),
23
+ (True, True, False, "", "approve"),
24
+ (False, False, False, "", "reject"),
25
+ (False, False, True, "y\n", "approve"),
26
+ (False, False, True, "n\n", "reject"),
27
+ ])
28
+ def test_ask_artifacts_and_approvals(logged_in, runner, monkeypatch, json_mode, approve, tty, input_text, decision):
29
+ events = [
30
+ {"type": "ui-action", "action": "open_artifact", "artifact": {"title": "Event total", "sql": "SELECT count()"}},
31
+ {"type": "ask-user", "callId": "ask_1", "kind": "approval", "question": "Disable alert?", "details": "alert a1"},
32
+ {"type": "ask-user-answered", "callId": "ask_1", "decision": decision},
33
+ {"type": "text-delta", "text": "Here is the result."},
34
+ {"type": "done"},
35
+ ]
36
+ # Patch the exact guard used by the command; CliRunner's stdin is otherwise non-TTY.
37
+ import sys
38
+ monkeypatch.setattr(sys.stdin, "isatty", lambda: tty)
39
+ # Runner replaces stdin during invocation, so patch the module's sys proxy instead.
40
+ from fp_cli.commands import agent_cmds
41
+ class Stdin:
42
+ def isatty(self):
43
+ return tty
44
+ class Sys:
45
+ stdin = Stdin()
46
+ stdout = sys.stdout
47
+ monkeypatch.setattr(agent_cmds, "sys", Sys())
48
+ with respx.mock as mock:
49
+ stream = mock.post(f"{BASE}/api/agent/chat").respond(200, text=sse(events), headers={"content-type": "text/event-stream"})
50
+ query = mock.post(f"{BASE}/api/queries/run").respond(200, json=RESULT)
51
+ answer = mock.post(f"{BASE}/api/agent/answer").respond(204)
52
+ mock.post(f"{BASE}/api/agent/conversations").respond(200, json={"id": "c1"})
53
+ saved = mock.put(f"{BASE}/api/agent/conversations/c1/messages").respond(204)
54
+ result = runner.invoke(app, [*(["--json"] if json_mode else []), "agent", "ask", "Count events",
55
+ *(["--approve"] if approve else [])], input=input_text)
56
+ assert result.exit_code == 0, result.output
57
+ assert json.loads(answer.calls[0].request.content) == {"callId": "ask_1", "decision": decision}
58
+ assert query.called
59
+ assert json.loads(stream.calls[0].request.content)["pageActions"] == [{"action": "open_artifact"}, {"action": "home"}]
60
+ assert json.loads(saved.calls[0].request.content)["messages"][-1]["content"]["artifacts"][0]["sql"] == "SELECT count()"
61
+ if json_mode:
62
+ data = json.loads(result.stdout)
63
+ assert data["answer"] == "Here is the result."
64
+ assert data["artifacts"][0]["result"] == RESULT
65
+ else:
66
+ assert "EVENT TOTAL" in result.output
67
+ assert "42" in result.output
68
+ assert result.stdout.index("failproof ai v") < result.stdout.index("EVENT TOTAL")
69
+ if tty:
70
+ assert "alert a1" in result.output
71
+ assert "Approve this change" in result.output
72
+
73
+
74
+ def test_home_blocks(logged_in, runner):
75
+ stream = sse([{"type": "ui-action", "action": "home", "home": {"op": "replace", "blocks": [
76
+ {"key": "n", "kind": "stats", "title": "Count", "sql": "SELECT 42"},
77
+ {"key": "note", "kind": "note", "title": "Summary", "text": "All good"},
78
+ ]}}, {"type": "done"}])
79
+ with respx.mock as mock:
80
+ mock.post(f"{BASE}/api/agent/chat").respond(200, text=stream)
81
+ query = mock.post(f"{BASE}/api/queries/run").respond(200, json=RESULT)
82
+ mock.post(f"{BASE}/api/agent/conversations").respond(200, json={"id": "c1"})
83
+ mock.put(f"{BASE}/api/agent/conversations/c1/messages").respond(204)
84
+ result = runner.invoke(app, ["--json", "agent", "ask", "Summarize"])
85
+ assert result.exit_code == 0, result.output
86
+ assert query.call_count == 1
87
+ assert len(json.loads(result.stdout)["artifacts"]) == 2
88
+
89
+
90
+ @pytest.mark.parametrize("json_mode", [False, True])
91
+ def test_brief(logged_in, runner, json_mode):
92
+ with respx.mock as mock:
93
+ route = mock.post(f"{BASE}/api/agent/home-brief").respond(200, json={
94
+ "greeting": "Hello", "summary": "Healthy", "blocks": [{"title": "Events", "sql": "SELECT count()"}]})
95
+ mock.post(f"{BASE}/api/queries/run").respond(200, json=RESULT)
96
+ result = runner.invoke(app, [*(["--json"] if json_mode else []), "agent", "brief", "--range", "7d"])
97
+ assert result.exit_code == 0, result.output
98
+ assert "Healthy" in result.output
99
+ assert json.loads(route.calls[0].request.content) == {"range": "7d"}
100
+
101
+
102
+ @pytest.mark.parametrize("argv,path,response", [
103
+ (["policies", "author", "Prevent force push"], "/api/agent/policy-author", "stream"),
104
+ (["policies", "backtest", "{file}"], "/api/agent/policy-backtest", {"matched": 3}),
105
+ (["policies", "candidacy", "--issue", "i1"], "/api/agent/policy-candidacy", {"candidate": True}),
106
+ ])
107
+ def test_policy_commands(logged_in, runner, tmp_path, argv, path, response):
108
+ file = tmp_path / "policy.mjs"
109
+ file.write_text("export default {}")
110
+ with respx.mock(assert_all_called=False) as mock:
111
+ mock.get(f"{BASE}/api/issues/i1").respond(200, json={"id": "i1", "title": "Force pushes"})
112
+ if response == "stream":
113
+ route = mock.post(f"{BASE}{path}").respond(200, text=sse([{"type": "done", "result": {"source": "policy"}}]))
114
+ else:
115
+ route = mock.post(f"{BASE}{path}").respond(200, json=response)
116
+ result = runner.invoke(app, ["--json", *[v.format(file=file) for v in argv]])
117
+ assert result.exit_code == 0, result.output
118
+ assert route.called
119
+ json.loads(result.stdout)
120
+ if argv[1] == "author":
121
+ assert json.loads(route.calls[0].request.content)["issue"]["title"] == "Prevent force push"
122
+ elif argv[1] == "backtest":
123
+ assert json.loads(route.calls[0].request.content) == {"source": "export default {}"}
124
+
125
+
126
+ @pytest.mark.parametrize("argv", [
127
+ ["policies", "author", "Prevent force push"], ["policies", "backtest", "p1"],
128
+ ["policies", "candidacy", "--issue", "i1"], ["agent", "brief"],
129
+ ])
130
+ def test_session_only(logged_in, runner, argv):
131
+ with respx.mock as mock:
132
+ result = runner.invoke(app, ["--json", "--api-key", "key", *argv])
133
+ assert not mock.calls
134
+ assert result.exit_code == 2
135
+ assert "needs a signed-in session" in result.stdout
136
+
137
+
138
+ @pytest.mark.parametrize("argv", [
139
+ ["policies", "author", "Prevent force push"], ["policies", "backtest", "p1"],
140
+ ["policies", "candidacy", "--issue", "i1"], ["agent", "brief"], ["agent", "ask", "Hello"],
141
+ ])
142
+ def test_404(logged_in, runner, argv):
143
+ with respx.mock as mock:
144
+ mock.route().respond(404, json={"error": "unknown route"})
145
+ result = runner.invoke(app, ["--json", *argv])
146
+ assert result.exit_code == 6, result.output
147
+ assert "not available on this FailproofAI Cloud yet" in result.stdout
148
+
149
+
150
+ def test_backtest_published_policy_uses_latest_version(logged_in, runner):
151
+ with respx.mock as mock:
152
+ mock.get(f"{BASE}/api/enforcement/policies").respond(200, json=[
153
+ {"id": "p1", "version": 1, "source": "old"},
154
+ {"id": "p1", "version": 2, "source": "new"},
155
+ ])
156
+ route = mock.post(f"{BASE}/api/agent/policy-backtest").respond(200, json={"matched": 1})
157
+ result = runner.invoke(app, ["--json", "policies", "backtest", "p1"])
158
+ assert result.exit_code == 0, result.output
159
+ assert json.loads(route.calls[0].request.content) == {"source": "new"}
160
+
161
+
162
+ @pytest.mark.parametrize("json_mode", [False, True])
163
+ @pytest.mark.parametrize("failure,exit_code", [("404", 6), ("network", 3)])
164
+ def test_approval_answer_failure_is_clean(logged_in, runner, json_mode, failure, exit_code):
165
+ with respx.mock(assert_all_called=False) as mock:
166
+ mock.post(f"{BASE}/api/agent/chat").respond(200, text=sse([
167
+ {"type": "ask-user", "kind": "approval", "callId": "ask_1", "question": "Disable alert?"},
168
+ {"type": "text-delta", "text": "Must not reach this answer."},
169
+ {"type": "done"},
170
+ ]))
171
+ answer = mock.post(f"{BASE}/api/agent/answer")
172
+ if failure == "404":
173
+ answer.respond(404, json={"error": "unknown route"})
174
+ else:
175
+ answer.mock(side_effect=httpx.ConnectError("connection refused"))
176
+ saved = mock.post(f"{BASE}/api/agent/conversations").respond(200, json={"id": "c1"})
177
+ result = runner.invoke(app, [*(["--json"] if json_mode else []), "agent", "ask", "Hello", "--approve"])
178
+ assert result.exit_code == exit_code, result.output
179
+ assert answer.called and not saved.called
180
+ assert "Traceback" not in result.output
181
+ assert "Must not reach this answer" not in result.output
182
+ if json_mode:
183
+ data = json.loads(result.stdout)
184
+ assert data["exit_code"] == exit_code
185
+ message = data["error"]
186
+ else:
187
+ assert len(result.stderr.strip().splitlines()) == 1
188
+ message = result.stderr
189
+ assert "Assistant approval answer failed" in message
190
+ assert "delivery unconfirmed" in message
191
+ if failure == "404":
192
+ assert "not available on this FailproofAI Cloud yet" in message
193
+
194
+
195
+ @pytest.mark.parametrize("failure,exit_code", [("404", 6), ("network", 3)])
196
+ def test_approval_failure_restores_terminal_status(logged_in, runner, monkeypatch, failure, exit_code):
197
+ # The suite defaults to TERM=dumb, which suppresses live cursor control.
198
+ monkeypatch.setenv("TERM", "xterm-256color")
199
+ capture = StringIO()
200
+ terminal = Console(file=capture, force_terminal=True, color_system="truecolor", width=200)
201
+ original_configure = output.configure
202
+
203
+ def configure_terminal(**kwargs):
204
+ original_configure(**kwargs)
205
+ monkeypatch.setattr(output, "_stderr", terminal)
206
+
207
+ monkeypatch.setattr(output, "configure", configure_terminal)
208
+ with respx.mock as mock:
209
+ mock.post(f"{BASE}/api/agent/chat").respond(200, text=sse([
210
+ {"type": "ask-user", "kind": "approval", "callId": "ask_1", "question": "Disable alert?"},
211
+ ]))
212
+ answer = mock.post(f"{BASE}/api/agent/answer")
213
+ if failure == "404":
214
+ answer.respond(404, json={"error": "unknown route"})
215
+ else:
216
+ answer.mock(side_effect=httpx.ConnectError("connection refused"))
217
+ result = runner.invoke(app, ["agent", "ask", "Hello", "--approve"])
218
+ assert result.exit_code == exit_code, result.output
219
+ rendered = capture.getvalue()
220
+ assert terminal._live_stack == []
221
+ assert "\x1b[?25l" in rendered
222
+ assert rendered.rfind("\x1b[?25h") > rendered.rfind("\x1b[?25l")
223
+ assert rendered.index("Assistant approval answer failed") > rendered.rfind("\x1b[?25h")
224
+ plain = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", rendered)
225
+ assert plain.count("Assistant approval answer failed") == 1
226
+ assert "Traceback" not in plain
227
+
228
+
229
+ @pytest.mark.parametrize("json_mode", [False, True])
230
+ def test_ask_api_key_uses_session_only_wording(logged_in, runner, json_mode):
231
+ with respx.mock as mock:
232
+ result = runner.invoke(app, [
233
+ "--api-key", "key", *(["--json"] if json_mode else []),
234
+ "agent", "ask", "Hello",
235
+ ])
236
+ assert not mock.calls
237
+ assert result.exit_code == 2, result.output
238
+ expected = "`fp agent ask` needs a signed-in session: run `fp login`"
239
+ if json_mode:
240
+ assert json.loads(result.stdout)["error"] == expected
241
+ else:
242
+ assert expected in result.stderr
243
+ assert len(result.stderr.strip().splitlines()) == 1
244
+
245
+
246
+ def test_artifact_header_precedes_title_once(capsys):
247
+ from fp_cli.commands.agent_cmds import render_artifact
248
+ output.configure(no_color=True)
249
+ render_artifact({"title": "First artifact", "result": RESULT})
250
+ render_artifact({"title": "Second artifact", "result": RESULT})
251
+ rendered = capsys.readouterr().out
252
+ assert rendered.count("failproof ai v") == 1
253
+ assert rendered.index("failproof ai v") < rendered.index("FIRST ARTIFACT") < rendered.index("SECOND ARTIFACT")
@@ -0,0 +1,287 @@
1
+ """Real command output with server-shaped payloads at both terminal widths."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ import httpx
8
+ import pytest
9
+ import respx
10
+
11
+ from fp_cli.app import app
12
+
13
+ BASE = "http://dash.test"
14
+ CODE = 'EvalResult(score=Score(0.7), reasoning="Tool usage")'
15
+ DEFINITION = {
16
+ "eval_key": "tool_usage", "display_name": "Tool usage", "eval_version": "1",
17
+ "result_kind": "score", "enabled": True, "labels": ["tools", "reliability"],
18
+ "condition_source": 'session.agent_id == "support-bot"', "evaluator_source": CODE,
19
+ "id": "b370402a-0ace-4e45-9c1c-d40e7062b5ea", "created_at": "2026-10-14T05:26:14Z",
20
+ }
21
+ ROLE = {"name": "reviewer", "is_builtin": False, "user_count": 3,
22
+ "permissions": ["events:read", "evaluations:read", "queries:run", "users:read"],
23
+ "description": "Review agent quality"}
24
+ DASHBOARD = {"id": "board1", "name": "Quality", "description": "Review production agents",
25
+ "tile_count": 1, "definition": {"filters": {"env": ["prod"]}}}
26
+ TILE = {"id": "tile1", "query_id": "query1", "title": "Traffic", "chart_type": "bar",
27
+ "chart_config": {"x": "agent", "y": "events"}, "param_bindings": {},
28
+ "pos_x": 0, "pos_y": 0, "pos_w": 6, "pos_h": 4}
29
+ DRY_RUN = {"runs": [{"session_id": "run-check", "ok": True, "mode": "run",
30
+ "result": {"score": {"value": 0.7}, "reasoning": "Seven tool calls"},
31
+ "duration_ms": 8}],
32
+ "summary": {"succeeded": 1, "failed": 0, "skipped": 0}}
33
+ QUERY = {"columns": [{"name": "agent", "type": "String"}, {"name": "events", "type": "UInt64"}],
34
+ "rows": [["support-bot", 7]], "elapsed_ms": 9, "truncated": False}
35
+ SESSION = {"session_id": "run-check", "agent_id": "support-bot", "environment": "prod",
36
+ "event_count": 7, "status": "done"}
37
+ CONTRACT = {"id": "contract1", "agent_id": "support-bot", "body": "# Purpose\nResolve requests",
38
+ "enabled": True, "purpose": "Resolve requests"}
39
+
40
+
41
+ def response(request):
42
+ path = request.url.path
43
+ method = request.method
44
+ body = json.loads(request.content) if request.content else {}
45
+ data = {
46
+ "/api/evaluator-instances": {"instances": [
47
+ {"name": "failproof-managed", "kind": "managed", "enabled": True, "eval_count": 4,
48
+ "queued": 2, "id": "instance1"}]},
49
+ "/api/evaluator-schema-profile": {"window_days": 7, "sampled_events": 100, "event_types": [
50
+ {"event_type": "tool_use", "count": 100, "keys": [{"key": "tool_name", "type": "string", "presence": 1.0}]}]},
51
+ "/api/evaluation-jobs": {"jobs": [{"session_id": "run-check", "eval_key": "tool_usage",
52
+ "status": "queued", "id": "job1"}]},
53
+ "/api/evaluations/backfill": {"queued": True, "sessions": 12, "eval_key": "tool_usage"},
54
+ "/api/permission-sets": [ROLE],
55
+ "/api/permission-sets/reviewer": ROLE,
56
+ "/api/dashboards": {"dashboards": [DASHBOARD]},
57
+ "/api/dashboards/board1": {"dashboard": DASHBOARD, "tiles": [TILE]},
58
+ "/api/dashboards/board1/tiles": {"tiles": [TILE]},
59
+ "/api/dashboards/board1/tiles/tile1": TILE,
60
+ "/api/dashboards/board1/tiles/layout": {"updated": 1},
61
+ "/api/sessions": {"sessions": [SESSION]},
62
+ "/api/events/summary": {"events": [{"ts": "2026-10-14T05:26:14Z", "event_type": "tool_use",
63
+ "agent_id": "support-bot", "summary": "Read customer request"}]},
64
+ "/api/sessions/run-check/export": {**SESSION, "events": []},
65
+ "/api/sessions/run-check/evaluation-history": {"evaluations": [
66
+ {"eval_key": "tool_usage", "eval_version": "1", "status": "done",
67
+ "score": {"value": 0.7}, "finished_at": "2026-10-14T05:26:14Z"}]},
68
+ "/api/sessions/run-check/re-evaluate": {
69
+ "session_id": "run-check", "status": "queued", "v2_assignments_queued": 1,
70
+ "v2_assignment_ids": ["assignment-uuid"]},
71
+ "/api/events/latency_aggregate": {"p50": 42, "p95": 145, "models": [
72
+ {"model": "model1", "calls": 10, "p95_ms": 145}]},
73
+ "/api/events/sentiment": {"totals": {"messages": 10, "flagged": 2, "sessions": 3},
74
+ "averages": {"frustration": 0.2}, "timeline": [], "by_agent": []},
75
+ "/api/events/sentiment/messages": {"items": [{"session_id": "run-check",
76
+ "text": "Please try again", "scores": {"frustration": 0.8, "satisfaction": 0.1}}]},
77
+ "/api/events/sentiment/sessions": {"sessions": [{"session_id": "run-check",
78
+ "negative_score": 0.8, "message_count": 10, "feeling": "frustration"}]},
79
+ "/api/events/sentiment/sessions/run-check": {"session_id": "run-check",
80
+ "scores": {"frustration": 0.8}, "messages": []},
81
+ "/api/queries/run": QUERY,
82
+ "/api/alerts/recipients": {"recipients": [{"email": "ops@example.test", "channel": "email"}]},
83
+ "/api/alerts/alert1/issues": {"issues": [{"title": "Too many failures", "state": "open",
84
+ "severity": "warning", "id": "issue1"}]},
85
+ "/api/audits/contracts": {"contracts": [CONTRACT]},
86
+ "/api/audits/contracts/contract1": CONTRACT,
87
+ "/api/agent/policy-backtest": {"matched": 4, "allowed": 6, "denied": 4,
88
+ "examples": [{"session_id": "run-check", "reason": "Force push blocked"}]},
89
+ "/api/agent/policy-candidacy": {"candidate": True, "reason": "Force push can be blocked", "intent": "Prevent force push"},
90
+ "/api/issues/issue1": {"id": "issue1", "title": "Force push incident"},
91
+ "/api/users/defaults": {"permission_set": "reviewer", "permissions": ROLE["permissions"]},
92
+ "/api/usage/windows": {"windows": [{"from": "2026-09-01", "to": "2026-10-01",
93
+ "usage": {"events_ingested": 300, "evaluations": 40}}]},
94
+ "/api/offers/me": {"offers": [{"offer_key": "team_offer", "tier": "team",
95
+ "status": "available", "expires_at": "2026-10-31"}]},
96
+ "/api/team-trial/me": {"active": True, "ends_at": "2026-10-31"},
97
+ "/api/offers/claim": {"claimed": True, "grant": {"tier": "team", "expires_at": "2026-10-31"}},
98
+ "/api/settings/model-context-windows": {"model_context_windows": [
99
+ {"model_id": "provider/model", "context_window": 64000, "updated_at": "2026-10-14"}]},
100
+ "/api/otel/status": {"enabled": True, "events_24h": 10, "last_received_at": "2026-10-14",
101
+ "content_missing": False, "services": [{"service_name": "support-service", "events": 10}]},
102
+ "/api/integrations/slack": {"connected": True, "team_name": "Reliability", "channel_name": "agent-alerts"},
103
+ "/api/agent/home-brief": {"greeting": "Hello", "summary": "One session needs attention", "blocks": [
104
+ {"title": "Traffic", "kind": "table", "sql": "SELECT 1"},
105
+ {"title": "Next steps", "kind": "note", "text": "Inspect the failed session"}]},
106
+ }.get(path)
107
+ if path == "/api/evaluator-definitions":
108
+ data = [DEFINITION] if method == "GET" else {**DEFINITION, **body, "id": "new-definition"}
109
+ elif path.startswith("/api/evaluator-definitions/") and path.endswith("dry-run"):
110
+ data = DRY_RUN
111
+ elif path.startswith("/api/evaluator-definitions/"):
112
+ data = {**DEFINITION, **body}
113
+ elif path == "/api/agent/compose-eval":
114
+ data = {k: v for k, v in DEFINITION.items() if k not in ("id", "created_at", "enabled")}
115
+ elif path in ("/api/agent/compose-sql", "/api/agent/policy-author"):
116
+ draft = {"sql": "SELECT agent, events FROM traffic"} if path.endswith("compose-sql") else {
117
+ "source": "customPolicies.add({ name: 'no-force-push' });",
118
+ "verdict": {"tone": "good", "reason": "Blocks force pushes"}, "tests": {"ran": 4, "passed": 4}}
119
+ return httpx.Response(200, text="data: " + json.dumps({"type": "done", "result": draft}) + "\n\n",
120
+ headers={"content-type": "text/event-stream"})
121
+ elif path == "/api/orgs":
122
+ data = {"id": "org1", "name": body["name"], "slug": body["slug"], "plan": "free"}
123
+ elif path == "/api/users/user1":
124
+ data = {"deleted": True}
125
+ elif path == "/api/settings/otel.ingest_enabled":
126
+ data = {"key": "otel.ingest_enabled", **body}
127
+ elif method == "DELETE":
128
+ data = {"deleted": True}
129
+ elif method in ("POST", "PUT"):
130
+ if path == "/api/dashboards":
131
+ data = {**DASHBOARD, **body}
132
+ elif path.endswith("/tiles"):
133
+ data = {**TILE, **body}
134
+ elif path == "/api/permission-sets":
135
+ data = {**ROLE, **body}
136
+ elif path == "/api/audits/contracts":
137
+ data = {**CONTRACT, **body}
138
+ elif path == "/api/settings/model-context-windows":
139
+ data = body
140
+ elif path == "/api/alerts/alert1/issues":
141
+ data = {"id": "issue1", "state": "open", **body}
142
+ assert data is not None, f"missing realistic fake payload for {method} {path}"
143
+ return httpx.Response(200, json=data)
144
+
145
+
146
+ CASES = [
147
+ (["evaluators", "list"], "tool_usage"),
148
+ (["evaluators", "show", "tool_usage@1"], "support-bot"),
149
+ (["evaluators", "create", "--key", "tool_usage", "--name", "Tool usage", "--code", "{code}"], "code"),
150
+ (["evaluators", "create", "--key", "resolution", "--name", "Resolution", "--judge",
151
+ "--criteria", "Resolved the request", "--step", "Assess relevance", "--threshold", "0.7"], "LLM judge"),
152
+ (["evaluators", "create", "--key", "frustration", "--name", "Frustration", "--jev", "noul",
153
+ "--instructions", "Read user messages", "--true", "Frustrated", "--false", "Calm"], "Jev noul"),
154
+ (["evaluators", "create", "--key", "quality", "--name", "Quality", "--jev", "score",
155
+ "--instructions", "Grade answer", "--level", "Wrong", "--level", "Partial", "--level", "Correct"], "level 3"),
156
+ (["evaluators", "deploy", "tool_usage", "--code", "{code}", "--dry-run-on", "run-check"], "disabled v1"),
157
+ (["evaluators", "draft", "Measure tools", "--out", "{draft}"], "create --file"),
158
+ (["evaluators", "dry-run", "--code", "{code}", "--session", "run-check"], "Seven tool calls"),
159
+ (["evaluators", "enable", "tool_usage@1"], "● enabled"),
160
+ (["evaluators", "disable", "tool_usage@1"], "○ disabled"),
161
+ (["evaluators", "label", "tool_usage@1", "--add", "qa"], "qa"),
162
+ (["evaluators", "instances"], "failproof-managed"),
163
+ (["evaluators", "schema"], "tool_name"),
164
+ (["evaluators", "jobs"], "queued"),
165
+ (["evaluators", "backfill", "--from", "2026-10-01T00:00:00Z", "--to", "2026-10-08T00:00:00Z", "--dry-run"], "12"),
166
+ (["dashboards", "list"], "Quality"),
167
+ (["dashboards", "show", "board1"], "Traffic"),
168
+ (["dashboards", "create", "--name", "Quality"], "Quality"),
169
+ (["dashboards", "update", "board1", "--name", "New quality"], "board1"),
170
+ (["dashboards", "delete", "board1"], "board1"),
171
+ (["dashboards", "tiles", "board1", "list"], "Traffic"),
172
+ (["dashboards", "tiles", "board1", "add", "--query", "query1", "--chart", "bar"], "query1"),
173
+ (["dashboards", "tiles", "board1", "update", "tile1", "--title", "New traffic"], "tile1"),
174
+ (["dashboards", "tiles", "board1", "remove", "tile1"], "tile1"),
175
+ (["dashboards", "tiles", "board1", "layout", "--file", "{layout}"], "tiles updated"),
176
+ (["roles", "list"], "reviewer"),
177
+ (["roles", "show", "reviewer"], "queries:run"),
178
+ (["roles", "create", "--name", "reviewer", "--permission", "events:read"], "events:read"),
179
+ (["roles", "update", "reviewer", "--add", "users:read"], "users:read"),
180
+ (["roles", "delete", "reviewer"], "members keep"),
181
+ (["sessions", "show", "run-check"], "Read customer"),
182
+ (["sessions", "export", "run-check", "--out", "{export}"], "file"),
183
+ (["sessions", "export", "run-check"], "run-check"),
184
+ (["sessions", "history", "run-check"], "tool_usage"),
185
+ (["sessions", "re-evaluate", "run-check"], "queued 1 evaluation for run-check"),
186
+ (["events", "latency"], "145"),
187
+ (["sentiment", "summary"], "frustration"),
188
+ (["sentiment", "messages"], "Please try again"),
189
+ (["sentiment", "sessions"], "run-check"),
190
+ (["sentiment", "session", "run-check"], "frustration"),
191
+ (["query", "compose", "Count events"], "SELECT"),
192
+ (["query", "compose", "Count events", "--run"], "support-bot"),
193
+ (["query", "export", "--sql", "SELECT 1", "--format", "csv"], "support-bot"),
194
+ (["query", "export", "--sql", "SELECT 1", "--format", "json"], "support-bot"),
195
+ (["query", "export", "--sql", "SELECT 1", "--out", "{export}"], "file"),
196
+ (["alerts", "recipients"], "ops@example.test"),
197
+ (["alerts", "issues", "alert1"], "Too many failures"),
198
+ (["alerts", "issues", "alert1", "--create", "--title", "Failure"], "Failure"),
199
+ (["audits", "contracts", "list"], "support-bot"),
200
+ (["audits", "contracts", "show", "contract1"], "Resolve requests"),
201
+ (["audits", "contracts", "create", "--agent-id", "support-bot", "--body", "Resolve requests"], "Resolve requests"),
202
+ (["audits", "contracts", "update", "contract1", "--body", "New purpose"], "support-bot"),
203
+ (["policies", "author", "Prevent force pushes"], "nothing published"),
204
+ (["policies", "backtest", "{policy}"], "Force push blocked"),
205
+ (["policies", "candidacy", "--issue", "issue1"], "can be blocked"),
206
+ (["agent", "brief"], "One session needs attention"),
207
+ (["orgs", "create", "--name", "Reliability"], "orgs switch"),
208
+ (["users", "delete", "user1"], "user1"),
209
+ (["users", "defaults"], "queries:run"),
210
+ (["plan"], "team_offer"),
211
+ (["plan", "claim", "team_offer"], "team_offer"),
212
+ (["otel", "status"], "support-service"),
213
+ (["otel", "enable"], "● enabled"),
214
+ (["otel", "disable"], "○ disabled"),
215
+ (["slack", "status"], "Reliability"),
216
+ (["slack", "disconnect"], "disconnected"),
217
+ (["settings", "context-windows", "list"], "64000"),
218
+ (["settings", "context-windows", "set", "provider/model", "64000"], "provider/model"),
219
+ (["settings", "context-windows", "delete", "provider/model"], "override removed"),
220
+ ]
221
+
222
+
223
+ @pytest.mark.parametrize("width", [80, 140])
224
+ @pytest.mark.parametrize("argv,expected", CASES, ids=lambda item: " ".join(item[:3]) if isinstance(item, list) else "")
225
+ def test_deliberate_human_command_views(logged_in, runner, monkeypatch, tmp_path, width, argv, expected):
226
+ monkeypatch.setenv("COLUMNS", str(width))
227
+ code, policy, layout = tmp_path / "source.py", tmp_path / "policy.mjs", tmp_path / "layout.json"
228
+ code.write_text(CODE)
229
+ policy.write_text("customPolicies.add({})")
230
+ layout.write_text(json.dumps({"tiles": [{"id": "tile1", "pos_x": 0, "pos_y": 0, "pos_w": 6, "pos_h": 4}]}))
231
+ paths = {"code": code, "policy": policy, "layout": layout, "draft": tmp_path / "draft.json", "export": tmp_path / "export.json"}
232
+ args = [v.format(**paths) for v in argv]
233
+ with respx.mock as mock:
234
+ mock.route().mock(side_effect=response)
235
+ result = runner.invoke(app, args)
236
+ assert result.exit_code == 0, result.output
237
+ assert expected in result.stdout, result.stdout
238
+ if argv[:2] != ["query", "export"] or "--out" in argv or "json" in argv:
239
+ assert result.stdout.lstrip().startswith("failproof ai"), result.stdout
240
+ assert "\nFIELD\n" not in result.stdout and "\nVALUE\n" not in result.stdout
241
+ for raw in ('"eval_key":', '"created":', '"permissions":', "dashboards []", "{'"):
242
+ assert raw not in result.stdout, result.stdout
243
+ if argv[:2] == ["evaluators", "deploy"]:
244
+ assert result.stdout.index("run-check · passed") < result.stdout.index("deployed tool_usage v2")
245
+ # Optional transcript capture for the manager, outside the repository.
246
+ if os.environ.get("FP_HUMAN_ARTIFACTS"):
247
+ directory = Path(os.environ["FP_HUMAN_ARTIFACTS"])
248
+ directory.mkdir(parents=True, exist_ok=True)
249
+ name = "-".join(argv[:3]).replace("/", "_").replace("{", "").replace("}", "")
250
+ with (directory / f"{width}-{name}.txt").open("a", encoding="utf-8") as dest:
251
+ dest.write("$ fp " + " ".join(argv) + "\n" + result.output + "\n")
252
+
253
+
254
+ @pytest.mark.parametrize("width", [80, 140])
255
+ @pytest.mark.parametrize("argv,path,next_step", [
256
+ (["dashboards", "list"], "/api/dashboards", "fp dashboards create --name"),
257
+ (["roles", "list"], "/api/permission-sets", "fp roles create"),
258
+ (["evaluators", "list"], "/api/evaluator-definitions", "fp evaluators create --help"),
259
+ (["sentiment", "messages"], "/api/events/sentiment/messages", "sentiment.enabled"),
260
+ (["settings", "context-windows", "list"], "/api/settings/model-context-windows", "context-windows set"),
261
+ ])
262
+ def test_empty_views_give_next_step(logged_in, runner, monkeypatch, width, argv, path, next_step):
263
+ monkeypatch.setenv("COLUMNS", str(width))
264
+ data = {"dashboards": []} if argv[0] == "dashboards" else {"items": []} if argv[0] == "sentiment" else []
265
+ with respx.mock as mock:
266
+ mock.get(BASE + path).respond(200, json=data)
267
+ result = runner.invoke(app, argv)
268
+ assert result.exit_code == 0, result.output
269
+ assert "no " in result.stdout
270
+ assert next_step in result.stdout
271
+ assert "[]" not in result.stdout
272
+
273
+
274
+ @pytest.mark.parametrize("width", [80, 140])
275
+ def test_roles_permissions_wrap_as_readable_text(logged_in, runner, monkeypatch, width):
276
+ monkeypatch.setenv("COLUMNS", str(width))
277
+ from fp_cli.permissions import ASSIGNABLE_PERMISSIONS
278
+ with respx.mock as mock:
279
+ mock.get(BASE + "/api/permission-sets/admin").respond(200, json={
280
+ "name": "admin", "is_builtin": True, "user_count": 1,
281
+ "permissions": ASSIGNABLE_PERMISSIONS,
282
+ })
283
+ result = runner.invoke(app, ["roles", "show", "admin"])
284
+ assert result.exit_code == 0
285
+ assert "permissions" in result.stdout
286
+ assert "[" not in result.stdout
287
+ assert "events:read" in result.stdout and "policies:write" in result.stdout