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
@@ -1,8 +1,8 @@
1
1
  """FailproofAI Cloud Jev / regex / both policies in `fp` (CONTRACT C4, C6).
2
2
 
3
3
  `fp policies publish --kind regex|jev|both [--source] [--semantic]`, list/show
4
- carrying the kind and the Jev check names, `fp fleet deploy --jev-mode`, and
5
- machines showing the policy errors they report.
4
+ carrying the kind and the Jev check names, deploys (no Jev mode: Cloud derives
5
+ it), and machines showing the policy errors they report.
6
6
  """
7
7
  from __future__ import annotations
8
8
 
@@ -144,7 +144,7 @@ def test_semantic_refuses_what_is_not_a_declaration_list(text, needle):
144
144
  parse_semantic(text)
145
145
 
146
146
 
147
- # ── deploy plan: --jev-mode ───────────────────────────────────────────────────
147
+ # ── deploy plan: no Jev mode ──────────────────────────────────────────────────
148
148
 
149
149
 
150
150
  def _plan(**kw):
@@ -153,38 +153,16 @@ def _plan(**kw):
153
153
  return plan_deploy("m1", **base)
154
154
 
155
155
 
156
- def test_jev_mode_alone_is_a_change_and_is_carried_in_the_plan():
157
- plan = _plan(jev_mode="observe")
158
- assert not plan.is_noop and plan.jev_mode_changes
159
- assert plan.to_dict()["jevMode"] == {"from": "local", "to": "observe", "changed": True}
160
-
161
-
162
- def test_the_same_jev_mode_again_is_a_noop():
163
- assert _plan(jev_mode="observe", current_jev_mode="observe").is_noop
164
- assert _plan(jev_mode="local").is_noop # already local
165
-
166
-
167
- def test_local_hands_the_mode_back_to_the_machine():
168
- plan = _plan(jev_mode="local", current_jev_mode="enforce")
169
- assert plan.jev_mode_changes and plan.jev_mode_after is None
170
- assert plan.to_dict()["jevMode"] == {"from": "enforce", "to": "local", "changed": True}
171
-
172
-
173
- def test_no_jev_mode_says_nothing_and_changes_nothing():
174
- plan = _plan(add=["a"], current_jev_mode="enforce")
156
+ def test_a_plan_never_carries_a_jev_mode():
157
+ with pytest.raises(TypeError):
158
+ _plan(jev_mode="observe")
159
+ plan = _plan(add=["a"])
175
160
  assert plan.is_noop and "jevMode" not in plan.to_dict()
176
161
 
177
162
 
178
- def test_an_unknown_jev_mode_is_a_usage_error():
179
- with pytest.raises(RefUsageError, match="not one of off, observe, enforce, local"):
180
- _plan(jev_mode="shadow")
181
-
182
-
183
- def test_observe_on_a_jev_policy_is_refused_with_the_mode_that_does_it():
163
+ def test_observe_applies_to_a_jev_policy_like_any_other():
184
164
  kinds = version_kinds([PolicyVersion.from_dict(dict(SERVER_JEV, id="j", version=2))])
185
- with pytest.raises(RefUsageError, match="use --jev-mode observe"):
186
- _plan(add=["j:observe"], kinds=kinds)
187
- # enforce is fine, and so is observe on a `both` policy (it applies to the JS half).
165
+ assert _plan(add=["j:observe"], kinds=kinds).added[0].effect == "observe"
188
166
  assert _plan(add=["j"], kinds=kinds).added[0].effect == "enforce"
189
167
  both = version_kinds([PolicyVersion.from_dict(dict(SERVER_BOTH, id="j", version=2))])
190
168
  assert _plan(add=["j:observe"], kinds=both).added[0].effect == "observe"
@@ -213,14 +191,13 @@ def test_publish_still_refuses_jev_fields_in_the_javascript(monkeypatch):
213
191
  assert "--kind both --semantic" in (exc.value.hint or "")
214
192
 
215
193
 
216
- def test_deploy_sends_jev_mode_only_when_given(monkeypatch):
194
+ def test_deploy_never_sends_a_jev_mode(monkeypatch):
217
195
  bodies = []
218
196
  monkeypatch.setattr(client, "_request_json",
219
197
  lambda ctx, method, path, json_body=None: bodies.append(json_body) or {})
220
198
  client.deploy_policies(None, "m1", [PolicyRef("a", 1)])
221
- client.deploy_policies(None, "m1", [PolicyRef("a", 1)], jev_mode="local")
222
199
  assert "jevMode" not in bodies[0]
223
- assert bodies[1]["jevMode"] == "local"
200
+ assert not hasattr(client, "set_jev_mode")
224
201
 
225
202
 
226
203
  # ── the commands, over HTTP ───────────────────────────────────────────────────
@@ -365,25 +342,7 @@ def _fleet_routes(current_policies, jev_mode=None, errors=None):
365
342
 
