failproofai 1.0.1 → 1.0.2-beta.0
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 +6 -6
- 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 +2 -2
- 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 +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- 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 +2 -2
- 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 +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
- 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 +5 -4
- 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 +2 -2
- 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 +4 -3
- 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 +4 -4
- 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 +4 -4
- 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 +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 +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- 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 +66 -22
- package/.next/standalone/.next/server/app/policies/page.js +5 -4
- 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 +2 -2
- 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 +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 +2 -2
- 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 +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +1 -1
- 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/{[externals]__1qzqcio._.js → [externals]__0e32-7c._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__1s462yl._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0neqlu-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t9oqnn._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
- package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -6
- package/.next/standalone/.next/server/chunks/lib_telemetry-id_ts_1k8gj9o._.js +3 -0
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_09d5c79._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1d2g73o._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js} +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0xbw3bu._.js +12 -0
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0spkm68._.js → [root-of-the-server]__07gm3zl._.js} +21 -20
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07tbbf3._.js → [root-of-the-server]__0zig5sv._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__192nhvh._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0lvyuv4._.js → _00e9tnf._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0u5zauv._.js → _12l4a_k._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +13 -13
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_08o7bqo._.js +10 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware.js +1 -1
- 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/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +71 -27
- package/.next/standalone/.next/static/chunks/{37fcpc36x9gpe.js → 0eqky-7n7dgdm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1ide1wl2f7zi1.js → 0sa9v5mhc8861.js} +13 -13
- package/.next/standalone/.next/static/chunks/1mfzsja6ftim5.css +2 -0
- package/.next/standalone/.next/static/chunks/{3k7mxbp9hiom0.js → 1nuqytbta44af.js} +1 -1
- package/.next/standalone/.next/static/chunks/1sl2xmn9inx3y.js +6 -0
- package/.next/standalone/.next/static/chunks/{2_2lhuojq2xop.js → 2aawsjld9ied_.js} +1 -1
- package/.next/standalone/.next/static/chunks/2gn7ut4svofm2.js +1 -0
- package/.next/standalone/.next/static/chunks/{31s1smp-g7gks.js → 2hs7ycvsav7v2.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0k5050ezjvv49.js → 2i2yt3hzjredm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3c808qrfcq0im.js → 35xg6retpe6rp.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2zpp1we2jv6tb.js → 3y54nw0i7dsjl.js} +1 -1
- package/.next/standalone/SECURITY.md +5 -4
- package/.next/standalone/app/actions/get-hooks-config.ts +108 -17
- package/.next/standalone/app/actions/pack-actions.ts +153 -0
- package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
- package/.next/standalone/app/audit/_components/share-templates.ts +13 -13
- package/.next/standalone/app/policies/hooks-client.tsx +320 -14
- package/.next/standalone/components/navbar.tsx +2 -28
- package/.next/standalone/components/reach-developers.tsx +8 -1
- package/.next/standalone/docs-old/agenteye/python-sdk-skill.mdx +1 -1
- package/.next/standalone/docs-old/agenteye/python-sdk.mdx +59 -35
- package/.next/standalone/docs-old/i18n/README.ar.md +1 -0
- package/.next/standalone/docs-old/i18n/README.de.md +1 -0
- package/.next/standalone/docs-old/i18n/README.es.md +1 -0
- package/.next/standalone/docs-old/i18n/README.fr.md +1 -0
- package/.next/standalone/docs-old/i18n/README.he.md +1 -0
- package/.next/standalone/docs-old/i18n/README.hi.md +1 -0
- package/.next/standalone/docs-old/i18n/README.it.md +1 -0
- package/.next/standalone/docs-old/i18n/README.ja.md +1 -0
- package/.next/standalone/docs-old/i18n/README.ko.md +1 -0
- package/.next/standalone/docs-old/i18n/README.pt-br.md +1 -0
- package/.next/standalone/docs-old/i18n/README.ru.md +1 -0
- package/.next/standalone/docs-old/i18n/README.tr.md +1 -0
- package/.next/standalone/docs-old/i18n/README.vi.md +1 -0
- package/.next/standalone/docs-old/i18n/README.zh.md +1 -0
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +431 -0
- package/.next/standalone/fp-cloud-cli/LICENSE +42 -0
- package/.next/standalone/fp-cloud-cli/MANIFEST.in +10 -0
- package/.next/standalone/fp-cloud-cli/README.md +259 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/__init__.py +10 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/__main__.py +4 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +64 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/_context.py +332 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/analytics.py +432 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/analytics_config.py +77 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/analytics_registry.py +83 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/app.py +492 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/auth.py +160 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +1694 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/__init__.py +0 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +214 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +407 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +445 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +1054 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/auth_cmds.py +512 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/errors_cmds.py +190 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/evals_cmds.py +161 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +159 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +416 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/guardrails_cmds.py +148 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +472 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +407 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +63 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +319 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +499 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +378 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +151 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +150 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +35 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +404 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/config.py +330 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/dates.py +78 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +345 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +98 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/models.py +891 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/orgs.py +30 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +6593 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +208 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +290 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/py.typed +0 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/select.py +322 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +53 -0
- package/.next/standalone/fp-cloud-cli/pyproject.toml +106 -0
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +302 -0
- package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +8 -0
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +254 -0
- package/.next/standalone/fp-cloud-cli/tests/__init__.py +0 -0
- package/.next/standalone/fp-cloud-cli/tests/conftest.py +92 -0
- package/.next/standalone/fp-cloud-cli/tests/test_alerting.py +644 -0
- package/.next/standalone/fp-cloud-cli/tests/test_analytics.py +385 -0
- package/.next/standalone/fp-cloud-cli/tests/test_audits.py +950 -0
- package/.next/standalone/fp-cloud-cli/tests/test_auth.py +98 -0
- package/.next/standalone/fp-cloud-cli/tests/test_auth_mode.py +272 -0
- package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +118 -0
- package/.next/standalone/fp-cloud-cli/tests/test_client.py +299 -0
- package/.next/standalone/fp-cloud-cli/tests/test_commands.py +814 -0
- package/.next/standalone/fp-cloud-cli/tests/test_config.py +99 -0
- package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +340 -0
- package/.next/standalone/fp-cloud-cli/tests/test_dates.py +76 -0
- package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +481 -0
- package/.next/standalone/fp-cloud-cli/tests/test_facets.py +170 -0
- package/.next/standalone/fp-cloud-cli/tests/test_failproofai_home.py +722 -0
- package/.next/standalone/fp-cloud-cli/tests/test_fp_home_contract.py +198 -0
- package/.next/standalone/fp-cloud-cli/tests/test_hardening.py +186 -0
- package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +127 -0
- package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +496 -0
- package/.next/standalone/fp-cloud-cli/tests/test_list.py +59 -0
- package/.next/standalone/fp-cloud-cli/tests/test_multivalue.py +183 -0
- package/.next/standalone/fp-cloud-cli/tests/test_no_customer_identifiers.py +168 -0
- package/.next/standalone/fp-cloud-cli/tests/test_operator.py +492 -0
- package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +692 -0
- package/.next/standalone/fp-cloud-cli/tests/test_output.py +1732 -0
- package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +191 -0
- package/.next/standalone/fp-cloud-cli/tests/test_readme_matches_reality.py +125 -0
- package/.next/standalone/fp-cloud-cli/tests/test_review_fixes.py +161 -0
- package/.next/standalone/fp-cloud-cli/tests/test_telemetry_completeness.py +90 -0
- package/.next/standalone/fp-cloud-cli/tests/test_usage.py +115 -0
- package/.next/standalone/fp-cloud-cli/tests/test_v1_origin_diagnostic.py +70 -0
- package/.next/standalone/fp-cloud-cli/tests/test_v1_routing.py +214 -0
- package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +104 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +552 -0
- package/.next/standalone/package.json +29 -22
- package/.next/standalone/sdk/python/CHANGELOG.md +70 -0
- package/.next/standalone/sdk/python/LICENSE +42 -0
- package/.next/standalone/sdk/python/MANIFEST.in +21 -0
- package/.next/standalone/sdk/python/README.md +466 -0
- package/.next/standalone/sdk/python/docs/README.md +249 -0
- package/.next/standalone/sdk/python/docs/_shared/README.md +43 -0
- package/.next/standalone/sdk/python/docs/_shared/__init__.py +195 -0
- package/.next/standalone/sdk/python/docs/crewai/README.md +328 -0
- package/.next/standalone/sdk/python/docs/crewai/examples/quickstart.py +61 -0
- package/.next/standalone/sdk/python/docs/crewai/examples/research_crew.py +92 -0
- package/.next/standalone/sdk/python/docs/langgraph/README.md +481 -0
- package/.next/standalone/sdk/python/docs/langgraph/examples/quickstart.py +51 -0
- package/.next/standalone/sdk/python/docs/langgraph/examples/research_agent.py +86 -0
- package/.next/standalone/sdk/python/docs/langgraph/examples/supervisor_handoff.py +83 -0
- package/.next/standalone/sdk/python/docs/llama_index/README.md +332 -0
- package/.next/standalone/sdk/python/docs/llama_index/examples/quickstart.py +62 -0
- package/.next/standalone/sdk/python/docs/llama_index/examples/research_agent.py +93 -0
- package/.next/standalone/sdk/python/docs/manual/README.md +491 -0
- package/.next/standalone/sdk/python/docs/manual/examples/quickstart.py +57 -0
- package/.next/standalone/sdk/python/docs/manual/examples/research_agent.py +133 -0
- package/.next/standalone/sdk/python/docs/pydantic_ai/README.md +323 -0
- package/.next/standalone/sdk/python/docs/pydantic_ai/examples/quickstart.py +48 -0
- package/.next/standalone/sdk/python/docs/pydantic_ai/examples/research_agent.py +94 -0
- package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +131 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_context.py +218 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_environment.py +78 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_events.py +752 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_resolver.py +118 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_runtime.py +21 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_schema.py +317 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_scopes.py +422 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_writer.py +750 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +274 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/_compat.py +247 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/_core.py +1076 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/crewai.py +1669 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/langchain.py +2151 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/llama_index.py +1588 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/pydantic_ai.py +977 -0
- package/.next/standalone/sdk/python/failproofai_sdk/py.typed +0 -0
- package/.next/standalone/sdk/python/pyproject.toml +92 -0
- package/.next/standalone/sdk/python/skill/SKILL.md +399 -0
- package/.next/standalone/sdk/python/skill/agents/openai.yaml +8 -0
- package/.next/standalone/sdk/python/skill/references/events.md +234 -0
- package/.next/standalone/sdk/python/skill/references/frameworks.md +314 -0
- package/.next/standalone/sdk/python/skill/references/install.md +100 -0
- package/.next/standalone/sdk/python/skill/references/integration.md +352 -0
- package/.next/standalone/sdk/python/tests/__init__.py +0 -0
- package/.next/standalone/sdk/python/tests/conftest.py +169 -0
- package/.next/standalone/sdk/python/tests/integrations/__init__.py +6 -0
- package/.next/standalone/sdk/python/tests/integrations/test_crewai.py +1299 -0
- package/.next/standalone/sdk/python/tests/integrations/test_langchain.py +2338 -0
- package/.next/standalone/sdk/python/tests/integrations/test_llama_index.py +1576 -0
- package/.next/standalone/sdk/python/tests/integrations/test_pydantic_ai.py +1102 -0
- package/.next/standalone/sdk/python/tests/test_context.py +410 -0
- package/.next/standalone/sdk/python/tests/test_docs.py +411 -0
- package/.next/standalone/sdk/python/tests/test_durability.py +1352 -0
- package/.next/standalone/sdk/python/tests/test_encoding.py +571 -0
- package/.next/standalone/sdk/python/tests/test_integrations.py +1127 -0
- package/.next/standalone/sdk/python/tests/test_no_customer_identifiers.py +214 -0
- package/.next/standalone/sdk/python/tests/test_packaging.py +185 -0
- package/.next/standalone/sdk/python/tests/test_resolver_umbrella.py +178 -0
- package/.next/standalone/sdk/python/tests/test_scopes.py +487 -0
- package/.next/standalone/sdk/python/tests/test_sdk.py +923 -0
- package/.next/standalone/sdk/python/tests/test_server_contract.py +650 -0
- package/.next/standalone/sdk/python/tests/test_site_docs.py +371 -0
- package/.next/standalone/sdk/python/tests/test_skill_snippets.py +165 -0
- package/.next/standalone/sdk/python/tests/test_spool_contract.py +287 -0
- package/.next/standalone/sdk/python/tests/test_spool_creation.py +360 -0
- package/.next/standalone/sdk/python/tests/test_wire_format.py +279 -0
- package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +378 -0
- package/.next/standalone/sdk/python/uv.lock +6005 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +77 -21
- package/bin/failproofai.mjs +145 -11
- package/dist/cli.mjs +4482 -2205
- package/dist/index.js +5 -5
- package/dist/worker.mjs +2399 -1978
- package/package.json +29 -22
- package/policy-pack/SHA256SUMS +2 -0
- package/policy-pack/failproofai-pack.json +732 -0
- package/policy-pack/failproofai-pack.mjs +2017 -0
- package/scripts/build-policy-pack.mjs +121 -0
- package/scripts/changelog-section.py +77 -0
- package/scripts/prune-standalone.mjs +3 -0
- package/scripts/python-version.py +232 -0
- package/scripts/translate-docs/cli.ts +21 -0
- package/scripts/translate-docs/mintlify-nav.ts +85 -13
- package/scripts/translate-docs/translator.ts +66 -11
- package/src/audit/cache.ts +40 -3
- package/src/audit/cli.ts +1 -1
- package/src/audit/index.ts +1 -1
- package/src/audit/replay.ts +89 -3
- package/src/audit/report.ts +5 -5
- package/src/audit/schedule-cli.ts +49 -53
- package/src/hooks/builtin-policies.ts +217 -579
- package/src/hooks/cloud-enrollment-cli.ts +66 -27
- package/src/hooks/cloud-managed-policies.ts +6 -1
- package/src/hooks/configure-wizard.ts +1 -1
- package/src/hooks/custom-hooks-loader.ts +89 -6
- package/src/hooks/fp-home.ts +92 -4
- package/src/hooks/fp-reset.ts +26 -2
- package/src/hooks/handler.ts +181 -11
- package/src/hooks/harness-cli.ts +79 -25
- package/src/hooks/hook-activity-store.ts +13 -3
- package/src/hooks/install-prompt.ts +25 -3
- package/src/hooks/manager.ts +418 -172
- package/src/hooks/migrations.ts +2 -2
- package/src/hooks/pack-cli.ts +505 -0
- package/src/hooks/pack-failclosed.ts +189 -0
- package/src/hooks/pack-manifest.ts +344 -0
- package/src/hooks/pack-store.ts +930 -0
- package/src/hooks/policy-catalog.ts +535 -0
- package/src/hooks/policy-evaluator.ts +24 -16
- package/src/hooks/policy-presets.ts +4 -2
- package/src/hooks/policy-registry.ts +8 -2
- package/src/hooks/policy-types.ts +26 -0
- package/src/hooks/session-pause-cli.ts +33 -9
- package/src/hooks/tui.ts +570 -0
- package/src/hooks/types.ts +15 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1ger0a2._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_1iiwm5e._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kc8_v5._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mhqk5a._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pltpn8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qkcyf-._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w0x_ok._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bfex62._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +0 -6
- package/.next/standalone/.next/static/chunks/36fw6yk1s6x_f.js +0 -1
- package/.next/standalone/.next/static/chunks/3tzq0260n17ae.js +0 -6
- package/.next/standalone/.next/static/chunks/3wq9x0c-7ld5d.css +0 -2
- /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"""Dev provisioning: API keys (one-time secret) and saved queries / SQL runner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import pytest
|
|
10
|
+
import respx
|
|
11
|
+
|
|
12
|
+
from fp_cli.app import app
|
|
13
|
+
from fp_cli.commands.keys_cmds import PermissionTokenError, _parse_permissions
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_audit_permissions_are_assignable_and_in_presets():
|
|
17
|
+
from fp_cli.permissions import ALL_PERMISSIONS, PRESETS
|
|
18
|
+
|
|
19
|
+
assert "audits:read" in ALL_PERMISSIONS
|
|
20
|
+
assert "audits:write" in ALL_PERMISSIONS
|
|
21
|
+
assert "audits:read" in PRESETS["read-only"]
|
|
22
|
+
assert "usage:read" in ALL_PERMISSIONS
|
|
23
|
+
assert "usage:read" in PRESETS["read-only"]
|
|
24
|
+
assert "audits:write" in PRESETS["admin"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_policy_permissions_are_assignable_and_in_presets():
|
|
28
|
+
from fp_cli.permissions import ALL_PERMISSIONS, PRESETS
|
|
29
|
+
|
|
30
|
+
assert "policies:read" in ALL_PERMISSIONS
|
|
31
|
+
assert "policies:write" in ALL_PERMISSIONS
|
|
32
|
+
assert "policies:pull" in ALL_PERMISSIONS
|
|
33
|
+
assert "policies:read" in PRESETS["read-only"]
|
|
34
|
+
assert "policies:read" in PRESETS["standard"]
|
|
35
|
+
assert "policies:pull" in PRESETS["admin"]
|
|
36
|
+
|
|
37
|
+
BASE = "http://dash.test"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_parse_permissions_expands_and_dedupes():
|
|
41
|
+
# dotted actions expand to flat slug:action, across tokens, de-duplicated, order preserved
|
|
42
|
+
assert _parse_permissions(["events:read.add", "keys:read.create"]) == [
|
|
43
|
+
"events:read", "events:add", "keys:read", "keys:create"]
|
|
44
|
+
assert _parse_permissions(["events:read", "events:read.add"]) == ["events:read", "events:add"]
|
|
45
|
+
assert _parse_permissions(["events:read.add keys:read"]) == ["events:read", "events:add", "keys:read"] # whitespace in one token
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.parametrize("bad", [["events"], ["events:"], ["events:frobnicate"], []])
|
|
49
|
+
def test_parse_permissions_rejects(bad):
|
|
50
|
+
with pytest.raises(PermissionTokenError):
|
|
51
|
+
_parse_permissions(bad)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_parse_permission_tokens_require_nonempty_flag():
|
|
55
|
+
# The shared parser (in permissions.py) lets users --add/--remove be empty when asked.
|
|
56
|
+
from fp_cli.permissions import PermissionTokenError as PTErr
|
|
57
|
+
from fp_cli.permissions import parse_permission_tokens
|
|
58
|
+
assert parse_permission_tokens([], require_nonempty=False) == []
|
|
59
|
+
assert parse_permission_tokens(None, require_nonempty=False) == []
|
|
60
|
+
with pytest.raises(PTErr):
|
|
61
|
+
parse_permission_tokens([]) # default requires at least one
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# --- keys -------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@respx.mock
|
|
68
|
+
def test_keys_list_json(logged_in, runner):
|
|
69
|
+
respx.get(f"{BASE}/api/keys").mock(
|
|
70
|
+
return_value=httpx.Response(
|
|
71
|
+
200,
|
|
72
|
+
json=[{"id": "k1", "name": "ci", "permissions": ["events:add"], "created_at": "t", "revoked_at": None}],
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
result = runner.invoke(app, ["--json", "keys", "list"])
|
|
76
|
+
assert result.exit_code == 0, result.output
|
|
77
|
+
assert json.loads(result.stdout)["keys"][0]["id"] == "k1"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@respx.mock
|
|
81
|
+
def test_keys_create_generates_secret_and_posts_it(logged_in, runner):
|
|
82
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[])) # no name collision
|
|
83
|
+
route = respx.post(f"{BASE}/api/keys").mock(
|
|
84
|
+
return_value=httpx.Response(201, json={"id": "k9", "name": "ci-bot",
|
|
85
|
+
"permissions": ["events:read", "events:add", "keys:read"], "created_at": "t"})
|
|
86
|
+
)
|
|
87
|
+
# compact --add tokens: `events:read.add` expands to events:read + events:add; `keys:read` too
|
|
88
|
+
result = runner.invoke(app, ["--json", "keys", "create", "ci-bot", "--add", "events:read.add,keys:read"])
|
|
89
|
+
assert result.exit_code == 0, result.output
|
|
90
|
+
body = json.loads(route.calls.last.request.content)
|
|
91
|
+
assert body["name"] == "ci-bot"
|
|
92
|
+
assert body["permissions"] == ["events:add", "events:read", "keys:read"] # expanded + de-duped + sorted
|
|
93
|
+
assert re.fullmatch(r"[0-9a-f]{64}", body["key"]) # CLI-generated 64-hex secret
|
|
94
|
+
assert json.loads(result.stdout)["key"] == body["key"] # secret only in the JSON `key` field
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_keys_create_rejects_unknown_permission(logged_in, runner):
|
|
98
|
+
result = runner.invoke(app, ["keys", "create", "k", "--add", "orgs:admin"])
|
|
99
|
+
assert result.exit_code == 2 # orgs:admin is not assignable
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_keys_create_rejects_human_only_permission(logged_in, runner):
|
|
103
|
+
# keys:update is human-only — rejected client-side for a key (exit 2), not a server 422.
|
|
104
|
+
result = runner.invoke(app, ["keys", "create", "k", "--add", "keys:update"])
|
|
105
|
+
assert result.exit_code == 2
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_keys_create_rejects_malformed_token(logged_in, runner):
|
|
109
|
+
# missing colon / empty action → red error box, exit 2, before any mutation
|
|
110
|
+
assert runner.invoke(app, ["keys", "create", "k", "--add", "events"]).exit_code == 2
|
|
111
|
+
assert runner.invoke(app, ["keys", "create", "k", "--add", "events:"]).exit_code == 2
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@respx.mock
|
|
115
|
+
def test_keys_create_name_collision(logged_in, runner):
|
|
116
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[
|
|
117
|
+
{"id": "k1", "name": "ci-bot", "permissions": [], "created_at": "t", "revoked_at": None}]))
|
|
118
|
+
result = runner.invoke(app, ["--json", "keys", "create", "ci-bot", "--add", "events:read"])
|
|
119
|
+
assert result.exit_code == 2
|
|
120
|
+
assert "already exists" in json.loads(result.stdout)["error"]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _one_key(name="ci-bot", key_id="k1", revoked=False):
|
|
124
|
+
return [{"id": key_id, "name": name, "permissions": ["events:add"], "created_at": "t",
|
|
125
|
+
"revoked_at": ("t" if revoked else None)}]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@respx.mock
|
|
129
|
+
def test_keys_disable_by_name_json(logged_in, runner):
|
|
130
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=_one_key()))
|
|
131
|
+
route = respx.post(f"{BASE}/api/keys/k1/disable").mock(return_value=httpx.Response(200, json={}))
|
|
132
|
+
# name is resolved to the key id; --json non-tty + --yes proceeds (auto-skip rule).
|
|
133
|
+
result = runner.invoke(app, ["--json", "keys", "disable", "ci-bot", "--yes"])
|
|
134
|
+
assert result.exit_code == 0, result.output
|
|
135
|
+
assert route.called # resolved "ci-bot" -> /keys/k1/disable
|
|
136
|
+
assert json.loads(result.stdout) == {"name": "ci-bot", "status": "disabled"}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@respx.mock
|
|
140
|
+
def test_keys_disable_already_disabled_is_noop(logged_in, runner):
|
|
141
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=_one_key(revoked=True)))
|
|
142
|
+
route = respx.post(f"{BASE}/api/keys/k1/disable").mock(return_value=httpx.Response(200, json={}))
|
|
143
|
+
result = runner.invoke(app, ["--json", "keys", "disable", "ci-bot"])
|
|
144
|
+
assert result.exit_code == 0, result.output
|
|
145
|
+
assert not route.called # already disabled → no destructive call
|
|
146
|
+
assert json.loads(result.stdout) == {"name": "ci-bot", "status": "disabled"}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@respx.mock
|
|
150
|
+
def test_keys_disable_not_found_exits_6(logged_in, runner):
|
|
151
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=_one_key()))
|
|
152
|
+
result = runner.invoke(app, ["keys", "disable", "nope", "--yes"])
|
|
153
|
+
assert result.exit_code == 6
|
|
154
|
+
assert "no key named" in (result.stderr or result.output)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@respx.mock
|
|
158
|
+
def test_keys_disable_forbidden_exits_5(logged_in, runner):
|
|
159
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=_one_key()))
|
|
160
|
+
respx.post(f"{BASE}/api/keys/k1/disable").mock(
|
|
161
|
+
return_value=httpx.Response(403, json={"error": "key is part of the configuration"})
|
|
162
|
+
)
|
|
163
|
+
result = runner.invoke(app, ["keys", "disable", "ci-bot", "--yes"])
|
|
164
|
+
assert result.exit_code == 5
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@respx.mock
|
|
168
|
+
def test_keys_regenerate_shows_new_secret(logged_in, runner):
|
|
169
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=_one_key()))
|
|
170
|
+
respx.post(f"{BASE}/api/keys/k1/regenerate").mock(
|
|
171
|
+
return_value=httpx.Response(200, json={"key": "a" * 64})
|
|
172
|
+
)
|
|
173
|
+
result = runner.invoke(app, ["--json", "keys", "regenerate", "ci-bot", "--yes"])
|
|
174
|
+
assert result.exit_code == 0, result.output
|
|
175
|
+
assert json.loads(result.stdout) == {"name": "ci-bot", "key": "a" * 64}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@respx.mock
|
|
179
|
+
def test_keys_list_box(logged_in, runner):
|
|
180
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[
|
|
181
|
+
{"id": "1f58376d", "name": "admin", "permissions": ["a", "b"], "created_at": "2026-06-18T05:14:00Z", "revoked_at": None},
|
|
182
|
+
{"id": "9c22aa01", "name": "old-key", "permissions": ["x"], "created_at": "2026-06-10T00:00:00Z", "revoked_at": "2026-06-12T00:00:00Z"},
|
|
183
|
+
]))
|
|
184
|
+
result = runner.invoke(app, ["keys", "list"])
|
|
185
|
+
assert result.exit_code == 0, result.output
|
|
186
|
+
assert "api keys" in result.stdout and "active first" in result.stdout
|
|
187
|
+
assert "admin" in result.stdout and "active" in result.stdout and "revoked" in result.stdout
|
|
188
|
+
assert result.stdout.index("admin") < result.stdout.index("old-key") # active sorts above revoked
|
|
189
|
+
# footer summary (stderr) counts by status
|
|
190
|
+
assert "2 keys" in (result.stderr or "") and "1 active" in (result.stderr or "") and "1 revoked" in (result.stderr or "")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@respx.mock
|
|
194
|
+
def test_keys_update_incremental_add(logged_in, runner):
|
|
195
|
+
# --add alone is INCREMENTAL — merged into the key's current grants (like users update).
|
|
196
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[
|
|
197
|
+
{"id": "k1", "name": "ci", "permissions": ["events:add"], "created_at": "t", "revoked_at": None}]))
|
|
198
|
+
route = respx.patch(f"{BASE}/api/keys/k1").mock(
|
|
199
|
+
return_value=httpx.Response(200, json={"id": "k1", "name": "ci",
|
|
200
|
+
"permissions": ["events:add", "users:read"], "created_at": "t", "revoked_at": None})
|
|
201
|
+
)
|
|
202
|
+
result = runner.invoke(app, ["--json", "keys", "update", "ci", "--add", "users:read", "--yes"])
|
|
203
|
+
assert result.exit_code == 0, result.output
|
|
204
|
+
# current {events:add} ∪ {users:read} = sorted ["events:add", "users:read"]
|
|
205
|
+
assert json.loads(route.calls.last.request.content) == {"permissions": ["events:add", "users:read"]}
|
|
206
|
+
body = json.loads(result.stdout)
|
|
207
|
+
assert body["added"] == ["users:read"] and body["removed"] == []
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@respx.mock
|
|
211
|
+
def test_keys_update_incremental_remove(logged_in, runner):
|
|
212
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[
|
|
213
|
+
{"id": "k1", "name": "ci", "permissions": ["events:add", "events:read"], "created_at": "t", "revoked_at": None}]))
|
|
214
|
+
route = respx.patch(f"{BASE}/api/keys/k1").mock(
|
|
215
|
+
return_value=httpx.Response(200, json={"id": "k1", "name": "ci",
|
|
216
|
+
"permissions": ["events:read"], "created_at": "t", "revoked_at": None})
|
|
217
|
+
)
|
|
218
|
+
result = runner.invoke(app, ["--json", "keys", "update", "ci", "--remove", "events:add", "--yes"])
|
|
219
|
+
assert result.exit_code == 0, result.output
|
|
220
|
+
assert json.loads(route.calls.last.request.content) == {"permissions": ["events:read"]}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@respx.mock
|
|
224
|
+
def test_keys_update_noop_skips_server(logged_in, runner):
|
|
225
|
+
# adding a grant the key already has → no-op: no PATCH, exit 0.
|
|
226
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[
|
|
227
|
+
{"id": "k1", "name": "ci", "permissions": ["events:add"], "created_at": "t", "revoked_at": None}]))
|
|
228
|
+
route = respx.patch(f"{BASE}/api/keys/k1").mock(return_value=httpx.Response(200, json={}))
|
|
229
|
+
result = runner.invoke(app, ["--json", "keys", "update", "ci", "--add", "events:add", "--yes"])
|
|
230
|
+
assert result.exit_code == 0, result.output
|
|
231
|
+
assert not route.called
|
|
232
|
+
assert json.loads(result.stdout)["added"] == [] and json.loads(result.stdout)["removed"] == []
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_keys_update_requires_a_change_flag(logged_in, runner):
|
|
236
|
+
result = runner.invoke(app, ["keys", "update", "ci"])
|
|
237
|
+
assert result.exit_code == 2 # nothing to update
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@respx.mock
|
|
241
|
+
def test_keys_update_not_found_exits_6(logged_in, runner):
|
|
242
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[
|
|
243
|
+
{"id": "k1", "name": "ci", "permissions": [], "created_at": "t", "revoked_at": None}]))
|
|
244
|
+
result = runner.invoke(app, ["keys", "update", "nope", "--add", "events:read", "--yes"])
|
|
245
|
+
assert result.exit_code == 6
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# --- queries ----------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@respx.mock
|
|
252
|
+
def test_query_list_unwraps_queries_key(logged_in, runner):
|
|
253
|
+
respx.get(f"{BASE}/api/queries").mock(
|
|
254
|
+
return_value=httpx.Response(200, json={"queries": [{"id": "q1", "name": "errs", "sql_text": "select 1"}]})
|
|
255
|
+
)
|
|
256
|
+
result = runner.invoke(app, ["--json", "query", "list"])
|
|
257
|
+
assert result.exit_code == 0, result.output
|
|
258
|
+
assert json.loads(result.stdout)["queries"][0]["id"] == "q1"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@respx.mock
|
|
262
|
+
def test_query_create_posts_body(logged_in, runner):
|
|
263
|
+
respx.get(f"{BASE}/api/queries").mock(return_value=httpx.Response(200, json={"queries": []})) # no collision
|
|
264
|
+
route = respx.post(f"{BASE}/api/queries").mock(
|
|
265
|
+
return_value=httpx.Response(201, json={"id": "q9", "name": "errs", "sql_text": "select 1", "params": []})
|
|
266
|
+
)
|
|
267
|
+
result = runner.invoke(
|
|
268
|
+
app, ["--json", "query", "create", "errs", "--sql", "select 1"] # name is positional now
|
|
269
|
+
)
|
|
270
|
+
assert result.exit_code == 0, result.output
|
|
271
|
+
body = json.loads(route.calls.last.request.content)
|
|
272
|
+
assert body["name"] == "errs"
|
|
273
|
+
assert body["sql_text"] == "select 1"
|
|
274
|
+
assert body["params"] == [] # --param was removed; created queries carry no params
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@respx.mock
|
|
278
|
+
def test_query_create_sql_from_file(logged_in, runner, tmp_path):
|
|
279
|
+
sql_file = tmp_path / "q.sql"
|
|
280
|
+
sql_file.write_text("select count(*) from analytics.events")
|
|
281
|
+
respx.get(f"{BASE}/api/queries").mock(return_value=httpx.Response(200, json={"queries": []}))
|
|
282
|
+
route = respx.post(f"{BASE}/api/queries").mock(
|
|
283
|
+
return_value=httpx.Response(201, json={"id": "q1", "name": "n", "sql_text": "x", "params": []})
|
|
284
|
+
)
|
|
285
|
+
result = runner.invoke(app, ["query", "create", "n", "--sql", f"@{sql_file}"])
|
|
286
|
+
assert result.exit_code == 0, result.output
|
|
287
|
+
assert json.loads(route.calls.last.request.content)["sql_text"] == "select count(*) from analytics.events"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@respx.mock
|
|
291
|
+
def test_query_create_name_collision_exits_2(logged_in, runner):
|
|
292
|
+
respx.get(f"{BASE}/api/queries").mock(return_value=httpx.Response(200, json={"queries": [{"id": "q1", "name": "errs"}]}))
|
|
293
|
+
result = runner.invoke(app, ["--json", "query", "create", "errs", "--sql", "select 1"])
|
|
294
|
+
assert result.exit_code == 2
|
|
295
|
+
assert "already exists" in json.loads(result.stdout)["error"]
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@respx.mock
|
|
299
|
+
def test_query_run_inline_renders_rows(logged_in, runner):
|
|
300
|
+
respx.post(f"{BASE}/api/queries/run").mock(
|
|
301
|
+
return_value=httpx.Response(200, json={"columns": [{"name": "n", "type": "int"}], "rows": [[5]], "truncated": False, "elapsed_ms": 3})
|
|
302
|
+
)
|
|
303
|
+
result = runner.invoke(app, ["--json", "query", "run", "--sql", "select 5 as n"])
|
|
304
|
+
assert result.exit_code == 0, result.output
|
|
305
|
+
data = json.loads(result.stdout)
|
|
306
|
+
assert data["rows"] == [[5]]
|
|
307
|
+
assert data["elapsed_ms"] == 3
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_query_run_requires_sql_or_saved(logged_in, runner):
|
|
311
|
+
result = runner.invoke(app, ["query", "run"])
|
|
312
|
+
assert result.exit_code == 2
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@respx.mock
|
|
316
|
+
def test_query_run_coerces_param_values(logged_in, runner):
|
|
317
|
+
route = respx.post(f"{BASE}/api/queries/run").mock(
|
|
318
|
+
return_value=httpx.Response(200, json={"columns": [], "rows": [], "truncated": False, "elapsed_ms": 1})
|
|
319
|
+
)
|
|
320
|
+
result = runner.invoke(app, ["--json", "query", "run", "--sql", "select $1,$2,$3", "--param", "5", "--param", "true", "--param", "hi"])
|
|
321
|
+
assert result.exit_code == 0, result.output
|
|
322
|
+
assert json.loads(route.calls.last.request.content)["params"] == [5, True, "hi"]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@respx.mock
|
|
326
|
+
def test_query_run_readonly_violation_403(logged_in, runner):
|
|
327
|
+
respx.post(f"{BASE}/api/queries/run").mock(
|
|
328
|
+
return_value=httpx.Response(403, json={"error": "permission denied — only analytics.* views are queryable"})
|
|
329
|
+
)
|
|
330
|
+
result = runner.invoke(app, ["query", "run", "--sql", "select * from pg_user"])
|
|
331
|
+
assert result.exit_code == 5
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@respx.mock
|
|
335
|
+
def test_query_schema_json_splits_nullable(logged_in, runner):
|
|
336
|
+
respx.get(f"{BASE}/api/queries/schema").mock(
|
|
337
|
+
return_value=httpx.Response(200, json={"schema": "analytics", "tables": [
|
|
338
|
+
{"name": "events", "columns": [{"name": "id", "type": "int"}, {"name": "tool_name", "type": "string?"}]}]})
|
|
339
|
+
)
|
|
340
|
+
result = runner.invoke(app, ["--json", "query", "schema"])
|
|
341
|
+
assert result.exit_code == 0, result.output
|
|
342
|
+
doc = json.loads(result.stdout)
|
|
343
|
+
assert doc["schema"] == "analytics"
|
|
344
|
+
cols = {c["column"]: c for c in doc["columns"]}
|
|
345
|
+
assert cols["id"] == {"table": "events", "column": "id", "type": "int", "nullable": False}
|
|
346
|
+
assert cols["tool_name"]["type"] == "string" and cols["tool_name"]["nullable"] is True # ? split off
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@respx.mock
|
|
350
|
+
def test_query_schema_table_filter(logged_in, runner):
|
|
351
|
+
respx.get(f"{BASE}/api/queries/schema").mock(
|
|
352
|
+
return_value=httpx.Response(200, json={"schema": "analytics", "tables": [
|
|
353
|
+
{"name": "events", "columns": [{"name": "id", "type": "int"}]},
|
|
354
|
+
{"name": "evaluations", "columns": [{"name": "status", "type": "string"}]}]})
|
|
355
|
+
)
|
|
356
|
+
result = runner.invoke(app, ["--json", "query", "schema", "evaluations"])
|
|
357
|
+
assert result.exit_code == 0, result.output
|
|
358
|
+
cols = json.loads(result.stdout)["columns"]
|
|
359
|
+
assert {c["table"] for c in cols} == {"evaluations"} # filtered to one table
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@respx.mock
|
|
363
|
+
def test_query_schema_table_not_found_exits_6(logged_in, runner):
|
|
364
|
+
respx.get(f"{BASE}/api/queries/schema").mock(
|
|
365
|
+
return_value=httpx.Response(200, json={"schema": "analytics", "tables": [{"name": "events", "columns": []}]}))
|
|
366
|
+
result = runner.invoke(app, ["query", "schema", "nope"])
|
|
367
|
+
assert result.exit_code == 6
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# --- queries: name-referenced show / run / update / delete ------------------
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@respx.mock
|
|
374
|
+
def test_query_show_by_name_json(logged_in, runner):
|
|
375
|
+
respx.get(f"{BASE}/api/queries").mock(return_value=httpx.Response(200, json={"queries": [
|
|
376
|
+
{"id": "q1", "name": "errs", "description": "d", "sql_text": "select 1", "created_by": "system", "created_at": "t"}]}))
|
|
377
|
+
result = runner.invoke(app, ["--json", "query", "show", "errs"]) # by NAME, not id
|
|
378
|
+
assert result.exit_code == 0, result.output
|
|
379
|
+
assert json.loads(result.stdout)["sql_text"] == "select 1"
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@respx.mock
|
|
383
|
+
def test_query_show_not_found_exits_6(logged_in, runner):
|
|
384
|
+
respx.get(f"{BASE}/api/queries").mock(return_value=httpx.Response(200, json={"queries": [{"id": "q1", "name": "errs"}]}))
|
|
385
|
+
result = runner.invoke(app, ["query", "show", "nope"])
|
|
386
|
+
assert result.exit_code == 6
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@respx.mock
|
|
390
|
+
def test_query_run_saved_by_positional_name(logged_in, runner):
|
|
391
|
+
respx.get(f"{BASE}/api/queries").mock(return_value=httpx.Response(200, json={"queries": [
|
|
392
|
+
{"id": "q1", "name": "errs", "sql_text": "select 1"}]}))
|
|
393
|
+
route = respx.post(f"{BASE}/api/queries/run").mock(
|
|
394
|
+
return_value=httpx.Response(200, json={"columns": [], "rows": [], "truncated": False, "elapsed_ms": 1}))
|
|
395
|
+
result = runner.invoke(app, ["--json", "query", "run", "errs", "--arg", "prod"]) # positional name, no --saved
|
|
396
|
+
assert result.exit_code == 0, result.output
|
|
397
|
+
body = json.loads(route.calls.last.request.content)
|
|
398
|
+
assert body["query_id"] == "q1" # name resolved to id
|
|
399
|
+
assert body["params"] == ["prod"]
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def test_query_run_requires_name_or_sql(logged_in, runner):
|
|
403
|
+
assert runner.invoke(app, ["query", "run"]).exit_code == 2 # neither
|
|
404
|
+
assert runner.invoke(app, ["query", "run", "errs", "--sql", "select 1"]).exit_code == 2 # both
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@respx.mock
|
|
408
|
+
def test_query_run_saved_not_found_exits_6(logged_in, runner):
|
|
409
|
+
respx.get(f"{BASE}/api/queries").mock(return_value=httpx.Response(200, json={"queries": [{"id": "q1", "name": "errs"}]}))
|
|
410
|
+
result = runner.invoke(app, ["query", "run", "ghost"])
|
|
411
|
+
assert result.exit_code == 6
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@respx.mock
|
|
415
|
+
def test_query_run_exec_error_is_clean(logged_in, runner):
|
|
416
|
+
respx.post(f"{BASE}/api/queries/run").mock(
|
|
417
|
+
return_value=httpx.Response(400, json={"error": "Syntax error: unexpected token near 'FORM'"}))
|
|
418
|
+
result = runner.invoke(app, ["query", "run", "--sql", "SELECT * FORM events"])
|
|
419
|
+
assert result.exit_code == 1
|
|
420
|
+
# The underlying DB error is surfaced directly (no opaque "query failed" wrapper).
|
|
421
|
+
assert "Syntax error" in result.stderr
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@respx.mock
|
|
425
|
+
def test_query_update_rename_collision_exits_2(logged_in, runner):
|
|
426
|
+
respx.get(f"{BASE}/api/queries").mock(return_value=httpx.Response(200, json={"queries": [
|
|
427
|
+
{"id": "q1", "name": "errs", "sql_text": "select 1"}, {"id": "q2", "name": "taken", "sql_text": "select 2"}]}))
|
|
428
|
+
result = runner.invoke(app, ["--json", "query", "update", "errs", "--name", "taken", "--yes"])
|
|
429
|
+
assert result.exit_code == 2
|
|
430
|
+
assert "already exists" in json.loads(result.stdout)["error"]
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@respx.mock
|
|
434
|
+
def test_query_update_noop_makes_no_put(logged_in, runner):
|
|
435
|
+
# --description equal to current → nothing actually changes → no PUT, exit 0.
|
|
436
|
+
respx.get(f"{BASE}/api/queries").mock(return_value=httpx.Response(200, json={"queries": [
|
|
437
|
+
{"id": "q1", "name": "errs", "description": "same", "sql_text": "select 1", "params": []}]}))
|
|
438
|
+
put = respx.put(f"{BASE}/api/queries/q1").mock(return_value=httpx.Response(200, json={"id": "q1"}))
|
|
439
|
+
result = runner.invoke(app, ["--json", "query", "update", "errs", "--description", "same", "--yes"])
|
|
440
|
+
assert result.exit_code == 0, result.output
|
|
441
|
+
assert not put.called
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@respx.mock
|
|
445
|
+
def test_query_update_by_name_keeps_omitted_fields(logged_in, runner):
|
|
446
|
+
respx.get(f"{BASE}/api/queries").mock(return_value=httpx.Response(200, json={"queries": [
|
|
447
|
+
{"id": "q1", "name": "errs", "description": "old desc", "sql_text": "select 1",
|
|
448
|
+
"params": [{"name": "e", "type": "text"}]}]}))
|
|
449
|
+
route = respx.put(f"{BASE}/api/queries/q1").mock(
|
|
450
|
+
return_value=httpx.Response(200, json={"id": "q1", "name": "errs", "sql_text": "select 2"}))
|
|
451
|
+
result = runner.invoke(app, ["--json", "query", "update", "errs", "--sql", "select 2", "--yes"])
|
|
452
|
+
assert result.exit_code == 0, result.output
|
|
453
|
+
sent = json.loads(route.calls.last.request.content)
|
|
454
|
+
assert sent["sql_text"] == "select 2" # changed
|
|
455
|
+
assert sent["name"] == "errs" # kept
|
|
456
|
+
assert sent["description"] == "old desc" # kept
|
|
457
|
+
assert sent["params"] == [{"name": "e", "type": "text"}] # kept
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def test_query_update_noop_rejected(logged_in, runner):
|
|
461
|
+
# No fields → usage error before any read/write (no routes mocked).
|
|
462
|
+
result = runner.invoke(app, ["query", "update", "errs", "--yes"])
|
|
463
|
+
assert result.exit_code == 2
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@respx.mock
|
|
467
|
+
def test_query_update_sql_from_stdin_reads_it_once(logged_in, runner):
|
|
468
|
+
# `--sql @-` is stdin, which drains on the first read. Reading it a second time for
|
|
469
|
+
# the request body would send "" — a query silently emptied at exit 0, while the
|
|
470
|
+
# change detection had compared the real text.
|
|
471
|
+
respx.get(f"{BASE}/api/queries").mock(return_value=httpx.Response(200, json={"queries": [
|
|
472
|
+
{"id": "q1", "name": "errs", "description": "d", "sql_text": "select 1", "params": []}]}))
|
|
473
|
+
route = respx.put(f"{BASE}/api/queries/q1").mock(
|
|
474
|
+
return_value=httpx.Response(200, json={"id": "q1", "name": "errs", "sql_text": "select 2"}))
|
|
475
|
+
result = runner.invoke(app, ["--json", "query", "update", "errs", "--sql", "@-", "--yes"],
|
|
476
|
+
input="select 2 from analytics.events")
|
|
477
|
+
assert result.exit_code == 0, result.output
|
|
478
|
+
assert json.loads(route.calls.last.request.content)["sql_text"] == "select 2 from analytics.events"
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@respx.mock
|
|
482
|
+
def test_query_delete_by_name(logged_in, runner):
|
|
483
|
+
respx.get(f"{BASE}/api/queries").mock(return_value=httpx.Response(200, json={"queries": [{"id": "q1", "name": "errs"}]}))
|
|
484
|
+
route = respx.delete(f"{BASE}/api/queries/q1").mock(return_value=httpx.Response(200, json={"deleted": True}))
|
|
485
|
+
result = runner.invoke(app, ["--json", "query", "delete", "errs", "--yes"])
|
|
486
|
+
assert result.exit_code == 0, result.output
|
|
487
|
+
assert route.called
|
|
488
|
+
body = json.loads(result.stdout)
|
|
489
|
+
assert body["deleted"] is True and body["name"] == "errs"
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@respx.mock
|
|
493
|
+
def test_query_delete_not_found_exits_6(logged_in, runner):
|
|
494
|
+
respx.get(f"{BASE}/api/queries").mock(return_value=httpx.Response(200, json={"queries": [{"id": "q1", "name": "errs"}]}))
|
|
495
|
+
result = runner.invoke(app, ["query", "delete", "ghost", "--yes"])
|
|
496
|
+
assert result.exit_code == 6
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""`fp list <thing>` — value discovery behind the dashboard's filter dropdowns."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pytest
|
|
9
|
+
import respx
|
|
10
|
+
|
|
11
|
+
from fp_cli.app import app
|
|
12
|
+
|
|
13
|
+
BASE = "http://dash.test"
|
|
14
|
+
|
|
15
|
+
# friendly `list` name -> the endpoint it must hit
|
|
16
|
+
_CASES = {
|
|
17
|
+
"envs": "/api/events/environments",
|
|
18
|
+
"agents": "/api/events/agent_ids",
|
|
19
|
+
"event_types": "/api/events/event_types",
|
|
20
|
+
"score_filters": "/api/evaluations/score-keys",
|
|
21
|
+
"models": "/api/events/models",
|
|
22
|
+
"hooks": "/api/events/hook_names",
|
|
23
|
+
"tools": "/api/events/tool_names",
|
|
24
|
+
"error_types": "/api/events/error_types",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.parametrize("name,path", list(_CASES.items()))
|
|
29
|
+
@respx.mock
|
|
30
|
+
def test_list_hits_right_endpoint_and_wraps(logged_in, runner, name, path):
|
|
31
|
+
route = respx.get(f"{BASE}{path}").mock(return_value=httpx.Response(200, json=["a", "b"]))
|
|
32
|
+
result = runner.invoke(app, ["--json", "list", name])
|
|
33
|
+
assert result.exit_code == 0, result.output
|
|
34
|
+
assert route.called # the friendly name resolved to the correct endpoint
|
|
35
|
+
assert json.loads(result.stdout) == {"kind": name, "values": ["a", "b"]}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@respx.mock
|
|
39
|
+
def test_list_empty_is_clean(logged_in, runner):
|
|
40
|
+
respx.get(f"{BASE}/api/events/models").mock(return_value=httpx.Response(200, json=[]))
|
|
41
|
+
result = runner.invoke(app, ["--json", "list", "models"])
|
|
42
|
+
assert result.exit_code == 0, result.output
|
|
43
|
+
assert json.loads(result.stdout)["values"] == []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_list_unknown_kind_is_usage_error(logged_in, runner):
|
|
47
|
+
# An undefined subcommand is a clean usage error, not a crash.
|
|
48
|
+
result = runner.invoke(app, ["list", "bogus"])
|
|
49
|
+
assert result.exit_code == 2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@respx.mock
|
|
53
|
+
def test_list_forbidden_exits_5(logged_in, runner):
|
|
54
|
+
# score_filters needs evaluations:read; a 403 maps to the forbidden exit code.
|
|
55
|
+
respx.get(f"{BASE}/api/evaluations/score-keys").mock(
|
|
56
|
+
return_value=httpx.Response(403, json={"error": "forbidden"})
|
|
57
|
+
)
|
|
58
|
+
result = runner.invoke(app, ["list", "score_filters"])
|
|
59
|
+
assert result.exit_code == 5
|