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