366
343
 
367
344
  @respx.mock
368
- def test_fleet_deploy_jev_mode_alone_changes_only_the_mode_through_its_own_route(logged_in, runner):
369
- # CONTRACT C9.1: a mode change sent as a deploy rewrote the set the CLI had
370
- # read — which leaves out disabled policies' assignments, so they were lost.
371
- _fleet_routes([{"id": "no-force-push", "version": 2, "effect": "enforce"}])
372
- deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1")
373
- mode = respx.put(f"{BASE}/api/enforcement/deployments/m1/jev-mode").mock(return_value=httpx.Response(
374
- 200, json={"machineId": "m1", "deployment": 6, "jevMode": "observe", "changed": True,
375
- "policies": [{"id": "no-force-push", "version": 2, "effect": "enforce"}]}))
376
- result = runner.invoke(app, ["--json", "fleet", "deploy", "m1", "--jev-mode", "observe", "--yes"])
377
- assert result.exit_code == 0, result.output
378
- assert not deploy.called
379
- assert json.loads(mode.calls.last.request.content) == {"jevMode": "observe"}
380
- out = json.loads(result.stdout)
381
- assert out["applied"] is True and out["jevModeOnly"] is True
382
- assert out["plan"]["jevMode"]["to"] == "observe" and out["deployment"]["deployment"] == 6
383
-
384
-
385
- @respx.mock
386
- def test_fleet_deploy_adds_a_jev_policy_and_leaves_the_mode_alone_without_the_flag(logged_in, runner):
345
+ def test_fleet_deploy_adds_a_jev_policy_and_sends_no_mode(logged_in, runner):
387
346
  _fleet_routes([], jev_mode="enforce")
388
347
  put = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
389
348
  200, json={"machineId": "m1", "deployment": 6, "jevMode": "enforce",
@@ -395,13 +354,26 @@ def test_fleet_deploy_adds_a_jev_policy_and_leaves_the_mode_alone_without_the_fl
395
354
 
396
355
 
397
356
  @respx.mock
398
- def test_fleet_deploy_refuses_observe_on_a_jev_policy_before_writing(logged_in, runner):
357
+ def test_fleet_deploy_observes_a_jev_policy(logged_in, runner):
399
358
  _fleet_routes([])
400
- put = respx.put(f"{BASE}/api/enforcement/deployments/m1")
359
+ put = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
360
+ 200, json={"machineId": "m1", "deployment": 6,
361
+ "policies": [{"id": "secrets", "version": 1, "effect": "observe"}]}))
401
362
  result = runner.invoke(app, ["fleet", "deploy", "m1", "--add", "secrets:observe", "--yes"])
402
- assert result.exit_code == 2
403
- assert "--jev-mode observe" in result.output + (result.stderr or "")
404
- assert not put.called
363
+ assert result.exit_code == 0, result.output
364
+ assert json.loads(put.calls.last.request.content) == {
365
+ "policies": [{"id": "secrets", "version": 1, "effect": "observe"}]}
366
+
367
+
368
+ @pytest.mark.parametrize("argv", [
369
+ ["fleet", "jev-mode", "m1", "observe"],
370
+ ["fleet", "deploy", "m1", "--jev-mode", "observe"],
371
+ ])
372
+ def test_the_jev_mode_cannot_be_set_by_hand(logged_in, runner, argv):
373
+ with respx.mock(assert_all_called=False) as mock:
374
+ result = runner.invoke(app, argv)
375
+ assert result.exit_code == 2, result.output
376
+ assert not mock.calls
405
377
 
406
378
 
407
379
  @respx.mock
@@ -424,7 +396,7 @@ def test_fleet_show_and_list_show_the_jev_mode_and_the_reported_errors(logged_in
424
396
  assert js["deployment"]["jevMode"] == "observe"
425
397
 
426
398
 
427
- # ── Review fixes: the Jev mode route (C9.1), rollback and history (m6), help (m7) ──
399
+ # ── Rollback and history show the mode Cloud derived (m6) ────────────────────
428
400
 
429
401
 
430
402
  def _fleet_many(deployments, machines=None):
@@ -443,133 +415,6 @@ def _fleet_many(deployments, machines=None):
443
415
  return_value=httpx.Response(200, json=[SERVER_BOTH, SERVER_JEV, SERVER_REGEX_OLD]))
444
416
 
445
417
 
