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,449 @@
1
+ import json
2
+ from datetime import datetime, timezone
3
+
4
+ import pytest
5
+ import respx
6
+
7
+ from fp_cli.app import app
8
+
9
+ BASE = "http://dash.test"
10
+ SOURCE = "EvalResult(score=Score(value=0.9), reasoning='ok')"
11
+
12
+
13
+ @pytest.fixture
14
+ def code(tmp_path):
15
+ path = tmp_path / "eval.py"
16
+ path.write_text(SOURCE)
17
+ return str(path)
18
+
19
+
20
+ KINDS = [
21
+ (["--judge", "--criteria", "Grade relevance", "--step", "Assess specificity"], "judge"),
22
+ (["--jev", "noul", "--instructions", "Assess resolution", "--true", "Resolved", "--false", "Unresolved"], "jev"),
23
+ (["--jev", "score", "--instructions", "Grade relevance", "--level", "Poor", "--level", "Fair", "--level", "Good"], "jev"),
24
+ ]
25
+
26
+
27
+ @pytest.mark.parametrize("key_mode", [False, True])
28
+ @pytest.mark.parametrize("flags,kind", KINDS + [([], "code")])
29
+ def test_create_bodies(logged_in, runner, code, key_mode, flags, kind):
30
+ prefix = "v1" if key_mode else "api"
31
+ flags = flags or ["--code", code, "--result-kind", "assertion"]
32
+ with respx.mock as mock:
33
+ route = mock.post(f"{BASE}/{prefix}/evaluator-definitions").respond(201, json={"id": "d1", "enabled": True})
34
+ result = runner.invoke(app, ["--json", *(["--api-key", "key"] if key_mode else []),
35
+ "evaluators", "create", "--key", "quality", "--name", "Quality", *flags])
36
+ assert result.exit_code == 0, result.output
37
+ body = json.loads(route.calls[0].request.content)
38
+ assert body["eval_key"] == "quality"
39
+ assert body["display_name"] == "Quality"
40
+ assert body["eval_version"] == "1"
41
+ assert body["labels"] == []
42
+ if kind == "code":
43
+ assert body["evaluator_source"] == SOURCE
44
+ assert body["result_kind"] == "assertion"
45
+ else:
46
+ assert body["result_kind"] == "score"
47
+ envelope = json.loads(body["evaluator_source"])
48
+ assert envelope["kind"] == kind
49
+ assert envelope["threshold"] is None
50
+ if kind == "judge":
51
+ assert envelope["steps"] == ["Assess specificity"]
52
+
53
+
54
+ @pytest.mark.parametrize("flags", [
55
+ ["--judge", "--criteria", "Grade", "--threshold", "1.5"],
56
+ ["--judge", "--criteria", "Grade", "--threshold", "nan"],
57
+ ["--judge", "--criteria", "Grade", "--result-kind", "metric"],
58
+ ["--judge", "--criteria", "Grade", "--step", "Answer yes/no"],
59
+ ["--jev", "score", "--instructions", "Grade", "--level", "Bad", "--level", "Good"],
60
+ ["--jev", "score", "--instructions", "Grade", "--level", "Bad", "--level", "Bad", "--level", "Good"],
61
+ ["--jev", "noul", "--instructions", "Grade"],
62
+ ["--judge", "--criteria", "Grade", "--dry-run-on", "s1"],
63
+ ["--judge", "--criteria", "Grade", "--code", "-"],
64
+ ["--judge", "--criteria", "Grade", "--timeout", "-1"],
65
+ ])
66
+ def test_validation_before_network(logged_in, runner, flags):
67
+ with respx.mock(assert_all_called=False) as mock:
68
+ catch_all = mock.route().respond(200, json={})
69
+ result = runner.invoke(app, ["--json", "evaluators", "create", "--key", "quality", "--name", "Quality", *flags])
70
+ assert result.exit_code == 2, result.output
71
+ assert catch_all.call_count == 0
72
+
73
+
74
+ @pytest.mark.parametrize("key", ["Bad", "1bad", "bad-key", "../bad", ""])
75
+ def test_key_validation(logged_in, runner, code, key):
76
+ result = runner.invoke(app, ["--json", "evaluators", "create", "--key", key, "--name", "Quality", "--code", code])
77
+ assert result.exit_code == 2
78
+
79
+
80
+ def test_deploy_order_and_dry_run(logged_in, runner, code):
81
+ old = {"id": "d1", "eval_key": "quality", "eval_version": "1", "display_name": "Quality", "enabled": True}
82
+ with respx.mock as mock:
83
+ mock.get(f"{BASE}/v1/evaluator-definitions").respond(200, json=[old])
84
+ dry = mock.post(f"{BASE}/v1/evaluator-definitions/dry-run").respond(200, json={"ok": True})
85
+ create = mock.post(f"{BASE}/v1/evaluator-definitions").respond(201, json={**old, "id": "d2", "eval_version": "2"})
86
+ patch = mock.patch(f"{BASE}/v1/evaluator-definitions/d1").respond(200, json={})
87
+ result = runner.invoke(app, ["--json", "--api-key", "key", "evaluators", "deploy", "quality", "--code", code, "--dry-run-on", "s1"])
88
+ order = [(call.request.method, call.request.url.path) for call in mock.calls]
89
+ assert result.exit_code == 0, result.output
90
+ assert order == [("GET", "/v1/evaluator-definitions"), ("POST", "/v1/evaluator-definitions/dry-run"),
91
+ ("POST", "/v1/evaluator-definitions"), ("PATCH", "/v1/evaluator-definitions/d1")]
92
+ assert json.loads(create.calls[0].request.content)["eval_version"] == "2"
93
+ assert json.loads(patch.calls[0].request.content) == {"enabled": False}
94
+ assert json.loads(dry.calls[0].request.content)["session_ids"] == ["s1"]
95
+ assert json.loads(result.stdout)["disabled"] == ["d1"]
96
+
97
+
98
+ @pytest.mark.parametrize("flags,expect_condition,expect_labels", [
99
+ ([], "session.agent_id == 'support-bot'", ["prod"]),
100
+ (["--condition", "True", "--label", "beta"], "True", ["beta"]),
101
+ ])
102
+ def test_deploy_inherits_condition_and_labels(logged_in, runner, code, flags, expect_condition, expect_labels):
103
+ older = {"id": "d0", "eval_key": "quality", "eval_version": "1", "display_name": "Quality", "enabled": False,
104
+ "created_at": "2026-10-01T00:00:00Z", "condition_source": None, "labels": []}
105
+ latest = {"id": "d1", "eval_key": "quality", "eval_version": "2", "display_name": "Quality", "enabled": True,
106
+ "created_at": "2026-10-02T00:00:00Z", "condition_source": "session.agent_id == 'support-bot'",
107
+ "labels": ["prod"]}
108
+ with respx.mock as mock:
109
+ mock.get(f"{BASE}/v1/evaluator-definitions").respond(200, json=[latest, older])
110
+ create = mock.post(f"{BASE}/v1/evaluator-definitions").respond(201, json={**latest, "id": "d2", "eval_version": "3"})
111
+ mock.patch(f"{BASE}/v1/evaluator-definitions/d1").respond(200, json={})
112
+ result = runner.invoke(app, ["--json", "--api-key", "key", "evaluators", "deploy", "quality", "--code", code, *flags])
113
+ assert result.exit_code == 0, result.output
114
+ sent = json.loads(create.calls[0].request.content)
115
+ assert sent["condition_source"] == expect_condition
116
+ assert sent["labels"] == expect_labels
117
+ assert sent["eval_version"] == "3"
118
+
119
+
120
+ @pytest.mark.parametrize("key_mode", [False, True])
121
+ def test_deploy_backfill_uses_utc_z_timestamps(logged_in, runner, code, monkeypatch, key_mode):
122
+ from fp_cli.commands import evaluators_cmds
123
+
124
+ class FixedDateTime(datetime):
125
+ @classmethod
126
+ def now(cls, tz=None):
127
+ assert tz == timezone.utc
128
+ return cls(2026, 10, 14, 12, 34, 56, 123456, tzinfo=tz)
129
+
130
+ monkeypatch.setattr(evaluators_cmds, "datetime", FixedDateTime)
131
+ prefix = "v1" if key_mode else "api"
132
+ with respx.mock as mock:
133
+ mock.get(f"{BASE}/{prefix}/evaluator-definitions").respond(200, json=[
134
+ {"id": "d1", "eval_key": "quality", "eval_version": "1", "enabled": True},
135
+ ])
136
+ mock.post(f"{BASE}/{prefix}/evaluator-definitions").respond(201, json={"id": "d2"})
137
+ mock.patch(f"{BASE}/{prefix}/evaluator-definitions/d1").respond(200, json={})
138
+ backfill = mock.post(f"{BASE}/{prefix}/evaluations/backfill").respond(200, json={"queued": True})
139
+ result = runner.invoke(app, [
140
+ "--json", *(["--api-key", "key"] if key_mode else []),
141
+ "evaluators", "deploy", "quality", "--code", code, "--backfill", "7d",
142
+ ])
143
+ order = [(call.request.method, call.request.url.path) for call in mock.calls]
144
+ assert result.exit_code == 0, result.output
145
+ assert json.loads(backfill.calls[0].request.content) == {
146
+ "from": "2026-10-07T12:34:56Z",
147
+ "to": "2026-10-14T12:34:56Z",
148
+ "eval_key": "quality",
149
+ "dry_run": False,
150
+ }
151
+ assert order[-2:] == [
152
+ ("PATCH", f"/{prefix}/evaluator-definitions/d1"),
153
+ ("POST", f"/{prefix}/evaluations/backfill"),
154
+ ]
155
+
156
+
157
+ def test_failed_create_never_disables(logged_in, runner, code):
158
+ with respx.mock(assert_all_called=False) as mock:
159
+ mock.get(f"{BASE}/api/evaluator-definitions").respond(200, json=[{"id": "d1", "eval_key": "quality", "eval_version": "1"}])
160
+ mock.post(f"{BASE}/api/evaluator-definitions").respond(409, json={"error": "version conflict"})
161
+ patch = mock.patch(f"{BASE}/api/evaluator-definitions/d1").respond(200, json={})
162
+ result = runner.invoke(app, ["--json", "evaluators", "deploy", "quality", "--code", code])
163
+ assert result.exit_code == 1
164
+ assert not patch.called
165
+
166
+
167
+ def test_partial_deploy_is_reported(logged_in, runner, code):
168
+ with respx.mock as mock:
169
+ mock.get(f"{BASE}/api/evaluator-definitions").respond(200, json=[{"id": "d1", "eval_key": "quality", "eval_version": "1"}])
170
+ mock.post(f"{BASE}/api/evaluator-definitions").respond(201, json={"id": "d2"})
171
+ mock.patch(f"{BASE}/api/evaluator-definitions/d1").respond(403, json={})
172
+ result = runner.invoke(app, ["--json", "evaluators", "deploy", "quality", "--code", code])
173
+ assert result.exit_code == 5
174
+ assert "Published quality@2" in result.stderr
175
+ assert "d2" in result.stderr
176
+
177
+
178
+ def test_draft_round_trip(logged_in, runner, tmp_path):
179
+ body = {"eval_key": "quality", "display_name": "Quality", "eval_version": "1",
180
+ "result_kind": "score", "labels": [], "evaluator_source": SOURCE}
181
+ path = tmp_path / "draft.json"
182
+ with respx.mock as mock:
183
+ mock.post(f"{BASE}/api/agent/compose-eval").respond(200, json={"spec": body})
184
+ result = runner.invoke(app, ["--json", "evaluators", "draft", "Grade quality", "--out", str(path)])
185
+ assert result.exit_code == 0, result.output
186
+ assert json.loads(path.read_text()) == body
187
+ with respx.mock as mock:
188
+ route = mock.post(f"{BASE}/v1/evaluator-definitions").respond(201, json={"id": "d1"})
189
+ result = runner.invoke(app, ["--json", "--api-key", "key", "evaluators", "create", "--file", str(path)])
190
+ assert result.exit_code == 0, result.output
191
+ assert json.loads(route.calls[0].request.content) == body
192
+
193
+
194
+ def test_draft_denies_key(logged_in, runner):
195
+ with respx.mock(assert_all_called=False) as mock:
196
+ route = mock.route().respond(200, json={})
197
+ result = runner.invoke(app, ["--json", "--api-key", "key", "evaluators", "draft", "Grade"])
198
+ assert result.exit_code == 2
199
+ assert "needs a signed-in session: run `fp login`" in result.stdout
200
+ assert not route.called
201
+
202
+
203
+ @pytest.mark.parametrize("count", [0, 2, 11])
204
+ def test_dry_run_sessions(logged_in, runner, code, count):
205
+ with respx.mock(assert_all_called=False) as mock:
206
+ route = mock.post(f"{BASE}/v1/evaluator-definitions/dry-run").respond(200, json={"ok": True})
207
+ result = runner.invoke(app, ["--json", "--api-key", "key", "evaluators", "dry-run", "--code", code,
208
+ *[v for n in range(count) for v in ("--session", str(n))]])
209
+ assert result.exit_code == (2 if count > 10 else 0), result.output
210
+ if count <= 10:
211
+ assert json.loads(route.calls[0].request.content)["session_ids"] == [str(n) for n in range(count)]
212
+ else:
213
+ assert not route.called
214
+
215
+
216
+ def test_judge_refusal(logged_in, runner):
217
+ result = runner.invoke(app, ["--json", "evaluators", "dry-run", "--judge", "--criteria", "Grade"])
218
+ assert result.exit_code == 2
219
+ assert "Cloud refuses judge dry-runs" in result.stdout
220
+
221
+
222
+ @pytest.mark.parametrize("command,path", [("instances", "evaluator-instances"), ("schema", "evaluator-schema-profile"), ("jobs", "evaluation-jobs")])
223
+ def test_auxiliary_commands(logged_in, runner, command, path):
224
+ with respx.mock as mock:
225
+ mock.get(f"{BASE}/v1/{path}").respond(200, json=[])
226
+ result = runner.invoke(app, ["--json", "--api-key", "key", "evaluators", command])
227
+ assert result.exit_code == 0, result.output
228
+
229
+
230
+ @pytest.mark.parametrize("command,body", [("enable", {"enabled": True}), ("disable", {"enabled": False}),
231
+ ("label", {"labels": ["new"]})])
232
+ def test_patch_commands(logged_in, runner, command, body):
233
+ with respx.mock as mock:
234
+ mock.get(f"{BASE}/v1/evaluator-definitions").respond(200, json=[
235
+ {"id": "d1", "eval_key": "quality", "eval_version": "1", "labels": ["old"]}])
236
+ route = mock.patch(f"{BASE}/v1/evaluator-definitions/d1").respond(200, json=body)
237
+ result = runner.invoke(app, ["--json", "--api-key", "key", "evaluators", command, "quality@1",
238
+ *(["--add", "new", "--remove", "old"] if command == "label" else [])])
239
+ assert result.exit_code == 0, result.output
240
+ assert json.loads(route.calls[0].request.content) == body
241
+
242
+
243
+ def test_list_show_filter(logged_in, runner):
244
+ rows = [{"id": "d1", "eval_key": "quality", "eval_version": "1", "enabled": False},
245
+ {"id": "d2", "eval_key": "quality", "eval_version": "2", "enabled": True}]
246
+ with respx.mock as mock:
247
+ mock.get(f"{BASE}/v1/evaluator-definitions").respond(200, json=rows)
248
+ listing = runner.invoke(app, ["--json", "--api-key", "key", "evaluators", "list", "--key", "quality"])
249
+ showing = runner.invoke(app, ["--json", "--api-key", "key", "evaluators", "show", "quality@1"])
250
+ assert listing.exit_code == showing.exit_code == 0
251
+ assert json.loads(listing.stdout) == [rows[1]]
252
+ assert json.loads(showing.stdout) == rows[0]
253
+
254
+
255
+ @pytest.mark.parametrize("frm,to,ok", [
256
+ ("2026-10-01T00:00:00Z", "2026-10-08T00:00:00Z", True),
257
+ ("2026-10-01T00:00:00Z", "2026-09-01T00:00:00Z", False),
258
+ ("2026-01-01T00:00:00Z", "2026-10-08T00:00:00Z", False),
259
+ ])
260
+ def test_backfill(logged_in, runner, frm, to, ok):
261
+ with respx.mock(assert_all_called=False) as mock:
262
+ route = mock.post(f"{BASE}/v1/evaluations/backfill").respond(200, json={"ok": True})
263
+ result = runner.invoke(app, ["--json", "--api-key", "key", "evaluators", "backfill", "--from", frm, "--to", to, "--dry-run"])
264
+ assert result.exit_code == (0 if ok else 2), result.output
265
+ if ok:
266
+ assert "excludes judge/Jev" not in result.stderr
267
+ assert json.loads(route.calls[0].request.content)["dry_run"] is True
268
+
269
+
270
+ def test_backfill_warns_about_model_budget_without_dry_run(logged_in, runner):
271
+ with respx.mock(assert_all_called=False) as mock:
272
+ route = mock.post(f"{BASE}/v1/evaluations/backfill").respond(200, json={"enqueued": 3})
273
+ result = runner.invoke(app, ["--json", "--api-key", "key", "evaluators", "backfill",
274
+ "--from", "2026-10-01T00:00:00Z", "--to", "2026-10-08T00:00:00Z", "--key", "resolved"])
275
+ assert result.exit_code == 0, result.output
276
+ assert "model budget" in result.stderr
277
+ assert json.loads(route.calls[0].request.content)["eval_key"] == "resolved"
278
+
279
+
280
+ @pytest.mark.parametrize("command", ["list", "instances", "schema", "jobs"])
281
+ def test_evaluator_404(logged_in, runner, command):
282
+ with respx.mock as mock:
283
+ mock.route().respond(404, json={})
284
+ result = runner.invoke(app, ["--json", "evaluators", command])
285
+ assert result.exit_code == 6
286
+ assert "not available on this FailproofAI Cloud yet" in result.stdout
287
+
288
+
289
+ def test_failed_dry_run_blocks_creation(logged_in, runner, code):
290
+ with respx.mock(assert_all_called=False) as mock:
291
+ mock.post(f"{BASE}/api/evaluator-definitions/dry-run").respond(200, json={
292
+ "runs": [{"ok": False, "error_message": "missing result"}], "summary": {"failed": 1}})
293
+ create = mock.post(f"{BASE}/api/evaluator-definitions").respond(201, json={"id": "d1"})
294
+ result = runner.invoke(app, ["--json", "evaluators", "create", "--key", "quality", "--name", "Quality", "--code", code, "--dry-run-on", "s1"])
295
+ assert result.exit_code == 1
296
+ assert not create.called
297
+ assert "was not created" in result.stdout
298
+
299
+
300
+ @pytest.mark.parametrize("draft_kind,fields", [
301
+ ("code", {"evaluator_source": SOURCE}),
302
+ ("judge", {"criteria": "Grade relevance", "steps": ["Assess specificity"], "threshold": 0.7, "evaluator_source": ""}),
303
+ ("jev", {"jevType": "noul", "instructions": "Check resolution", "jevCriteria": {"true": "Resolved", "false": "Unresolved"}, "evaluator_source": ""}),
304
+ ])
305
+ def test_real_dashboard_draft_shape(logged_in, runner, tmp_path, draft_kind, fields):
306
+ draft = {"eval_key": "quality", "display_name": "Quality", "result_kind": "score", "labels": [],
307
+ "condition_source": "", "reasoning": "Draft explanation", "kind": draft_kind, **fields}
308
+ path = tmp_path / "draft.json"
309
+ with respx.mock as mock:
310
+ mock.post(f"{BASE}/api/agent/compose-eval").respond(200, json=draft)
311
+ result = runner.invoke(app, ["--json", "evaluators", "draft", "Grade", "--kind", draft_kind, "--out", str(path)])
312
+ assert result.exit_code == 0, result.output
313
+ body = json.loads(path.read_text())
314
+ assert body["eval_version"] == "1"
315
+ assert "reasoning" not in body and "kind" not in body and "condition_source" not in body
316
+ if draft_kind != "code":
317
+ assert json.loads(body["evaluator_source"])["kind"] == draft_kind
318
+
319
+
320
+ @pytest.mark.parametrize("flags", [
321
+ ["--judge", "--criteria", "Grade", "--step", "Score of 0 on failure"],
322
+ ["--judge", "--criteria", "Grade", "--step", "score: 1 on success"],
323
+ ["--jev", "score", "--instructions", "Grade", "--level", "Poor", "--level", "poor", "--level", "Good"],
324
+ ["--judge", "--criteria", "Grade", "--version", "bad version"],
325
+ ["--judge", "--criteria", "Grade", "--label", "bad\nlabel"],
326
+ ["--judge", "--criteria", "Grade", "--timeout", "3601"],
327
+ ])
328
+ def test_server_validation_edges(logged_in, runner, flags):
329
+ with respx.mock as mock:
330
+ result = runner.invoke(app, ["--json", "evaluators", "create", "--key", "quality", "--name", "Quality", *flags])
331
+ assert not mock.calls
332
+ assert result.exit_code == 2, result.output
333
+
334
+
335
+ @pytest.mark.parametrize("step", [
336
+ "Score 0 if incorrect; score 1 otherwise.",
337
+ "score 1 if the answer is correct",
338
+ "give a score of 0 for a violation",
339
+ "SCORE OF 1 when compliant",
340
+ "score: 0 on failure",
341
+ "score: 1 on success",
342
+ "score\n1 if correct",
343
+ "score : 1 if correct",
344
+ "score=0 when it fails",
345
+ "score one if compliant",
346
+ "Give 0 when the request is unresolved",
347
+ "Give 1 if the answer is correct",
348
+ "Assign a value of 0 when evidence is absent",
349
+ "Award a score of one if it succeeds",
350
+ "Return zero otherwise",
351
+ "Grade pass/fail",
352
+ "Return pass or fail",
353
+ "Answer yes/no",
354
+ "Answer yes or no",
355
+ "Respond true/false",
356
+ "Return true or false",
357
+ "Return false or true",
358
+ "Answer no or yes",
359
+ "Grade fail/pass",
360
+ "Give 0.0 when incorrect",
361
+ "Give 1.0 if correct",
362
+ "Return true if correct, false otherwise",
363
+ "Output yes when the answer is complete",
364
+ ])
365
+ @pytest.mark.parametrize("file_input", [False, True])
366
+ def test_binary_judge_phrasing_is_rejected_before_http(logged_in, runner, tmp_path, step, file_input):
367
+ if file_input:
368
+ path = tmp_path / "judge.json"
369
+ path.write_text(json.dumps({
370
+ "eval_key": "quality", "display_name": "Quality", "eval_version": "1",
371
+ "result_kind": "score",
372
+ "evaluator_source": json.dumps({"kind": "judge", "criteria": "Grade quality", "steps": [step]}),
373
+ }))
374
+ flags = ["--file", str(path)]
375
+ else:
376
+ flags = ["--key", "quality", "--name", "Quality", "--judge", "--criteria", "Grade quality", "--step", step]
377
+ with respx.mock as mock:
378
+ result = runner.invoke(app, ["--json", "evaluators", "create", *flags])
379
+ assert not mock.calls
380
+ assert result.exit_code == 2, result.output
381
+ assert "binary judge steps" in result.stdout
382
+
383
+
384
+ @pytest.mark.parametrize("step", [
385
+ "Return a score between 0 and 1 reflecting how well it did.",
386
+ "The score should reflect 0 to 1 confidence.",
387
+ "Award a high score when evidence supports the answer.",
388
+ "Award a low score when a refund was promised without verification.",
389
+ "Determine whether a policy-verification tool was called before the refund.",
390
+ ])
391
+ def test_continuous_judge_steps_are_accepted(logged_in, runner, step):
392
+ with respx.mock as mock:
393
+ route = mock.post(f"{BASE}/api/evaluator-definitions").respond(201, json={"id": "d1"})
394
+ result = runner.invoke(app, [
395
+ "--json", "evaluators", "create", "--key", "quality", "--name", "Quality",
396
+ "--judge", "--criteria", "Grade quality", "--step", step,
397
+ ])
398
+ assert result.exit_code == 0, result.output
399
+ assert json.loads(json.loads(route.calls[0].request.content)["evaluator_source"])["steps"] == [step]
400
+
401
+
402
+ @pytest.mark.parametrize("server_body", [None, {"error": "binary-phrased steps are invalid"}])
403
+ def test_judge_server_422_is_visible(logged_in, runner, server_body):
404
+ with respx.mock as mock:
405
+ mock.post(f"{BASE}/api/evaluator-definitions").respond(422, json=server_body)
406
+ result = runner.invoke(app, [
407
+ "--json", "evaluators", "create", "--key", "quality", "--name", "Quality",
408
+ "--judge", "--criteria", "Grade quality", "--step", "Assess relevance continuously",
409
+ ])
410
+ assert result.exit_code == 1, result.output
411
+ payload = json.loads(result.stdout)
412
+ assert payload["status"] == 422
413
+ assert payload["error"] == (server_body["error"] if server_body else "Request failed with status 422.")
414
+
415
+
416
+ @pytest.mark.parametrize("key_mode", [False, True])
417
+ def test_stale_evaluator_patch_is_missing_record(logged_in, runner, key_mode):
418
+ prefix = "v1" if key_mode else "api"
419
+ with respx.mock as mock:
420
+ mock.get(f"{BASE}/{prefix}/evaluator-definitions").respond(200, json=[
421
+ {"id": "stale", "eval_key": "quality", "eval_version": "1", "enabled": True},
422
+ ])
423
+ mock.patch(f"{BASE}/{prefix}/evaluator-definitions/stale").respond(404, json={})
424
+ result = runner.invoke(app, ["--json", *(["--api-key", "key"] if key_mode else []), "evaluators", "disable", "quality@1"])
425
+ assert result.exit_code == 6
426
+ assert "Record not found" in result.stdout
427
+ assert "stale" in result.stdout
428
+ assert "not available on this FailproofAI Cloud yet" not in result.stdout
429
+
430
+
431
+ @pytest.mark.parametrize("frm,to", [
432
+ ("2026-10-01T02:00:00+02:00", "2026-10-08T05:30:00+05:30"),
433
+ ("2026-09-30T20:00:00-04:00", "2026-10-07T20:00:00-04:00"),
434
+ (" 2026-10-01T00:00:00.123456Z ", " 2026-10-08T00:00:00.654321Z "),
435
+ ])
436
+ @pytest.mark.parametrize("key_mode", [False, True])
437
+ def test_standalone_backfill_normalizes_utc_z(logged_in, runner, frm, to, key_mode):
438
+ prefix = "v1" if key_mode else "api"
439
+ with respx.mock as mock:
440
+ route = mock.post(f"{BASE}/{prefix}/evaluations/backfill").respond(200, json={})
441
+ result = runner.invoke(app, [
442
+ "--json", *(["--api-key", "key"] if key_mode else []), "evaluators", "backfill",
443
+ "--from", frm, "--to", to, "--key", "quality", "--dry-run",
444
+ ])
445
+ assert result.exit_code == 0, result.output
446
+ assert json.loads(route.calls[0].request.content) == {
447
+ "from": "2026-10-01T00:00:00Z", "to": "2026-10-08T00:00:00Z",
448
+ "eval_key": "quality", "dry_run": True,
449
+ }
@@ -87,7 +87,7 @@ def test_errors_list_table(logged_in, runner):
87
87
  result = runner.invoke(app, ["errors", "--since", "24h"])
