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,191 @@
|
|
|
1
|
+
"""Syntax checking and the local policy runner.
|
|
2
|
+
|
|
3
|
+
The gap these close: nothing between an author and a fleet parsed policy source.
|
|
4
|
+
The CLI rejected a NUL byte, the server checked the id and a size ceiling, and a
|
|
5
|
+
file that was not JavaScript at all published, deployed, and failed on the
|
|
6
|
+
machine at enforcement time.
|
|
7
|
+
|
|
8
|
+
Every test here needs `node`, so each skips without it rather than failing —
|
|
9
|
+
node is a real dependency of the check but deliberately not of the CLI.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from fp_cli.policy_check import check_syntax, node_available, run_policy
|
|
16
|
+
|
|
17
|
+
needs_node = pytest.mark.skipif(not node_available(), reason="node is not on PATH")
|
|
18
|
+
|
|
19
|
+
VALID = '''import { customPolicies, allow, deny, instruct } from "failproofai";
|
|
20
|
+
customPolicies.add({
|
|
21
|
+
name: "t", description: "d", match: { events: ["PreToolUse"] },
|
|
22
|
+
fn: async (ctx) => {
|
|
23
|
+
const cmd = String(ctx.toolInput?.command ?? "");
|
|
24
|
+
if (cmd.includes("force")) return deny("nope");
|
|
25
|
+
if (cmd.includes("apply")) return instruct("add a note");
|
|
26
|
+
return allow();
|
|
27
|
+
},
|
|
28
|
+
});'''
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── syntax ───────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@needs_node
|
|
35
|
+
def test_a_real_policy_parses():
|
|
36
|
+
r = check_syntax(VALID)
|
|
37
|
+
assert r.ok and r.checked and r.message == ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@needs_node
|
|
41
|
+
@pytest.mark.parametrize("src,label", [
|
|
42
|
+
("this is not javascript at all {{{", "prose"),
|
|
43
|
+
("export default { name: 'x'", "unclosed brace"),
|
|
44
|
+
("const x = ;", "bad expression"),
|
|
45
|
+
("# python comment\nprint('hi')", "python"),
|
|
46
|
+
("function f( {", "unclosed paren"),
|
|
47
|
+
])
|
|
48
|
+
def test_broken_source_is_caught(src, label):
|
|
49
|
+
r = check_syntax(src)
|
|
50
|
+
assert r.ok is False and r.checked is True, label
|
|
51
|
+
assert r.message, "a refusal with no explanation is not a refusal"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@needs_node
|
|
55
|
+
def test_esm_import_syntax_is_accepted():
|
|
56
|
+
"""Policies are ESM. Checking them as a script would reject every real one."""
|
|
57
|
+
assert check_syntax('import { deny } from "failproofai";\nexport const x = 1;').ok
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@needs_node
|
|
61
|
+
def test_top_level_await_is_valid_in_a_module():
|
|
62
|
+
assert check_syntax('const x = await Promise.resolve(1);\nexport default x;').ok
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@needs_node
|
|
66
|
+
def test_the_error_keeps_the_caret_and_drops_nodes_own_stack():
|
|
67
|
+
"""The caret is the useful part; node's internal frames and version banner
|
|
68
|
+
are node talking about itself inside an error about the user's policy."""
|
|
69
|
+
msg = check_syntax("this is not javascript {{{").message
|
|
70
|
+
assert "^" in msg
|
|
71
|
+
assert "node:internal" not in msg and "Node.js v" not in msg
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_a_missing_node_is_reported_as_unchecked_not_as_passing(monkeypatch):
|
|
75
|
+
""""we did not look" must never render as "we looked and it passed"."""
|
|
76
|
+
monkeypatch.setattr("fp_cli.policy_check.node_available", lambda: False)
|
|
77
|
+
r = check_syntax("anything at all")
|
|
78
|
+
assert r.ok is True and r.checked is False and "node" in r.message
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ── running ──────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@needs_node
|
|
85
|
+
@pytest.mark.parametrize("cmd,expected", [
|
|
86
|
+
("git push --force origin main", "deny"),
|
|
87
|
+
("kubectl apply -f x.yaml", "instruct"),
|
|
88
|
+
("git status", "allow"),
|
|
89
|
+
])
|
|
90
|
+
def test_the_policy_decides_per_input(cmd, expected):
|
|
91
|
+
run = run_policy(VALID, tool="Bash", command=cmd)
|
|
92
|
+
assert run.ok and run.decision == expected
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@needs_node
|
|
96
|
+
def test_the_bare_failproofai_import_resolves():
|
|
97
|
+
"""The file under test is byte-identical to the one that gets published, so
|
|
98
|
+
its bare specifier has to resolve the way node resolves it in production."""
|
|
99
|
+
assert run_policy(VALID, command="git status").ok
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@needs_node
|
|
103
|
+
def test_the_strictest_decision_wins():
|
|
104
|
+
"""One refusal is a refusal regardless of what the other policies said."""
|
|
105
|
+
two = VALID + '''
|
|
106
|
+
customPolicies.add({ name: "always-allow", match: { events: ["PreToolUse"] },
|
|
107
|
+
fn: async () => allow() });'''
|
|
108
|
+
run = run_policy(two, command="git push --force x")
|
|
109
|
+
assert run.decision == "deny" and len(run.results) == 2
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@needs_node
|
|
113
|
+
def test_a_file_registering_nothing_says_so():
|
|
114
|
+
"""An empty result is not an allow — it means the file never called add()."""
|
|
115
|
+
run = run_policy('export const x = 1;')
|
|
116
|
+
assert run.ok is False and "registered no policies" in run.error
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@needs_node
|
|
120
|
+
def test_a_policy_that_throws_is_reported_per_policy_not_as_a_crash():
|
|
121
|
+
boom = '''import { customPolicies } from "failproofai";
|
|
122
|
+
customPolicies.add({ name: "boom", fn: async () => { throw new Error("kaboom"); } });'''
|
|
123
|
+
run = run_policy(boom, command="x")
|
|
124
|
+
assert run.ok and "kaboom" in run.results[0]["error"]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@needs_node
|
|
128
|
+
def test_an_infinite_loop_times_out_instead_of_hanging():
|
|
129
|
+
"""A policy that cannot decide in five seconds cannot sit on a hook either."""
|
|
130
|
+
spin = '''import { customPolicies } from "failproofai";
|
|
131
|
+
customPolicies.add({ name: "spin", fn: async () => { while (true) {} } });'''
|
|
132
|
+
run = run_policy(spin, command="x")
|
|
133
|
+
assert run.ok is False and "finish" in run.error
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@needs_node
|
|
137
|
+
def test_file_path_inputs_reach_the_policy():
|
|
138
|
+
src = '''import { customPolicies, allow, deny } from "failproofai";
|
|
139
|
+
customPolicies.add({ name: "env", fn: async (ctx) =>
|
|
140
|
+
/\\.env/.test(String(ctx.toolInput?.file_path ?? "")) ? deny("no") : allow() });'''
|
|
141
|
+
assert run_policy(src, tool="Write", file_path=".env").decision == "deny"
|
|
142
|
+
assert run_policy(src, tool="Write", file_path="README.md").decision == "allow"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_running_without_node_fails_loudly(monkeypatch):
|
|
146
|
+
monkeypatch.setattr("fp_cli.policy_check.node_available", lambda: False)
|
|
147
|
+
run = run_policy(VALID, command="x")
|
|
148
|
+
assert run.ok is False and "node" in run.error
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_a_slow_machine_is_not_reported_as_a_bad_policy(monkeypatch):
|
|
152
|
+
"""A syntax-check timeout says nothing about the source.
|
|
153
|
+
|
|
154
|
+
`node --check` runs none of the policy, so a timeout is a statement about the
|
|
155
|
+
machine, not the file. It used to share the EXECUTION budget (5s, sized so a
|
|
156
|
+
looping policy fails instead of hanging) and report `ok=False` — which reads
|
|
157
|
+
as "your policy is broken" and made `fp policies publish` refuse a perfectly
|
|
158
|
+
good file because the box was busy. Seen on CI the moment the fp-cloud-cli matrix
|
|
159
|
+
widened to four concurrent interpreters: three legs passed and the fourth
|
|
160
|
+
timed out on this same source.
|
|
161
|
+
|
|
162
|
+
`checked=False` is the honest verdict and the one `SyntaxResult` already
|
|
163
|
+
models — the same shape as node being absent entirely.
|
|
164
|
+
"""
|
|
165
|
+
import subprocess
|
|
166
|
+
|
|
167
|
+
from fp_cli import policy_check
|
|
168
|
+
|
|
169
|
+
def _timeout(*a, **kw):
|
|
170
|
+
raise subprocess.TimeoutExpired(cmd="node", timeout=policy_check._SYNTAX_TIMEOUT_SECS)
|
|
171
|
+
|
|
172
|
+
monkeypatch.setattr(policy_check, "node_available", lambda: True)
|
|
173
|
+
monkeypatch.setattr(subprocess, "run", _timeout)
|
|
174
|
+
|
|
175
|
+
result = check_syntax(VALID)
|
|
176
|
+
assert result.ok is True, "a busy machine must not be reported as a syntax error"
|
|
177
|
+
assert result.checked is False, "'we did not look' must never render as 'we looked and it passed'"
|
|
178
|
+
assert "not checked" in result.message
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_parsing_and_executing_have_separate_budgets():
|
|
182
|
+
"""The tight budget is a product statement about hook latency.
|
|
183
|
+
|
|
184
|
+
It applies to running a policy, where an accidental infinite loop must fail
|
|
185
|
+
the command rather than hang it. Parsing runs no user code, so nothing there
|
|
186
|
+
can loop and the same number is only a cold-start race.
|
|
187
|
+
"""
|
|
188
|
+
from fp_cli import policy_check
|
|
189
|
+
|
|
190
|
+
assert policy_check._TIMEOUT_SECS == 5
|
|
191
|
+
assert policy_check._SYNTAX_TIMEOUT_SECS > policy_check._TIMEOUT_SECS
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""The README ships inside the wheel and IS the PyPI landing page.
|
|
2
|
+
|
|
3
|
+
Nothing else checks it, and it had rotted before this file existed: it documented
|
|
4
|
+
`fp incidents` (renamed to `issues` long ago) and claimed the dashboard URL was
|
|
5
|
+
required with no default (there is one). Both would have been the first thing a new
|
|
6
|
+
user read.
|
|
7
|
+
|
|
8
|
+
These tests pin the README's factual claims to the code that implements them.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import pathlib
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
from typer.main import get_command
|
|
17
|
+
|
|
18
|
+
from fp_cli import config, errors
|
|
19
|
+
from fp_cli.app import app
|
|
20
|
+
|
|
21
|
+
README = pathlib.Path(__file__).resolve().parent.parent / "README.md"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _registered() -> set[str]:
|
|
25
|
+
return set(get_command(app).commands) # type: ignore[attr-defined]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _readme() -> str:
|
|
29
|
+
return README.read_text(encoding="utf-8")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_readme_exists_and_is_substantial():
|
|
33
|
+
"""Guards every assertion below from passing vacuously on a missing file."""
|
|
34
|
+
assert README.is_file(), f"{README} is missing — it ships in the wheel"
|
|
35
|
+
assert len(_readme()) > 2000
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_every_command_the_readme_documents_actually_exists():
|
|
39
|
+
text = _readme()
|
|
40
|
+
documented = set(re.findall(r"^fp ([a-z][a-z-]*)\b", text, re.M))
|
|
41
|
+
documented |= set(re.findall(r"`fp ([a-z][a-z-]*)[ `]", text))
|
|
42
|
+
# Global flags and shell noise are not commands.
|
|
43
|
+
documented = {d for d in documented if not d.startswith("-")}
|
|
44
|
+
unknown = documented - _registered()
|
|
45
|
+
assert not unknown, (
|
|
46
|
+
f"the README documents commands that do not exist: {sorted(unknown)} "
|
|
47
|
+
f"(registered: {sorted(_registered())})"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_every_subcommand_the_readme_documents_actually_exists():
|
|
52
|
+
"""The group-level check above anchors on `^fp <group>` and never looks at the
|
|
53
|
+
pipe-separated verb lists beside it. `fp audits ... |update|` sat in the README
|
|
54
|
+
documenting a verb that does not exist (it is `edit`) and passed CI.
|
|
55
|
+
"""
|
|
56
|
+
cmd = get_command(app)
|
|
57
|
+
text = _readme()
|
|
58
|
+
bad = []
|
|
59
|
+
for line in text.split("\n"):
|
|
60
|
+
m = re.match(r"^fp ([a-z][a-z-]*) ([a-z][a-z|-]*)", line)
|
|
61
|
+
if not m:
|
|
62
|
+
continue
|
|
63
|
+
group, verbs = m.group(1), m.group(2)
|
|
64
|
+
sub = cmd.commands.get(group) # type: ignore[attr-defined]
|
|
65
|
+
if sub is None or not hasattr(sub, "commands"):
|
|
66
|
+
continue # not a group; the group-level test covers it
|
|
67
|
+
real = set(sub.commands)
|
|
68
|
+
for verb in verbs.split("|"):
|
|
69
|
+
if verb and verb not in real:
|
|
70
|
+
bad.append(f"`fp {group} {verb}` (real verbs: {sorted(real)})")
|
|
71
|
+
assert not bad, "the README documents subcommands that do not exist:\n " + "\n ".join(bad)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_the_readme_install_instructions_name_the_distribution_not_the_command():
|
|
75
|
+
"""`pip install fp` installs somebody else's package. The dist is `fp-cloud-cli`."""
|
|
76
|
+
text = _readme()
|
|
77
|
+
bad = re.findall(r"(?:pipx|pip|uv tool) install fp(?![-\w])", text)
|
|
78
|
+
assert not bad, f"install instructions must say fp-cloud-cli, not fp: {bad}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_the_documented_default_base_url_is_the_real_one():
|
|
82
|
+
assert config.DEFAULT_BASE_URL in _readme(), (
|
|
83
|
+
f"README does not mention the real default base URL {config.DEFAULT_BASE_URL}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_the_readme_exit_code_table_matches_the_error_classes():
|
|
88
|
+
"""The exit codes are a public scripting contract, restated in four places."""
|
|
89
|
+
text = _readme()
|
|
90
|
+
table = dict(
|
|
91
|
+
(int(code), meaning.strip())
|
|
92
|
+
for code, meaning in re.findall(r"^\|\s*(\d)\s*\|\s*([^|]+)\|", text, re.M)
|
|
93
|
+
)
|
|
94
|
+
assert table, "no exit-code table found in the README"
|
|
95
|
+
actual = {
|
|
96
|
+
e.exit_code
|
|
97
|
+
for e in vars(errors).values()
|
|
98
|
+
if isinstance(e, type)
|
|
99
|
+
and issubclass(e, Exception)
|
|
100
|
+
and isinstance(getattr(e, "exit_code", None), int)
|
|
101
|
+
}
|
|
102
|
+
documented = set(table)
|
|
103
|
+
missing = actual - documented
|
|
104
|
+
assert not missing, f"exit codes raised by the CLI but undocumented: {sorted(missing)}"
|
|
105
|
+
assert 0 in documented, "the README must document exit code 0"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_the_readme_does_not_link_private_repo_paths():
|
|
109
|
+
"""This README is published to PyPI; enterprise-docs/ is a private-repo path."""
|
|
110
|
+
text = _readme()
|
|
111
|
+
for needle in ("enterprise-docs/", "agenteye-enterprise/", "github.com/agenteye"):
|
|
112
|
+
assert needle not in text, f"private path {needle!r} must not ship in a public README"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_the_readme_documents_only_env_vars_the_cli_reads():
|
|
116
|
+
"""A documented-but-unread env var is worse than an undocumented one: the user
|
|
117
|
+
sets it, nothing happens, and there is no error to search for."""
|
|
118
|
+
import fp_cli
|
|
119
|
+
|
|
120
|
+
pkg = pathlib.Path(fp_cli.__file__).resolve().parent
|
|
121
|
+
source = "\n".join(p.read_text(encoding="utf-8") for p in pkg.rglob("*.py"))
|
|
122
|
+
documented = set(re.findall(r"`(FP_[A-Z_]+)`", _readme()))
|
|
123
|
+
assert documented, "the README documents no FP_* env vars — did the table move?"
|
|
124
|
+
unread = {v for v in documented if f'"{v}"' not in source}
|
|
125
|
+
assert not unread, f"README documents env vars the CLI never reads: {sorted(unread)}"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Tests for the review-driven fixes: json-aware error contract, resolver, SQL detail,
|
|
2
|
+
NaN-safe JSON, markup-safe tables, settings int parsing, and the org-switch json path."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
from fp_cli import _click_compat as click # the Click Typer is running
|
|
10
|
+
import httpx
|
|
11
|
+
import pytest
|
|
12
|
+
import respx
|
|
13
|
+
|
|
14
|
+
from fp_cli import client, output
|
|
15
|
+
from fp_cli.app import app
|
|
16
|
+
from fp_cli.commands import _write
|
|
17
|
+
from fp_cli.errors import NotFoundError
|
|
18
|
+
|
|
19
|
+
BASE = "http://dash.test"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# --- json-aware error contract (envelope on stdout) ------------------------------
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@respx.mock
|
|
26
|
+
def test_json_error_envelope_not_found(logged_in, runner):
|
|
27
|
+
respx.get(f"{BASE}/api/users").mock(return_value=httpx.Response(200, json=[]))
|
|
28
|
+
result = runner.invoke(app, ["--json", "users", "show", "nobody@x.com"])
|
|
29
|
+
assert result.exit_code == 6
|
|
30
|
+
data = json.loads(result.stdout) # must be a clean JSON object on stdout
|
|
31
|
+
assert "nobody@x.com" in data["error"]
|
|
32
|
+
assert data["exit_code"] == 6
|
|
33
|
+
assert "hint" in data # the "run `fp users list`" hint rides along
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_json_error_envelope_usage(logged_in, runner):
|
|
37
|
+
# A client-side validation error is ALSO a JSON envelope on stdout under --json.
|
|
38
|
+
result = runner.invoke(app, ["--json", "sessions", "--since", "bogus"])
|
|
39
|
+
assert result.exit_code == 2
|
|
40
|
+
data = json.loads(result.stdout)
|
|
41
|
+
assert data["exit_code"] == 2
|
|
42
|
+
assert "since" in data["error"].lower()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@respx.mock
|
|
46
|
+
def test_json_error_envelope_carries_status_and_request_id(logged_in, runner):
|
|
47
|
+
respx.get(f"{BASE}/api/sessions").mock(
|
|
48
|
+
return_value=httpx.Response(500, json={"error": "boom"}, headers={"x-request-id": "req-123"})
|
|
49
|
+
)
|
|
50
|
+
result = runner.invoke(app, ["--json", "sessions"])
|
|
51
|
+
assert result.exit_code == 1
|
|
52
|
+
data = json.loads(result.stdout)
|
|
53
|
+
assert data["error"] == "boom" and data["exit_code"] == 1 and data["status"] == 500
|
|
54
|
+
assert data["request_id"] == "req-123" # kept for server-log correlation
|
|
55
|
+
assert "HTTP" not in data["error"] # clean message; status is a separate field
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_unknown_command_json_envelope_via_env(logged_in, runner):
|
|
59
|
+
# An error raised BEFORE the group callback runs (unknown command) still honors --json via the
|
|
60
|
+
# env fallback, so an agent that typos a command under --json gets a parseable error on stdout.
|
|
61
|
+
result = runner.invoke(app, ["frobnicate"], env={"FP_JSON": "1"})
|
|
62
|
+
assert result.exit_code == 2
|
|
63
|
+
data = json.loads(result.stdout)
|
|
64
|
+
assert "frobnicate" in data["error"] and data["exit_code"] == 2
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_flag_placement_hint(logged_in, runner):
|
|
68
|
+
# A global flag AFTER the command nudges toward the right placement.
|
|
69
|
+
result = runner.invoke(app, ["sessions", "--json"])
|
|
70
|
+
assert result.exit_code == 2
|
|
71
|
+
assert "before the command" in result.stderr
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# --- shared resolve_one ----------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_resolve_one_resolves_by_name_and_id():
|
|
78
|
+
items = [SimpleNamespace(name="a", id="1"), SimpleNamespace(name="b", id="2")]
|
|
79
|
+
assert _write.resolve_one(items, "a", kind="key", list_cmd="keys list").id == "1"
|
|
80
|
+
assert _write.resolve_one(items, "2", kind="key", list_cmd="keys list").name == "b"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_resolve_one_not_found_raises_exit_6():
|
|
84
|
+
with pytest.raises(NotFoundError) as exc:
|
|
85
|
+
_write.resolve_one([], "zzz", kind="key", list_cmd="keys list")
|
|
86
|
+
assert exc.value.exit_code == 6
|
|
87
|
+
assert exc.value.hint # carries a "run `fp keys list`" hint
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_resolve_one_ambiguous_raises_usage():
|
|
91
|
+
dup = [SimpleNamespace(name="a", id="1"), SimpleNamespace(name="a", id="2")]
|
|
92
|
+
with pytest.raises(click.UsageError):
|
|
93
|
+
_write.resolve_one(dup, "a", kind="key", list_cmd="keys list")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# --- client._extract_error folds in the server `detail` --------------------------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_extract_error_includes_detail():
|
|
100
|
+
resp = httpx.Response(400, json={"error": "query failed", "detail": "syntax error near 'FORM'"})
|
|
101
|
+
assert client._extract_error(resp) == "query failed: syntax error near 'FORM'"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_extract_error_without_detail():
|
|
105
|
+
resp = httpx.Response(404, json={"error": "Not found."})
|
|
106
|
+
assert client._extract_error(resp) == "Not found."
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# --- emit_json is always valid JSON (no NaN/Infinity tokens) ---------------------
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_emit_json_coerces_non_finite(capsys):
|
|
113
|
+
output.emit_json({"a": float("nan"), "b": float("inf"), "c": float("-inf"), "d": 1.5})
|
|
114
|
+
data = json.loads(capsys.readouterr().out) # would raise on a bare NaN token
|
|
115
|
+
assert data == {"a": None, "b": None, "c": None, "d": 1.5}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# --- print_table never raises MarkupError on bracketed cells ---------------------
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_print_table_escapes_markup(capsys):
|
|
122
|
+
output.configure(no_color=True)
|
|
123
|
+
output.print_table(["k", "v"], [["x", "[/]"], ["y", "[red]bad[/red]"]]) # must not raise
|
|
124
|
+
out = capsys.readouterr().out
|
|
125
|
+
assert "[/]" in out and "[red]bad[/red]" in out # rendered literally
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# --- settings set --value coercion never crashes on odd input --------------------
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@respx.mock
|
|
132
|
+
def test_settings_set_unicode_value_no_crash(logged_in, runner):
|
|
133
|
+
respx.get(f"{BASE}/api/settings").mock(
|
|
134
|
+
return_value=httpx.Response(200, json={"settings": [
|
|
135
|
+
{"key": "session_ttl_secs", "value": 100, "schema": {"kind": "positive_int"}}]})
|
|
136
|
+
)
|
|
137
|
+
respx.put(f"{BASE}/api/settings/session_ttl_secs").mock(
|
|
138
|
+
return_value=httpx.Response(422, json={"error": "must be an integer"})
|
|
139
|
+
)
|
|
140
|
+
result = runner.invoke(app, ["--json", "settings", "set", "session_ttl_secs", "--value", "²"])
|
|
141
|
+
assert "Traceback" not in result.output # the old isdigit()/int() path raised here
|
|
142
|
+
assert result.exit_code == 1
|
|
143
|
+
assert json.loads(result.stdout)["error"] == "must be an integer"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# --- orgs switch not-found now emits a JSON envelope (was silent under --json) ----
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@respx.mock
|
|
150
|
+
def test_orgs_switch_not_found_json_emits_error(logged_in, runner):
|
|
151
|
+
respx.get(f"{BASE}/api/auth/session").mock(return_value=httpx.Response(200, json={
|
|
152
|
+
"id": "u1", "email": "me@test", "is_instance_admin": False,
|
|
153
|
+
"memberships": [{"org_id": "o1", "org_slug": "acme", "org_name": "Acme",
|
|
154
|
+
"permissions": ["events:read"], "permission_set": "standard"}],
|
|
155
|
+
}))
|
|
156
|
+
result = runner.invoke(app, ["--json", "orgs", "switch", "acm"])
|
|
157
|
+
assert result.exit_code == 2
|
|
158
|
+
data = json.loads(result.stdout)
|
|
159
|
+
assert "no org named acm" in data["error"]
|
|
160
|
+
assert data["exit_code"] == 2
|
|
161
|
+
assert "acme" in data.get("hint", "")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Anti-drift guard: every command and flag must be tracked by telemetry.
|
|
2
|
+
|
|
3
|
+
Introspects the assembled Typer/Click app and asserts the derived telemetry catalog
|
|
4
|
+
covers every command, every leaf subcommand, and every option token. This fails the
|
|
5
|
+
moment a new command or flag is added without being trackable — so usage signal can
|
|
6
|
+
never silently go missing and a flag VALUE can never leak (a flag must be allowlisted
|
|
7
|
+
by name to be emitted at all).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
from typer.main import get_command
|
|
14
|
+
|
|
15
|
+
from fp_cli import _click_compat as click # the Click Typer is running
|
|
16
|
+
from fp_cli import analytics
|
|
17
|
+
from fp_cli import analytics_registry as reg
|
|
18
|
+
from fp_cli.app import app
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _leaf_commands():
|
|
22
|
+
"""Yield (path_tuple, click.Command) for every leaf command in the app."""
|
|
23
|
+
cli = get_command(app)
|
|
24
|
+
|
|
25
|
+
def walk(cmd, prefix):
|
|
26
|
+
subs = getattr(cmd, "commands", None)
|
|
27
|
+
if subs:
|
|
28
|
+
for name, sub in subs.items():
|
|
29
|
+
yield from walk(sub, prefix + (name,))
|
|
30
|
+
else:
|
|
31
|
+
yield prefix, cmd
|
|
32
|
+
|
|
33
|
+
yield from walk(cli, ())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _all_option_tokens():
|
|
37
|
+
cli = get_command(app)
|
|
38
|
+
tokens = set()
|
|
39
|
+
|
|
40
|
+
def walk(cmd):
|
|
41
|
+
for p in getattr(cmd, "params", []):
|
|
42
|
+
if click.is_option(p):
|
|
43
|
+
tokens.update(p.opts)
|
|
44
|
+
tokens.update(p.secondary_opts)
|
|
45
|
+
for sub in (getattr(cmd, "commands", None) or {}).values():
|
|
46
|
+
walk(sub)
|
|
47
|
+
|
|
48
|
+
walk(cli)
|
|
49
|
+
return tokens
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_every_command_is_known():
|
|
53
|
+
known, leaves, _flags, _vf = reg.build()
|
|
54
|
+
missing = []
|
|
55
|
+
for path, _cmd in _leaf_commands():
|
|
56
|
+
group = path[0]
|
|
57
|
+
if group not in known:
|
|
58
|
+
missing.append(group)
|
|
59
|
+
if len(path) >= 2 and path[1] not in leaves.get(group, frozenset()):
|
|
60
|
+
missing.append(" ".join(path))
|
|
61
|
+
assert not missing, f"commands not tracked by telemetry catalog: {sorted(set(missing))}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_every_option_is_tracked():
|
|
65
|
+
_known, _leaves, flags, _vf = reg.build()
|
|
66
|
+
untracked = sorted(t for t in _all_option_tokens() if t not in flags)
|
|
67
|
+
assert not untracked, f"option flags missing from telemetry catalog: {untracked}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_resolve_command_path_for_nested_groups():
|
|
71
|
+
# A nested invocation resolves to (group, leaf) using only static catalog names.
|
|
72
|
+
assert analytics._resolve_command_path(["--json", "orgs", "list"]) == ("orgs", "list")
|
|
73
|
+
assert analytics._resolve_command_path(["orgs", "switch", "acme"]) == ("orgs", "switch")
|
|
74
|
+
assert analytics._resolve_command_path(["--org", "acme", "agent", "show", "s1"]) == ("agent", "show")
|
|
75
|
+
assert analytics._resolve_command_path(["whoami"]) == ("whoami", None)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_sanitize_flags_never_emits_values_for_any_command():
|
|
79
|
+
# For every leaf command, feed its own option tokens with fake values and assert
|
|
80
|
+
# the sanitised output contains only known flag names (never the values).
|
|
81
|
+
_known, _leaves, flags, _vf = reg.build()
|
|
82
|
+
for path, cmd in _leaf_commands():
|
|
83
|
+
argv = list(path)
|
|
84
|
+
for p in getattr(cmd, "params", []):
|
|
85
|
+
if click.is_option(p) and p.opts:
|
|
86
|
+
argv += [p.opts[0], "SECRET-VALUE"]
|
|
87
|
+
emitted = analytics._sanitize_flags(argv)
|
|
88
|
+
assert "SECRET-VALUE" not in emitted
|
|
89
|
+
for f in emitted:
|
|
90
|
+
assert f in set(flags.values()), f"{f} not a canonical flag name"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import respx
|
|
7
|
+
|
|
8
|
+
from fp_cli.app import app
|
|
9
|
+
|
|
10
|
+
BASE = "http://dash.test"
|
|
11
|
+
|
|
12
|
+
USAGE = {
|
|
13
|
+
"org_id": "00000000-0000-0000-0000-000000000001",
|
|
14
|
+
"billing_anchor": "2026-07-15T00:00:00Z",
|
|
15
|
+
"window": {
|
|
16
|
+
"start": "2026-07-15T00:00:00Z",
|
|
17
|
+
"end": "2026-08-14T00:00:00Z",
|
|
18
|
+
"current": True,
|
|
19
|
+
},
|
|
20
|
+
"usage": {
|
|
21
|
+
"events_ingested": 12840,
|
|
22
|
+
"sessions": 146,
|
|
23
|
+
"agents": 12,
|
|
24
|
+
"environments": 3,
|
|
25
|
+
"evaluation_runs": 84,
|
|
26
|
+
"evaluation_finishes": 79,
|
|
27
|
+
"evaluations": 420,
|
|
28
|
+
"metrics": 1260,
|
|
29
|
+
"queries_created": 31,
|
|
30
|
+
"dashboards_created": 6,
|
|
31
|
+
"alerts_created": 11,
|
|
32
|
+
"issues_created": 34,
|
|
33
|
+
"audit_runs": 18,
|
|
34
|
+
"audit_finishes": 16,
|
|
35
|
+
"keys_created": 5,
|
|
36
|
+
"keys_active": 3,
|
|
37
|
+
"users_created": 5,
|
|
38
|
+
"users_active": 1,
|
|
39
|
+
},
|
|
40
|
+
"calculated_at": "2026-07-31T12:00:00Z",
|
|
41
|
+
"stale_after": "2026-07-31T12:01:00Z",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@respx.mock
|
|
46
|
+
def test_usage_json_returns_dashboard_contract_unchanged(logged_in, runner):
|
|
47
|
+
request = respx.get(f"{BASE}/api/usage").mock(
|
|
48
|
+
return_value=httpx.Response(200, json=USAGE)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
result = runner.invoke(app, ["--json", "usage"])
|
|
52
|
+
|
|
53
|
+
assert result.exit_code == 0, result.output
|
|
54
|
+
assert json.loads(result.stdout) == USAGE
|
|
55
|
+
assert request.called
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@respx.mock
|
|
59
|
+
def test_usage_human_output_is_grouped_and_readable(logged_in, runner):
|
|
60
|
+
respx.get(f"{BASE}/api/usage").mock(return_value=httpx.Response(200, json=USAGE))
|
|
61
|
+
|
|
62
|
+
result = runner.invoke(app, ["--no-color", "usage"])
|
|
63
|
+
|
|
64
|
+
assert result.exit_code == 0, result.output
|
|
65
|
+
assert "Jul 15, 2026" in result.stdout
|
|
66
|
+
assert "12,840" in result.stdout
|
|
67
|
+
assert "PIPELINE COMPLETION" in result.stdout
|
|
68
|
+
assert "WORKSPACE & ACCESS" in result.stdout
|
|
69
|
+
assert "94%" in result.stdout
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@respx.mock
|
|
73
|
+
def test_usage_names_required_permission(logged_in, runner):
|
|
74
|
+
respx.get(f"{BASE}/api/usage").mock(
|
|
75
|
+
return_value=httpx.Response(
|
|
76
|
+
403,
|
|
77
|
+
json={"error": "forbidden", "required_permission": "usage:read"},
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
result = runner.invoke(app, ["--json", "usage"])
|
|
82
|
+
|
|
83
|
+
assert result.exit_code == 5
|
|
84
|
+
assert json.loads(result.stdout)["error"] == "you don't have the usage:read permission"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@respx.mock
|
|
88
|
+
def test_usage_reports_missing_billing_date(logged_in, runner):
|
|
89
|
+
respx.get(f"{BASE}/api/usage").mock(
|
|
90
|
+
return_value=httpx.Response(404, json={"error": "billing date is not set"})
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
result = runner.invoke(app, ["--json", "usage"])
|
|
94
|
+
|
|
95
|
+
assert result.exit_code == 6
|
|
96
|
+
assert json.loads(result.stdout)["error"] == "billing date is not set"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@respx.mock
|
|
100
|
+
def test_usage_rejects_non_object_success_response(logged_in, runner):
|
|
101
|
+
respx.get(f"{BASE}/api/usage").mock(return_value=httpx.Response(200, json=[]))
|
|
102
|
+
|
|
103
|
+
result = runner.invoke(app, ["--json", "usage"])
|
|
104
|
+
|
|
105
|
+
assert result.exit_code == 1
|
|
106
|
+
assert json.loads(result.stdout)["error"] == "The dashboard returned an invalid usage response."
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_usage_is_one_command_with_help_but_no_subcommands(home, runner):
|
|
110
|
+
help_result = runner.invoke(app, ["usage", "--help"])
|
|
111
|
+
assert help_result.exit_code == 0
|
|
112
|
+
assert "current fixed 30-day metering window" in help_result.stdout
|
|
113
|
+
|
|
114
|
+
child_result = runner.invoke(app, ["usage", "history"])
|
|
115
|
+
assert child_result.exit_code == 2
|