446
- def _mode_route(machine_id, mode, deployment=6, status=200):
447
- body = ({"machineId": machine_id, "deployment": deployment, "jevMode": None if mode == "local" else mode,
448
- "changed": True, "policies": []} if status == 200
449
- else {"error": f"machine {machine_id} has no deployment", "code": "no_deployment"})
450
- return respx.put(f"{BASE}/api/enforcement/deployments/{machine_id}/jev-mode").mock(
451
- return_value=httpx.Response(status, json=body))
452
-
453
-
454
- def test_set_jev_mode_sends_only_the_mode(monkeypatch):
455
- seen = {}
456
-
457
- def fake(ctx, method, path, json_body=None, **kw):
458
- seen.update(method=method, path=path, body=json_body)
459
- return {"machineId": "m1", "deployment": 9, "jevMode": "enforce", "changed": False, "policies": []}
460
-
461
- monkeypatch.setattr(client, "_request_json", fake)
462
- dep, changed = client.set_jev_mode(object(), "m1", "enforce")
463
- assert seen == {"method": "PUT", "path": "/api/enforcement/deployments/m1/jev-mode", "body": {"jevMode": "enforce"}}
464
- assert (dep.deployment, dep.jev_mode, changed) == (9, "enforce", False)
465
-
466
-
467
- @respx.mock
468
- def test_fleet_deploy_jev_mode_on_a_machine_with_no_deployment_deploys_its_first(logged_in, runner):
469
- respx.get(f"{BASE}/api/enforcement/machines").mock(return_value=httpx.Response(
470
- 200, json=[{"machineId": "m1", "deployment": None, "deployed": False}]))
471
- respx.get(f"{BASE}/api/enforcement/deployments").mock(return_value=httpx.Response(200, json=[]))
472
- respx.get(f"{BASE}/api/enforcement/policies").mock(return_value=httpx.Response(200, json=[SERVER_JEV]))
473
- mode = respx.put(f"{BASE}/api/enforcement/deployments/m1/jev-mode")
474
- deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
475
- 200, json={"machineId": "m1", "deployment": 1, "jevMode": "observe", "policies": []}))
476
- result = runner.invoke(app, ["--json", "fleet", "deploy", "m1", "--jev-mode", "observe", "--yes"])
477
- assert result.exit_code == 0, result.output
478
- assert not mode.called
479
- assert json.loads(deploy.calls.last.request.content) == {"policies": [], "jevMode": "observe"}
480
-
481
-
482
- @respx.mock
483
- def test_fleet_deploy_with_a_set_change_and_a_mode_still_deploys_both_together(logged_in, runner):
484
- _fleet_routes([{"id": "no-force-push", "version": 2, "effect": "enforce"}])
485
- mode = respx.put(f"{BASE}/api/enforcement/deployments/m1/jev-mode")
486
- deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
487
- 200, json={"machineId": "m1", "deployment": 6, "jevMode": "enforce", "policies": []}))
488
- result = runner.invoke(app, ["fleet", "deploy", "m1", "--add", "secrets", "--jev-mode", "enforce", "--yes"])
489
- assert result.exit_code == 0, result.output
490
- assert not mode.called
491
- assert json.loads(deploy.calls.last.request.content)["jevMode"] == "enforce"
492
-
493
-
494
- @respx.mock
495
- def test_fleet_jev_mode_sets_named_machines_and_skips_one_already_there(logged_in, runner):
496
- _fleet_many({"m1": None, "m2": "enforce", "m3": "observe"})
497
- r1, r2, r3 = _mode_route("m1", "enforce"), _mode_route("m2", "enforce"), _mode_route("m3", "enforce", 7)
498
- deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1")
499
- result = runner.invoke(app, ["--json", "fleet", "jev-mode", "m1", "m2", "m3", "enforce"])
500
- assert result.exit_code == 0, result.output
501
- assert r1.called and r3.called and not r2.called and not deploy.called
502
- assert json.loads(r1.calls.last.request.content) == {"jevMode": "enforce"}
503
- out = json.loads(result.stdout)
504
- assert out["mode"] == "enforce" and out["applied"] is True
505
- rows = {r["machineId"]: r for r in out["machines"]}
506
- assert rows["m1"]["from"] == "local" and rows["m1"]["to"] == "enforce" and rows["m1"]["deployment"] == 6
507
- assert rows["m2"]["changed"] is False
508
- assert rows["m3"]["from"] == "observe" and rows["m3"]["deployment"] == 7
509
-
510
-
511
- @respx.mock
512
- def test_fleet_jev_mode_all_covers_every_machine_with_a_deployment_only(logged_in, runner):
513
- _fleet_many({"m1": "enforce", "m2": "off"}, machines=["m1", "m2", "m-new"])
514
- r1, r2 = _mode_route("m1", "local"), _mode_route("m2", "local")
515
- fresh = respx.put(f"{BASE}/api/enforcement/deployments/m-new/jev-mode")
516
- result = runner.invoke(app, ["fleet", "jev-mode", "--all", "local"])
517
- assert result.exit_code == 0, result.output
518
- assert r1.called and r2.called and not fresh.called
519
- assert json.loads(r1.calls.last.request.content) == {"jevMode": "local"}
520
- assert "enforce → local" in result.stdout and "off → local" in result.stdout
521
-
522
-
523
- @respx.mock
524
- def test_fleet_jev_mode_when_nothing_changes_writes_nothing(logged_in, runner):
525
- _fleet_many({"m1": "observe"})
526
- route = _mode_route("m1", "observe")
527
- result = runner.invoke(app, ["fleet", "jev-mode", "m1", "observe"])
528
- assert result.exit_code == 0, result.output
529
- assert not route.called
530
- assert "already has jev mode observe" in result.output + (result.stderr or "")
531
-
532
-
533
- @pytest.mark.parametrize("argv, needle", [
534
- (["fleet", "jev-mode"], "pass the mode"),
535
- (["fleet", "jev-mode", "m1", "shadow"], "is not a Jev mode"),
536
- (["fleet", "jev-mode", "observe"], "name at least one machine"),
537
- (["fleet", "jev-mode", "--all", "m1", "observe"], "do not name machines"),
538
- ])
539
- def test_fleet_jev_mode_usage_errors_exit_2_before_any_request(logged_in, runner, argv, needle):
540
- with respx.mock(assert_all_called=False) as mock:
541
- result = runner.invoke(app, argv)
542
- assert result.exit_code == 2, result.output
543
- assert needle in result.output + (result.stderr or "")
544
- assert not mock.calls
545
-
546
-
547
- @respx.mock
548
- def test_fleet_jev_mode_refuses_a_named_machine_with_no_deployment_before_writing(logged_in, runner):
549
- _fleet_many({"m1": None}, machines=["m1", "m2"])
550
- r1 = _mode_route("m1", "observe")
551
- result = runner.invoke(app, ["fleet", "jev-mode", "m1", "m2", "observe"])
552
- assert result.exit_code == 1, result.output
553
- text = result.output + (result.stderr or "")
554
- assert "m2 has no deployment" in text and "fp fleet deploy <machine> --jev-mode observe" in text
555
- assert not r1.called
556
-
557
-
558
- @respx.mock
559
- def test_fleet_jev_mode_goes_on_past_a_machine_that_fails_and_names_it(logged_in, runner):
560
- _fleet_many({"m1": None, "m2": None})
561
- _mode_route("m1", "observe", status=404)
562
- r2 = _mode_route("m2", "observe")
563
- result = runner.invoke(app, ["--json", "fleet", "jev-mode", "m1", "m2", "observe"])
564
- assert result.exit_code == 1, result.output
565
- assert r2.called
566
- # The result document, then the error document the exit code comes with.
567
- out, end = json.JSONDecoder().raw_decode(result.stdout)
568
- rows = {r["machineId"]: r for r in out["machines"]}
569
- assert "error" in rows["m1"] and rows["m2"]["deployment"] == 6 and out["applied"] is False
570
- assert "could not be changed on m1" in result.stdout[end:] + (result.stderr or "")
571
-
572
-
573
418
  @respx.mock
