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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__167mipqmjprq5._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0b78b0j8iyv6l._.js → [root-of-the-server]__1vofkb2oq7p7e._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1a3tubds7s7ow._.js → _0ouu886szh6yj._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1d98fyfrcaqhy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
- 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
- package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589gp4p60j._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/{1b32-afqacaen.js → 0el9n3h9_zp5m.js} +1 -1
- package/.next/standalone/.next/static/chunks/0k7g_8x3o6gd-.js +1 -0
- package/.next/standalone/.next/static/chunks/{42e8zab314654.js → 0umzct31duk5g.js} +1 -1
- package/.next/standalone/.next/static/chunks/{24w3x2da9sk49.js → 0wobqgosirs8p.js} +1 -1
- package/.next/standalone/.next/static/chunks/12rug3jilwqzs.js +1 -0
- package/.next/standalone/.next/static/chunks/{2-tf6_7u623kd.js → 1kduj74-715jh.js} +1 -1
- package/.next/standalone/.next/static/chunks/2701amhrbsbot.js +1 -0
- package/.next/standalone/.next/static/chunks/{18433qz-8njv3.js → 27cavoc8paelg.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0_zfad4co-t2z.js → 2ve23a7keupjw.js} +1 -1
- package/.next/standalone/.next/static/chunks/3bv6xaoitjpnh.js +6 -0
- package/.next/standalone/app/actions/update-jev-config.ts +5 -3
- package/.next/standalone/app/settings/jev-panel.tsx +1 -1
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +28 -18
- package/.next/standalone/fp-cloud-cli/README.md +127 -9
- package/.next/standalone/fp-cloud-cli/fp_cli/app.py +15 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +199 -34
- package/.next/standalone/fp-cloud-cli/fp_cli/cloud_output.py +523 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/_cloud.py +45 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +3 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/account_cmds.py +171 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +80 -7
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +23 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +2 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/contracts_cmds.py +67 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/dashboards_cmds.py +159 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/evaluators_cmds.py +380 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +14 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +12 -216
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +82 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +4 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +17 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +143 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +70 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/sentiment_cmds.py +49 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +95 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +2 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +21 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +14 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +7 -53
- package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +3 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/evaluator_spec.py +266 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +189 -152
- package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +21 -24
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +124 -23
- package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +2 -1
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +173 -22
- package/.next/standalone/fp-cloud-cli/tests/fake_cloud_smoke.py +312 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_account.py +125 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_agent.py +253 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_human_views.py +287 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +32 -197
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_observability.py +116 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_transport.py +141 -0
- package/.next/standalone/fp-cloud-cli/tests/test_commands.py +3 -3
- package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +3 -3
- package/.next/standalone/fp-cloud-cli/tests/test_evaluator_authoring.py +449 -0
- package/.next/standalone/fp-cloud-cli/tests/test_facets.py +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +15 -0
- package/.next/standalone/fp-cloud-cli/tests/test_operator.py +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_output.py +37 -37
- package/.next/standalone/fp-cloud-cli/tests/test_round4_behaviour.py +144 -0
- package/.next/standalone/fp-cloud-cli/tests/test_screen_language.py +80 -0
- package/.next/standalone/fp-cloud-cli/tests/test_session_reevaluate.py +33 -0
- package/.next/standalone/fp-cloud-cli/tests/test_staging_parity.py +198 -0
- package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +1 -1
- package/.next/standalone/hermes-plugin/__init__.py +11 -0
- package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
- package/.next/standalone/package.json +11 -10
- package/.next/standalone/sdk/python/uv.lock +31 -8
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +68 -176
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +2 -2
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +98 -115
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +3 -3
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +149 -171
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +2 -2
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +25 -25
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +10 -10
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +1760 -638
- package/dist/worker.mjs +426 -76
- package/hermes-plugin/__init__.py +11 -0
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +11 -10
- package/src/audit/open-browser.ts +24 -7
- package/src/hooks/agent-roster.ts +9 -1
- package/src/hooks/agent-skill.ts +206 -0
- package/src/hooks/cloud-connection.ts +27 -51
- package/src/hooks/cloud-introspect.ts +5 -2
- package/src/hooks/cloud-login.ts +248 -0
- package/src/hooks/cloud-managed-policies.ts +54 -4
- package/src/hooks/codex-system-hooks.ts +10 -4
- package/src/hooks/config-status.ts +9 -3
- package/src/hooks/configure-wizard.ts +356 -32
- package/src/hooks/delivery-health.ts +47 -2
- package/src/hooks/effective-reviewers.ts +14 -0
- package/src/hooks/fp-config.ts +19 -0
- package/src/hooks/fp-home.ts +1 -1
- package/src/hooks/handler.ts +43 -5
- package/src/hooks/hook-activity-store.ts +7 -1
- package/src/hooks/integrations.ts +18 -3
- package/src/hooks/jev-activity.ts +117 -1
- package/src/hooks/jev-cli.ts +7 -9
- package/src/hooks/manager.ts +80 -46
- package/src/hooks/policy-evaluator.ts +17 -0
- package/src/hooks/policy-reviewability.ts +4 -2
- package/src/hooks/semantic/cloud-jev.ts +219 -28
- package/src/hooks/semantic/combine.ts +26 -1
- package/src/hooks/semantic/intent.ts +127 -24
- package/src/hooks/semantic/jev-client.ts +1 -1
- package/src/hooks/semantic/jev-config.ts +6 -27
- package/src/hooks/semantic/jev-review.ts +111 -4
- package/src/hooks/tui.ts +280 -0
- package/src/hooks/uninstall-cli.ts +12 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0588st-tpiuia._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_128ebxr83nk6g._.js +0 -3
- package/.next/standalone/.next/static/chunks/0jutamwqz8pg5.js +0 -1
- package/.next/standalone/.next/static/chunks/114hbolvrv94e.js +0 -1
- package/.next/standalone/.next/static/chunks/1p-hsi58i-6h0.js +0 -6
- package/.next/standalone/.next/static/chunks/3-pku9qwv1yfu.js +0 -1
- /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_clientMiddlewareManifest.js +0 -0
- /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 "
|
|
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 "
|
|
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 "
|
|
396
|
+
assert "PERMISSIONS · 2 · Globex" in result.stdout
|
|
397
397
|
assert "events" in result.stdout and "keys" in result.stdout
|
|
398
398
|
|
|
399
399
|
|