88
88
  assert result.exit_code == 0, result.output
89
89
  # the 80-col CliRunner truncates cells; just assert the error-themed box rendered
90
- assert "errors" in result.stdout and "newest first" in result.stdout
90
+ assert "ERRORS" in result.stdout and "newest first" in result.stdout
91
91
 
92
92
 
93
93
  @respx.mock
@@ -125,3 +125,18 @@ def test_every_subcommand_the_hint_column_advertises_actually_exists():
125
125
  bad.append(f"{name}: '{token}' is not a subcommand of `fp {name}`")
126
126
 
127
127
  assert not bad, "the help screen advertises commands that do not exist:\n " + "\n ".join(bad)
128
+
129
+
130
+ def test_group_hints_cover_all_registered_subcommands():
131
+ """New commands must not silently disappear from the hand-maintained index."""
132
+ cli = get_command(app)
133
+ missing = []
134
+ for _section, entries in output._TOP_LEVEL_GROUPS:
135
+ for name, _description, hint in entries:
136
+ registered = set(getattr(cli.commands[name], "commands", {}) or {})
137
+ advertised = {
138
+ sub for sub in registered
139
+ if any(sub == token or token.endswith("*") and sub.startswith(token[:-1]) for token in hint.split())
140
+ }
141
+ missing.extend(f"{name} {sub}" for sub in sorted(registered - advertised))
142
+ assert not missing, f"subcommands missing from the top-level help hints: {missing}"
@@ -388,7 +388,7 @@ def test_settings_list_human_boxed(logged_in, runner):
388
388
  result = runner.invoke(app, ["settings", "list"])
389
389
  assert result.exit_code == 0, result.output
390
390
  out = result.stdout + result.stderr
391
- assert "settings · 3" in out
391
+ assert "SETTINGS · 3" in out
392
392
  for c in ("key", "value", "type", "updated"):
393
393
  assert c in out
394
394
  assert "session_ttl_secs" in out and "86400" in out and "integer" in out
@@ -393,7 +393,7 @@ def test_orgs_perms_table(logged_in, runner):
393
393
  # identity header (name · slug · role) + the shared grouped permissions panel, with the
394
394
  # org NAME in the panel's border title.
395
395
  assert "Globex" in result.stdout and "globex" in result.stdout and "role admin" in result.stdout
396
- assert "permissions · 2 · Globex" in result.stdout
396
+ assert "PERMISSIONS · 2 · Globex" in result.stdout
397
397
  assert "events" in result.stdout and "keys" in result.stdout
398
398
 
399
399