574
419
  def test_fleet_rollback_says_which_jev_mode_it_restores(logged_in, runner):
575
420
  _fleet_many({"m1": "off"})
@@ -607,15 +452,6 @@ def test_fleet_history_shows_each_generations_jev_mode_and_a_mode_only_change(lo
607
452
  assert any("local" in ln for ln in lines if "#5" in ln)
608
453
 
609
454
 
610
- def test_the_help_says_enforce_blocks_and_that_the_mode_is_machine_wide(runner):
611
- for argv in (["fleet", "deploy", "--help"], ["fleet", "jev-mode", "--help"]):
612
- result = runner.invoke(app, argv)
613
- assert result.exit_code == 0, result.output
614
- flat = " ".join(result.stdout.split())
615
- assert "BLOCK" in flat, argv
616
- assert "installed" in flat and "pack" in flat, argv
617
-
618
-
619
455
  # ── CONTRACT C10.6: Jev checks run on FailproofAI Cloud (help text only) ──────
620
456
 
621
457
 
@@ -623,7 +459,6 @@ def test_the_help_says_enforce_blocks_and_that_the_mode_is_machine_wide(runner):
623
459
  "argv",
624
460
  [
625
461
  ["fleet", "deploy", "--help"],
626
- ["fleet", "jev-mode", "--help"],
627
462
  ["policies", "publish", "--help"],
628
463
  ],
629
464
  )
@@ -637,7 +472,7 @@ def test_the_help_says_jev_checks_run_on_cloud_and_nothing_is_installed(runner,
637
472
  assert "AgentEye" not in flat, argv
638
473
 
639
474
 
640
- @pytest.mark.parametrize("argv", [["fleet", "show", "--help"], ["fleet", "jev-mode", "--help"]])
475
+ @pytest.mark.parametrize("argv", [["fleet", "show", "--help"]])
641
476
  def test_the_help_names_the_reasons_a_machine_cannot_ask(runner, argv):
642
477
  flat = " ".join(runner.invoke(app, argv).stdout.split())
643
478
  assert "transcripts_disabled" in flat, argv
@@ -0,0 +1,116 @@
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
+ DASHBOARD = {"name": "Quality", "description": "Keep", "definition": {"filters": {"env": "prod"}}}
10
+ TILE = {"id": "t1", "query_id": "q1", "title": "Before", "chart_type": "table",
11
+ "chart_config": {}, "param_bindings": {}, "pos_x": 0, "pos_y": 0, "pos_w": 6, "pos_h": 4}
12
+ RESULT = {"columns": [{"name": "agent", "type": "String"}, {"name": "n", "type": "Int64"}],
13
+ "rows": [["agent-001", 42]], "truncated": False, "elapsed_ms": 1}
14
+ CASES = [
15
+ (["dashboards", "list"], "GET", "/dashboards", {"dashboards": []}),
16
+ (["dashboards", "show", "d1"], "GET", "/dashboards/d1", {"dashboard": DASHBOARD, "tiles": []}),
17
+ (["dashboards", "create", "--name", "Quality"], "POST", "/dashboards", {"id": "d1"}),
18
+ (["dashboards", "update", "d1", "--name", "New"], "PUT", "/dashboards/d1", {"id": "d1"}),
19
+ (["dashboards", "delete", "d1"], "DELETE", "/dashboards/d1", {"deleted": True}),
20
+ (["dashboards", "tiles", "d1", "list"], "GET", "/dashboards/d1/tiles", {"tiles": [TILE]}),
21
+ (["dashboards", "tiles", "d1", "add", "--query", "q1", "--chart", "bar"], "POST", "/dashboards/d1/tiles", {"id": "t1"}),
22
+ (["dashboards", "tiles", "d1", "update", "t1", "--title", "After"], "PUT", "/dashboards/d1/tiles/t1", {"id": "t1"}),
23
+ (["dashboards", "tiles", "d1", "remove", "t1"], "DELETE", "/dashboards/d1/tiles/t1", {"deleted": True}),
24
+ (["dashboards", "tiles", "d1", "layout", "--file", "{layout}"], "PUT", "/dashboards/d1/tiles/layout", {"updated": 1}),
25
+ (["sessions", "show", "s1"], "GET", "/events/summary", {"events": [], "next_cursor": None}),
26
+ (["sessions", "export", "s1"], "GET", "/sessions/s1/export", {"session_id": "s1", "events": []}),
27
+ (["sessions", "history", "s1"], "GET", "/sessions/s1/evaluation-history", []),
28
+ (["events", "latency"], "GET", "/events/latency_aggregate", {"p95": 42}),
29
+ (["sentiment", "summary"], "GET", "/events/sentiment", {"positive": 1}),
30
+ (["sentiment", "messages"], "GET", "/events/sentiment/messages", []),
31
+ (["sentiment", "sessions"], "GET", "/events/sentiment/sessions", []),
32
+ (["sentiment", "session", "s1"], "GET", "/events/sentiment/sessions/s1", {}),
33
+ (["query", "export", "q1", "--format", "json"], "POST", "/queries/run", RESULT),
34
+ (["query", "export", "--sql", "SELECT 1", "--format", "csv"], "POST", "/queries/run", RESULT),
35
+ ]
36
+
37
+
38
+ @pytest.fixture
39
+ def files(tmp_path):
40
+ path = tmp_path / "layout.json"
41
+ path.write_text(json.dumps({"tiles": [{k: v for k, v in TILE.items() if k in ("id", "pos_x", "pos_y", "pos_w", "pos_h")}]}))
42
+ return {"layout": str(path)}
43
+
44
+
45
+ @pytest.mark.parametrize("key_mode", [False, True])
46
+ @pytest.mark.parametrize("argv,method,path,response", CASES, ids=lambda v: " ".join(v) if isinstance(v, list) else str(v))
47
+ def test_observability_commands(logged_in, runner, files, key_mode, argv, method, path, response):
48
+ prefix = "/v1" if key_mode else "/api"
49
+ with respx.mock(assert_all_called=False) as mock:
50
+ mock.get(f"{BASE}{prefix}/dashboards/d1").respond(200, json={"dashboard": DASHBOARD, "tiles": []})
51
+ mock.get(f"{BASE}{prefix}/dashboards/d1/tiles").respond(200, json={"tiles": [TILE]})
52
+ mock.get(f"{BASE}{prefix}/sessions").respond(200, json={"sessions": [{"session_id": "s1"}]})
53
+ route = mock.request(method, f"{BASE}{prefix}{path}").respond(200, json=response)
54
+ result = runner.invoke(app, ["--json", *(["--api-key", "key"] if key_mode else []), *[v.format(**files) for v in argv]])
55
+ assert result.exit_code == 0, result.output
56
+ assert route.called
57
+ json.loads(result.stdout)
58
+ if argv[:2] == ["dashboards", "create"]:
59
+ assert json.loads(route.calls[0].request.content) == {"name": "Quality", "description": "", "definition": {}}
60
+ if argv[:2] == ["dashboards", "update"]:
61
+ assert json.loads(route.calls[0].request.content) == {**DASHBOARD, "name": "New"}
62
+ if argv[:4] == ["dashboards", "tiles", "d1", "update"]:
63
+ assert json.loads(route.calls[0].request.content) == {k: ("After" if k == "title" else v) for k, v in TILE.items() if k != "id"}
64
+
65
+
66
+ @pytest.mark.parametrize("argv,method,path,response", CASES)
67
+ @pytest.mark.parametrize("key_mode", [False, True])
68
+ def test_observability_404(logged_in, runner, files, key_mode, argv, method, path, response):
69
+ with respx.mock as mock:
70
+ mock.route().respond(404, json={"error": "unknown route"})
71
+ result = runner.invoke(app, ["--json", *(["--api-key", "key"] if key_mode else []), *[v.format(**files) for v in argv]])
72
+ assert result.exit_code == 6, result.output
73
+ assert "not available on this FailproofAI Cloud yet" in result.stdout
74
+
75
+
76
+ def test_query_compose(logged_in, runner):
77
+ stream = 'data: {"type":"done","sql":"SELECT 1"}\n\n'
78
+ with respx.mock as mock:
79
+ draft = mock.post(f"{BASE}/api/agent/compose-sql").respond(200, text=stream, headers={"content-type": "text/event-stream"})
80
+ run = mock.post(f"{BASE}/api/queries/run").respond(200, json=RESULT)
81
+ result = runner.invoke(app, ["--json", "query", "compose", "Count events", "--run"])
82
+ assert result.exit_code == 0, result.output
83
+ assert json.loads(draft.calls[0].request.content) == {"intent": "Count events"}
84
+ assert json.loads(run.calls[0].request.content) == {"sql": "SELECT 1", "params": []}
85
+
86
+
87
+ def test_query_compose_key_denial(logged_in, runner):
88
+ with respx.mock as mock:
89
+ result = runner.invoke(app, ["--json", "--api-key", "key", "query", "compose", "Count events"])
90
+ assert not mock.calls
91
+ assert result.exit_code == 2
92
+ assert "fp login" in result.stdout
93
+
94
+
95
+ def test_exports_write_files(logged_in, runner, tmp_path):
96
+ path = tmp_path / "data.csv"
97
+ with respx.mock as mock:
98
+ mock.post(f"{BASE}/api/queries/run").respond(200, json=RESULT)
99
+ result = runner.invoke(app, ["--json", "query", "export", "--sql", "SELECT 1", "--out", str(path)])
100
+ assert result.exit_code == 0
101
+ assert path.read_text().splitlines() == ["agent,n", "agent-001,42"]
102
+
103
+
104
+ @pytest.mark.parametrize("argv", [
105
+ ["dashboards", "create", "--name", ""],
106
+ ["dashboards", "update", "d1"],
107
+ ["dashboards", "tiles", "d1", "add", "--query", "q1", "--chart", "invalid"],
108
+ ["sentiment", "messages", "--limit", "0"],
109
+ ["query", "export", "--sql", "SELECT 1", "--format", "xml"],
110
+ ["query", "export", "q1", "--sql", "SELECT 1"],
111
+ ])
112
+ def test_validation(logged_in, runner, argv):
113
+ with respx.mock as mock:
114
+ result = runner.invoke(app, ["--json", *argv])
115
+ assert not mock.calls
116
+ assert result.exit_code == 2, result.output
@@ -0,0 +1,141 @@
1
+ import pytest
2
+ import respx
3
+
4
+ from fp_cli import client
5
+ from fp_cli.errors import NotFoundError
6
+
7
+
8
+ @pytest.mark.parametrize("family", [
9
+ "evaluator-definitions", "evaluator-instances", "evaluator-schema-profile",
10
+ "evaluation-jobs", "dashboards", "otel", "offers", "team-trial",
11
+ ])
12
+ def test_added_v1_families(family):
13
+ assert client._v1_path(f"/api/{family}") == f"/v1/{family}"
14
+
15
+
16
+ @pytest.mark.parametrize("method", ["GET", "POST", "PATCH", "DELETE"])
17
+ @pytest.mark.parametrize("key_mode", [False, True])
18
+ def test_cloud_request_degrades(method, key_mode):
19
+ ctx = client.ClientContext(
20
+ "http://dash.test", token=None if key_mode else "tok",
21
+ api_key="key" if key_mode else None,
22
+ auth_mode=client.AuthMode.API_KEY if key_mode else client.AuthMode.SESSION,
23
+ )
24
+ prefix = "v1" if key_mode else "api"
25
+ with respx.mock:
26
+ route = respx.request(method, f"http://dash.test/{prefix}/dashboards").respond(404, json={})
27
+ with pytest.raises(NotFoundError, match="not available on this FailproofAI Cloud yet") as exc:
28
+ client.cloud_request(ctx, method, "/api/dashboards")
29
+ assert exc.value.exit_code == 6
30
+ headers = route.calls[0].request.headers
31
+ assert ("authorization" in headers) is key_mode
32
+ assert ("cookie" in headers) is not key_mode
33
+
34
+
35
+ def test_new_capability_html_404_degrades():
36
+ ctx = client.ClientContext("http://dash.test", api_key="key", auth_mode=client.AuthMode.API_KEY)
37
+ with respx.mock as mock:
38
+ mock.get("http://dash.test/v1/dashboards").respond(404, text="<html>old Cloud</html>", headers={"content-type": "text/html"})
39
+ with pytest.raises(NotFoundError, match="not available on this FailproofAI Cloud yet") as exc:
40
+ client.cloud_request(ctx, "GET", "/api/dashboards")
41
+ assert exc.value.exit_code == 6
42
+
43
+
44
+ @pytest.mark.parametrize("key_mode", [False, True])
45
+ @pytest.mark.parametrize("method,path,body,params", [
46
+ ("PATCH", "/api/evaluator-definitions/stale-id", {"enabled": False}, None),
47
+ ("GET", "/api/sessions/missing/export", None, None),
48
+ ("POST", "/api/sessions/missing/re-evaluate", None, None),
49
+ ("GET", "/api/dashboards/missing/tiles", None, None),
50
+ ("DELETE", "/api/dashboards/board/tiles/stale-tile", None, None),
51
+ ("GET", "/api/events/sentiment/sessions/missing", None, None),
52
+ ("GET", "/api/audits/contracts/stale-id", None, None),
53
+ ("POST", "/api/queries/run", {"query_id": "missing"}, None),
54
+ ("POST", "/api/agent/answer", {"callId": "missing", "decision": "reject"}, None),
55
+ ("DELETE", "/api/settings/model-context-windows", None, {"model": "missing"}),
56
+ ])
57
+ def test_record_404_is_not_route_unavailable(key_mode, method, path, body, params):
58
+ ctx = client.ClientContext(
59
+ "http://dash.test", token=None if key_mode else "tok",
60
+ api_key="key" if key_mode else None,
61
+ auth_mode=client.AuthMode.API_KEY if key_mode else client.AuthMode.SESSION,
62
+ )
63
+ if key_mode and path.startswith("/api/agent/"):
64
+ pytest.skip("the agent has no key route; tested via session-only command guards")
65
+ wire_path = client._v1_path(path) if key_mode else path
66
+ with respx.mock as mock:
67
+ mock.request(method, f"http://dash.test{wire_path}").respond(404, json={})
68
+ with pytest.raises(NotFoundError) as exc:
69
+ client.cloud_request(ctx, method, path, body=body, params=params)
70
+ assert exc.value.exit_code == 6
71
+ assert "Record not found" in exc.value.message
72
+ assert "not available on this FailproofAI Cloud yet" not in exc.value.message
73
+
74
+
75
+ @pytest.mark.parametrize("response,route_unknown", [
76
+ ({"code": "route_not_found"}, True),
77
+ ({"error": {"code": "route_not_found"}}, True),
78
+ ({"error": "unknown endpoint"}, True),
79
+ ({"message": "Cannot PATCH /evaluator-definitions/stale-id"}, True),
80
+ ({"error": "route does not exist"}, True),
81
+ ({"error": "old Cloud route absent"}, True),
82
+ ({"error": "evaluator definition not found"}, False),
83
+ ({"error": "session not found"}, False),
84
+ ({"error": "query not found"}, False),
85
+ ({"error": "not available in demo"}, False),
86
+ ])
87
+ def test_404_response_overrides_request_shape(response, route_unknown):
88
+ ctx = client.ClientContext("http://dash.test", token="tok")
89
+ with respx.mock as mock:
90
+ mock.patch("http://dash.test/api/evaluator-definitions/stale-id").respond(404, json=response)
91
+ with pytest.raises(NotFoundError) as exc:
92
+ client.cloud_request(ctx, "PATCH", "/api/evaluator-definitions/stale-id", body={"enabled": False})
93
+ assert exc.value.exit_code == 6
94
+ if route_unknown:
95
+ assert exc.value.message == "not available on this FailproofAI Cloud yet"
96
+ else:
97
+ assert exc.value.message == response.get("error", response.get("message"))
98
+
99
+
100
+ @pytest.mark.parametrize("body", [{"code": "record_not_found"}, {"error": "resource_not_found"}])
101
+ def test_record_error_code_on_collection_is_not_unavailable(body):
102
+ ctx = client.ClientContext("http://dash.test", token="tok")
103
+ with respx.mock as mock:
104
+ mock.post("http://dash.test/api/queries/run").respond(404, json=body)
105
+ with pytest.raises(NotFoundError, match="Record not found") as exc:
106
+ client.cloud_request(ctx, "POST", "/api/queries/run", body={"sql": "SELECT 1"})
107
+ assert "not available" not in exc.value.message
108
+
109
+
110
+ def test_collection_with_missing_saved_query_preserves_server_error():
111
+ ctx = client.ClientContext("http://dash.test", token="tok")
112
+ with respx.mock as mock:
113
+ mock.post("http://dash.test/api/queries/run").respond(404, json={"error": "saved query not found"})
114
+ with pytest.raises(NotFoundError, match="saved query not found"):
115
+ client.cloud_request(ctx, "POST", "/api/queries/run", body={"query_id": "stale"})
116
+
117
+
118
+ def test_stream_404_distinguishes_route_and_record():
119
+ ctx = client.ClientContext("http://dash.test", token="tok")
120
+ for error, expected in [
121
+ ({"code": "unknown_route"}, "not available on this FailproofAI Cloud yet"),
122
+ ({"error": "conversation not found"}, "conversation not found"),
123
+ ]:
124
+ with respx.mock as mock:
125
+ mock.post("http://dash.test/api/agent/chat").respond(404, json=error)
126
+ with pytest.raises(NotFoundError, match=expected):
127
+ list(client.cloud_stream(ctx, "/api/agent/chat", {"conversationId": "missing"}))
128
+
129
+
130
+ @pytest.mark.parametrize("path", [
131
+ "/api/agent/conversations/stale",
132
+ "/api/offers/stale",
133
+ "/api/integrations/slack/stale",
134
+ ])
135
+ def test_named_endpoint_families_do_not_hide_record_404s(path):
136
+ ctx = client.ClientContext("http://dash.test", token="tok")
137
+ with respx.mock as mock:
138
+ mock.get(f"http://dash.test{path}").respond(404, json={})
139
+ with pytest.raises(NotFoundError, match="Record not found") as exc:
140
+ client.cloud_request(ctx, "GET", path)
141
+ assert "not available" not in exc.value.message
@@ -522,7 +522,7 @@ def test_sessions_table(logged_in, runner):
522
522
  )
523
523
  result = runner.invoke(app, ["sessions"])
524
524
  assert result.exit_code == 0, result.output
525
- assert "sessions" in result.stdout # the boxed panel title
525
+ assert "SESSIONS" in result.stdout
526
526
  assert "done" in result.stdout # status flattened up from latest_evaluation
527
527
 
528
528
 
@@ -679,7 +679,7 @@ def test_help_command(home, runner):
679
679
  result = runner.invoke(app, ["help"])
680
680
  assert result.exit_code == 0
681
681
  out = result.stdout
682
- assert "Commands" in out # the grouped top-level panel
682
+ assert "COMMANDS" in out
683
683
  # the four purpose groups, in order
684
684
  for g in ("ESSENTIALS", "OBSERVE", "MANAGE", "TOOLS"):
685
685
  assert g in out
@@ -744,7 +744,7 @@ def test_fields_unknown_is_usage_error(logged_in, runner):
744
744
  def test_dash_h_alias_shows_help(home, runner):
745
745
  result = runner.invoke(app, ["-h"])
746
746
  assert result.exit_code == 0
747
- assert "Commands" in result.stdout and "sessions" in result.stdout
747
+ assert "COMMANDS" in result.stdout and "sessions" in result.stdout
748
748
  assert "ESSENTIALS" in result.stdout # the grouped top-level help, via the rich_format_help override
749
749
 
750
750
 
@@ -171,7 +171,7 @@ def test_agent_health_renders_configured_card(logged_in, runner):
171
171
  result = runner.invoke(app, ["agent", "health"])
172
172
  assert result.exit_code == 0, result.output
173
173
  out = result.stdout
174
- assert "assistant" in out and "configured" in out and "claude-x" in out
174
+ assert "ASSISTANT" in out and "configured" in out and "claude-x" in out
175
175
 
176
176
 
177
177
  @respx.mock
@@ -192,7 +192,7 @@ def test_agent_models_renders_default_marker(logged_in, runner):
192
192
  result = runner.invoke(app, ["agent", "models"])
193
193
  assert result.exit_code == 0, result.output
194
194
  out = result.stdout
195
- assert "models" in out and "m-default" in out and "m-fast" in out and "default" in out
195
+ assert "MODELS" in out and "m-default" in out and "m-fast" in out and "default" in out
196
196
 
197
197
 
198
198
  @respx.mock
@@ -213,7 +213,7 @@ def test_agent_chats_renders_table(logged_in, runner):
213
213
  )
214
214
  result = runner.invoke(app, ["agent", "chats"])
215
215
  assert result.exit_code == 0, result.output
216
- assert "chats" in result.stdout and "perf review" in result.stdout
216
+ assert "CHATS" in result.stdout and "perf review" in result.stdout
217
217
  assert "chat-id" in result.stdout and "c1" in result.stdout # full chat-id column by default
218
218
  assert "open one with" not in (result.stderr or "") # footer removed
219
219