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,481 @@
|
|
|
1
|
+
"""The deploy planner, the race check, and source resolution.
|
|
2
|
+
|
|
3
|
+
These are tested hard because they are the two places this feature can destroy
|
|
4
|
+
something: `PUT /enforcement/deployments/{id}` is a FULL REPLACE with no
|
|
5
|
+
server-side lock, so a wrong resulting set is a permanent silent undeploy, and a
|
|
6
|
+
missed race is somebody else's change gone with a 200 on screen.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import io
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from fp_cli.enforcement import (
|
|
15
|
+
DeployPlan,
|
|
16
|
+
RefError,
|
|
17
|
+
check_race,
|
|
18
|
+
latest_versions,
|
|
19
|
+
parse_ref,
|
|
20
|
+
plan_deploy,
|
|
21
|
+
read_source,
|
|
22
|
+
resolve_ref,
|
|
23
|
+
)
|
|
24
|
+
from fp_cli.errors import ApiError
|
|
25
|
+
from fp_cli.models import PolicyRef, PolicyVersion
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def ref(pid, version=1, effect="enforce"):
|
|
29
|
+
return PolicyRef(id=pid, version=version, effect=effect)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def pv(pid, version=1, archived=False):
|
|
33
|
+
return PolicyVersion(
|
|
34
|
+
id=pid, version=version, description="", sha256="", source=None,
|
|
35
|
+
created_at="", created_by=None, disabled=False, archived=archived,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── parsing ──────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.parametrize(
|
|
43
|
+
"token,expected",
|
|
44
|
+
[
|
|
45
|
+
("a", ("a", None, None)),
|
|
46
|
+
("a@3", ("a", 3, None)),
|
|
47
|
+
("a:observe", ("a", None, "observe")),
|
|
48
|
+
("a@3:observe", ("a", 3, "observe")),
|
|
49
|
+
("a@3:enforce", ("a", 3, "enforce")),
|
|
50
|
+
("no-force-push.v2_x", ("no-force-push.v2_x", None, None)),
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
def test_parse_ref_shapes(token, expected):
|
|
54
|
+
assert parse_ref(token) == expected
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize("token", ["", " ", "a@", "a@x", "a:", "a:enforced", "a b", "a@1:bad"])
|
|
58
|
+
def test_parse_ref_rejects_junk(token):
|
|
59
|
+
with pytest.raises(RefError):
|
|
60
|
+
parse_ref(token)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_an_unknown_effect_names_the_valid_ones():
|
|
64
|
+
"""The message has to say what IS allowed — 'invalid effect' helps nobody."""
|
|
65
|
+
with pytest.raises(RefError, match="enforce, observe"):
|
|
66
|
+
parse_ref("a:audit")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── version and effect resolution ────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_add_of_an_already_deployed_policy_keeps_its_version():
|
|
73
|
+
"""`--add` must not silently upgrade.
|
|
74
|
+
|
|
75
|
+
A machine pinned to v1 while v3 exists is pinned deliberately. Treating a
|
|
76
|
+
bare `--add` as "give me the newest" would roll the fleet forward on a
|
|
77
|
+
command whose author was only reordering.
|
|
78
|
+
"""
|
|
79
|
+
got = resolve_ref("a", latest={"a": 3}, current={"a": ref("a", 1)})
|
|
80
|
+
assert (got.version, got.effect) == (1, "enforce")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_add_of_a_new_policy_takes_the_latest_version():
|
|
84
|
+
got = resolve_ref("a", latest={"a": 3}, current={})
|
|
85
|
+
assert got.version == 3
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_an_explicit_version_always_wins():
|
|
89
|
+
got = resolve_ref("a@2", latest={"a": 3}, current={"a": ref("a", 1)})
|
|
90
|
+
assert got.version == 2
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_effect_is_inherited_then_defaults_to_enforce():
|
|
94
|
+
assert resolve_ref("a", latest={"a": 1}, current={"a": ref("a", 1, "observe")}).effect == "observe"
|
|
95
|
+
assert resolve_ref("a", latest={"a": 1}, current={}).effect == "enforce"
|
|
96
|
+
assert resolve_ref("a:observe", latest={"a": 1}, current={}).effect == "observe"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_an_unpublished_policy_is_refused_before_any_write():
|
|
100
|
+
with pytest.raises(RefError, match="no published policy"):
|
|
101
|
+
resolve_ref("ghost", latest={"a": 1}, current={})
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_latest_versions_ignores_archived():
|
|
105
|
+
assert latest_versions([pv("a", 1), pv("a", 3, archived=True), pv("b", 2)]) == {"a": 1, "b": 2}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── the planner: the thing that decides what gets written ────────────────────
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_add_preserves_everything_already_deployed():
|
|
112
|
+
"""The whole reason --add exists. A full replace built from the delta alone
|
|
113
|
+
would drop `b` and `c` here, permanently, with a 200."""
|
|
114
|
+
plan = plan_deploy(
|
|
115
|
+
"m", current=[ref("b"), ref("c")], base=4, add=["a"], latest={"a": 1},
|
|
116
|
+
)
|
|
117
|
+
assert [p.id for p in plan.result] == ["a", "b", "c"]
|
|
118
|
+
assert [p.id for p in plan.added] == ["a"]
|
|
119
|
+
assert [p.id for p in plan.unchanged] == ["b", "c"]
|
|
120
|
+
assert plan.removed == []
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_remove_takes_exactly_one_out():
|
|
124
|
+
plan = plan_deploy("m", current=[ref("a"), ref("b")], base=1, remove=["a"])
|
|
125
|
+
assert [p.id for p in plan.result] == ["b"]
|
|
126
|
+
assert [p.id for p in plan.removed] == ["a"]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_removing_something_not_deployed_is_refused():
|
|
130
|
+
"""Silently succeeding would let a typo read as "already gone"."""
|
|
131
|
+
with pytest.raises(RefError, match="not deployed"):
|
|
132
|
+
plan_deploy("m", current=[ref("a")], base=1, remove=["b"])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_set_replaces_the_whole_set():
|
|
136
|
+
plan = plan_deploy(
|
|
137
|
+
"m", current=[ref("a"), ref("b")], base=2, replace=["c"], latest={"c": 5},
|
|
138
|
+
)
|
|
139
|
+
assert [p.id for p in plan.result] == ["c"]
|
|
140
|
+
assert [p.id for p in plan.removed] == ["a", "b"]
|
|
141
|
+
assert plan.result[0].version == 5
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_set_cannot_be_mixed_with_add_or_remove():
|
|
145
|
+
""""exactly these" and "these as well" have no single reading."""
|
|
146
|
+
with pytest.raises(RefError, match="cannot be combined"):
|
|
147
|
+
plan_deploy("m", current=[], base=None, replace=["a"], add=["b"], latest={"a": 1, "b": 1})
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_a_version_or_effect_change_is_reported_as_changed_not_add_remove():
|
|
151
|
+
plan = plan_deploy(
|
|
152
|
+
"m", current=[ref("a", 1, "enforce")], base=3, add=["a@2:observe"], latest={"a": 2},
|
|
153
|
+
)
|
|
154
|
+
assert plan.added == [] and plan.removed == []
|
|
155
|
+
was, now = plan.changed[0]
|
|
156
|
+
assert (was.version, was.effect) == (1, "enforce")
|
|
157
|
+
assert (now.version, now.effect) == (2, "observe")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_a_noop_is_detectable_so_the_cli_can_skip_the_write():
|
|
161
|
+
plan = plan_deploy("m", current=[ref("a")], base=1, add=["a"], latest={"a": 1})
|
|
162
|
+
assert plan.is_noop is True
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_deploying_to_a_machine_with_nothing_yet():
|
|
166
|
+
plan = plan_deploy("m", current=None, base=None, add=["a"], latest={"a": 2})
|
|
167
|
+
assert [p.label for p in plan.result] == ["a@2:enforce"]
|
|
168
|
+
assert plan.base is None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_the_result_is_sorted_so_two_equal_sets_serialise_identically():
|
|
172
|
+
plan = plan_deploy("m", current=[], base=None, add=["c", "a", "b"],
|
|
173
|
+
latest={"a": 1, "b": 1, "c": 1})
|
|
174
|
+
assert [p.id for p in plan.result] == ["a", "b", "c"]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_plan_json_carries_the_diff_a_harness_would_otherwise_recompute():
|
|
178
|
+
plan = plan_deploy("m", current=[ref("b")], base=1, add=["a"], latest={"a": 1})
|
|
179
|
+
d = plan.to_dict()
|
|
180
|
+
assert d["machineId"] == "m" and d["base"] == 1 and d["noop"] is False
|
|
181
|
+
assert [p["id"] for p in d["result"]] == ["a", "b"]
|
|
182
|
+
assert [p["id"] for p in d["added"]] == ["a"]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ── the race check ───────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_a_clean_write_is_base_plus_one():
|
|
189
|
+
check_race(4, 5) # no raise
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_a_skipped_generation_means_someone_else_wrote():
|
|
193
|
+
with pytest.raises(ApiError, match="someone else deployed"):
|
|
194
|
+
check_race(4, 7)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_a_repeated_generation_is_also_a_race():
|
|
198
|
+
with pytest.raises(ApiError):
|
|
199
|
+
check_race(4, 4)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_a_first_deployment_has_no_base_to_check():
|
|
203
|
+
check_race(None, 1) # no raise
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_the_race_message_says_a_deploy_replaces():
|
|
207
|
+
"""The operator's next move depends on knowing it did not merge."""
|
|
208
|
+
with pytest.raises(ApiError, match="REPLACES"):
|
|
209
|
+
check_race(1, 9)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ── source input ─────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_source_from_a_path(tmp_path):
|
|
216
|
+
f = tmp_path / "p.mjs"
|
|
217
|
+
f.write_text("export default {}")
|
|
218
|
+
assert read_source(str(f)) == "export default {}"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_source_from_an_at_path(tmp_path):
|
|
222
|
+
f = tmp_path / "p.mjs"
|
|
223
|
+
f.write_text("x")
|
|
224
|
+
assert read_source(f"@{f}") == "x"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_source_from_explicit_stdin():
|
|
228
|
+
assert read_source("-", stdin=io.StringIO("piped"), isatty=False) == "piped"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_source_from_a_pipe_with_no_argument():
|
|
232
|
+
assert read_source(None, stdin=io.StringIO("piped"), isatty=False) == "piped"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_source_from_a_paste_prompts_first():
|
|
236
|
+
"""On a TTY, blocking on stdin without saying so is indistinguishable from a hang."""
|
|
237
|
+
called = []
|
|
238
|
+
out = read_source(None, stdin=io.StringIO("pasted"), isatty=True, prompt=lambda: called.append(1))
|
|
239
|
+
assert out == "pasted" and called == [1]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_a_missing_file_names_the_path():
|
|
243
|
+
with pytest.raises(RefError, match="no such file"):
|
|
244
|
+
read_source("/nope/definitely-not-here.mjs")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ── the JSON contract ────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_models_emit_the_server_shape_not_pythons():
|
|
251
|
+
"""`vars()` would leak snake_case into a contract that is camelCase
|
|
252
|
+
everywhere else — a difference a harness finds at runtime, not in review."""
|
|
253
|
+
from fp_cli.models import Deployment, Machine, PolicyVersion
|
|
254
|
+
|
|
255
|
+
pv_keys = set(PolicyVersion.from_dict({"id": "a", "version": 1}).to_dict())
|
|
256
|
+
assert "createdAt" in pv_keys and "created_at" not in pv_keys
|
|
257
|
+
|
|
258
|
+
dep_keys = set(Deployment.from_dict({"machineId": "m", "deployment": 1}).to_dict())
|
|
259
|
+
assert "machineId" in dep_keys and "machine_id" not in dep_keys
|
|
260
|
+
|
|
261
|
+
m = Machine.from_dict({"machineId": "m", "deployment": 3, "appliedDeployment": 1})
|
|
262
|
+
keys = set(m.to_dict())
|
|
263
|
+
assert "appliedDeployment" in keys
|
|
264
|
+
assert not [k for k in keys if "_" in k], keys
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_drift_is_intent_ahead_of_delivery():
|
|
268
|
+
"""The one field the CLI computes, and the reason `fleet diff` exists: a
|
|
269
|
+
machine can be deployed-to and still enforcing an older set."""
|
|
270
|
+
from fp_cli.models import Machine
|
|
271
|
+
|
|
272
|
+
def m(intended, delivered):
|
|
273
|
+
return Machine.from_dict({"machineId": "m", "deployment": intended,
|
|
274
|
+
"appliedDeployment": delivered})
|
|
275
|
+
|
|
276
|
+
assert m(3, 1).drifted is True # behind
|
|
277
|
+
assert m(3, None).drifted is True # never collected anything
|
|
278
|
+
assert m(3, 3).drifted is False # in sync
|
|
279
|
+
assert m(None, None).drifted is False # nothing deployed: not drift
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def test_a_nul_byte_in_source_is_refused_with_a_readable_reason():
|
|
283
|
+
"""Reaching Postgres with a NUL returns a bare "database error" — an internal
|
|
284
|
+
failure shown to somebody who most likely pointed the command at a binary
|
|
285
|
+
file. Catching it here turns that into a sentence."""
|
|
286
|
+
with pytest.raises(RefError, match="NUL byte"):
|
|
287
|
+
read_source("-", stdin=io.StringIO("export default {}\x00\x01"), isatty=False)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_the_nul_check_covers_every_input_shape(tmp_path):
|
|
291
|
+
"""A guard on one of five paths is not a guard."""
|
|
292
|
+
f = tmp_path / "bin.mjs"
|
|
293
|
+
f.write_text("ok\x00bad")
|
|
294
|
+
with pytest.raises(RefError, match="NUL byte"):
|
|
295
|
+
read_source(str(f))
|
|
296
|
+
with pytest.raises(RefError, match="NUL byte"):
|
|
297
|
+
read_source(f"@{f}")
|
|
298
|
+
with pytest.raises(RefError, match="NUL byte"):
|
|
299
|
+
read_source(None, stdin=io.StringIO("a\x00b"), isatty=False)
|
|
300
|
+
with pytest.raises(RefError, match="NUL byte"):
|
|
301
|
+
read_source(None, stdin=io.StringIO("a\x00b"), isatty=True, prompt=lambda: None)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_ordinary_unicode_is_not_mistaken_for_binary():
|
|
305
|
+
"""Emoji and CJK are legitimate policy content; only NUL is refused."""
|
|
306
|
+
assert read_source("-", stdin=io.StringIO("// 日本語 🎌\n"), isatty=False) == "// 日本語 🎌\n"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ── disabled policies ────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def test_adding_a_disabled_policy_is_refused_before_the_plan_is_built():
|
|
313
|
+
"""The server rejects it anyway — but only after the CLI has drawn a plan
|
|
314
|
+
and asked the operator to confirm it, so the last thing on screen is a
|
|
315
|
+
change that cannot happen under a prompt that implied it could."""
|
|
316
|
+
with pytest.raises(RefError, match="disabled"):
|
|
317
|
+
plan_deploy("m", current=[], base=None, add=["a"],
|
|
318
|
+
latest={"a": 1}, disabled={"a"})
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def test_the_refusal_names_the_command_that_fixes_it():
|
|
322
|
+
with pytest.raises(RefError, match="policies enable a"):
|
|
323
|
+
plan_deploy("m", current=[], base=None, add=["a"],
|
|
324
|
+
latest={"a": 1}, disabled={"a"})
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def test_set_checks_disabled_too():
|
|
328
|
+
"""`--set` resolves refs by the same path; a gap in one is a gap in both."""
|
|
329
|
+
with pytest.raises(RefError, match="disabled"):
|
|
330
|
+
plan_deploy("m", current=[], base=None, replace=["a"],
|
|
331
|
+
latest={"a": 1}, disabled={"a"})
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_a_disabled_policy_already_deployed_can_still_be_removed():
|
|
335
|
+
"""Defensive, for a state the server normally prevents.
|
|
336
|
+
|
|
337
|
+
Disabling REMOVES a policy from every deployment carrying it (verified
|
|
338
|
+
against a live server: generation 16 held it, disabling minted 17 without
|
|
339
|
+
it), so a disabled policy should not appear in `current` at all. If one ever
|
|
340
|
+
does — a stale read, a server that changes this — refusing the removal would
|
|
341
|
+
leave it stuck on the machine with no CLI path off."""
|
|
342
|
+
plan = plan_deploy("m", current=[ref("a")], base=1, remove=["a"], disabled={"a"})
|
|
343
|
+
assert [p.id for p in plan.removed] == ["a"]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_an_unrelated_add_does_not_re_resolve_what_is_already_there():
|
|
347
|
+
"""Only the refs you name are resolved. Re-resolving the whole set would
|
|
348
|
+
make an unrelated `--add` fail because of something already on the machine
|
|
349
|
+
— the same trap in reverse."""
|
|
350
|
+
plan = plan_deploy("m", current=[ref("a")], base=1, add=["b"],
|
|
351
|
+
latest={"a": 1, "b": 1}, disabled={"a"})
|
|
352
|
+
assert [p.id for p in plan.result] == ["a", "b"]
|
|
353
|
+
assert [p.id for p in plan.unchanged] == ["a"]
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def test_disabled_ids_ignores_archived():
|
|
357
|
+
"""An archived policy is already excluded from `latest`, so listing it here
|
|
358
|
+
too would produce 'disabled' for something that no longer exists."""
|
|
359
|
+
from fp_cli.enforcement import disabled_ids
|
|
360
|
+
pols = [pv("live"), pv("off"), pv("gone", archived=True)]
|
|
361
|
+
pols[1].disabled = True
|
|
362
|
+
pols[2].disabled = True
|
|
363
|
+
assert disabled_ids(pols) == {"off"}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ── machine labels ───────────────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def test_an_operator_rename_wins_over_the_machines_own_label():
|
|
370
|
+
"""The bug this fixes: `fleet rename` reported success and `fleet list` kept
|
|
371
|
+
showing `-`. The server stores the operator's name in `labelOverride`, a
|
|
372
|
+
DIFFERENT column from the machine's self-asserted `label`, and reading only
|
|
373
|
+
the latter made the rename invisible. Mirrors `machinePicker.ts`."""
|
|
374
|
+
from fp_cli.models import Machine
|
|
375
|
+
m = Machine.from_dict({"machineId": "m", "label": "self-named",
|
|
376
|
+
"labelOverride": "operator-named"})
|
|
377
|
+
assert m.display_label == "operator-named"
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def test_the_machines_own_label_is_used_when_there_is_no_override():
|
|
381
|
+
from fp_cli.models import Machine
|
|
382
|
+
assert Machine.from_dict({"machineId": "m", "label": "self-named"}).display_label == "self-named"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_no_label_at_all_is_none_not_an_empty_string():
|
|
386
|
+
"""The renderer substitutes a dash; an empty string would print as blank."""
|
|
387
|
+
from fp_cli.models import Machine
|
|
388
|
+
assert Machine.from_dict({"machineId": "m"}).display_label is None
|
|
389
|
+
assert Machine.from_dict({"machineId": "m", "label": " ",
|
|
390
|
+
"labelOverride": ""}).display_label is None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def test_both_label_fields_survive_into_json():
|
|
394
|
+
"""A harness may want to know which of the two it is looking at."""
|
|
395
|
+
from fp_cli.models import Machine
|
|
396
|
+
d = Machine.from_dict({"machineId": "m", "label": "a", "labelOverride": "b"}).to_dict()
|
|
397
|
+
assert d["label"] == "a" and d["labelOverride"] == "b"
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# ── review round: what these commands got wrong ──────────────────────────────
|
|
401
|
+
#
|
|
402
|
+
# Every test below stands for a bug that shipped in the first cut of these
|
|
403
|
+
# commands and passed every test that existed at the time. They are grouped
|
|
404
|
+
# because they share a shape: the command did something defensible and then
|
|
405
|
+
# described it wrongly, or classified its own failure wrongly.
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def test_a_binary_file_is_refused_by_name_not_by_traceback(tmp_path):
|
|
409
|
+
"""`policies publish x logo.png` printed a Python traceback.
|
|
410
|
+
|
|
411
|
+
`read_source` caught `OSError`, but decoding happens inside `read()` and
|
|
412
|
+
raises `UnicodeDecodeError`, which is a `ValueError` — so it sailed past the
|
|
413
|
+
handler and out through Click as a rich traceback with internal paths in it.
|
|
414
|
+
The NUL-byte guard could not save this: it inspects text, and a file that
|
|
415
|
+
fails to decode never becomes text.
|
|
416
|
+
"""
|
|
417
|
+
from fp_cli.enforcement import RefUsageError
|
|
418
|
+
|
|
419
|
+
f = tmp_path / "logo.png"
|
|
420
|
+
f.write_bytes(b"\x89PNG\r\n\x1a\n\xff\xfe\x00\x01binary")
|
|
421
|
+
for value in (str(f), f"@{f}"):
|
|
422
|
+
with pytest.raises(RefUsageError, match="not UTF-8 text"):
|
|
423
|
+
read_source(value)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def test_a_binary_pipe_is_refused_the_same_way():
|
|
427
|
+
"""`cat logo.png | fp policies publish x` reaches a different branch of
|
|
428
|
+
`read_source` than a path does, and used to traceback from that one too."""
|
|
429
|
+
from fp_cli.enforcement import RefUsageError
|
|
430
|
+
|
|
431
|
+
class _Undecodable:
|
|
432
|
+
def isatty(self):
|
|
433
|
+
return False
|
|
434
|
+
|
|
435
|
+
def read(self):
|
|
436
|
+
raise UnicodeDecodeError("utf-8", b"\xff\xfe", 0, 1, "invalid start byte")
|
|
437
|
+
|
|
438
|
+
with pytest.raises(RefUsageError, match="not UTF-8 text"):
|
|
439
|
+
read_source("-", stdin=_Undecodable())
|
|
440
|
+
with pytest.raises(RefUsageError, match="not UTF-8 text"):
|
|
441
|
+
read_source(None, stdin=_Undecodable(), isatty=False)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def test_mistyping_a_flag_is_a_usage_error_not_an_api_error():
|
|
445
|
+
"""These all exited 1 ("the server returned an error") for mistakes the
|
|
446
|
+
server never saw. The CLI documents 2 for usage, and `--since` / `--expect`
|
|
447
|
+
in these same commands already use it; a script branching on exit codes
|
|
448
|
+
could not tell a typo from a rejected write."""
|
|
449
|
+
from fp_cli.enforcement import RefUsageError
|
|
450
|
+
|
|
451
|
+
for token in ("", "bad ref!!", "policy:banana"):
|
|
452
|
+
with pytest.raises(RefUsageError):
|
|
453
|
+
parse_ref(token)
|
|
454
|
+
|
|
455
|
+
with pytest.raises(RefUsageError, match="cannot be combined"):
|
|
456
|
+
plan_deploy("m", current=[], base=1, add=("a",), replace=("b",))
|
|
457
|
+
|
|
458
|
+
with pytest.raises(RefUsageError, match="no such file"):
|
|
459
|
+
read_source("/nope/definitely-not-here.mjs")
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def test_usage_errors_are_still_ref_errors():
|
|
463
|
+
"""The split must not break `except RefError`, which is what every call site
|
|
464
|
+
and every earlier test in this file catches."""
|
|
465
|
+
from fp_cli.enforcement import RefUsageError
|
|
466
|
+
|
|
467
|
+
assert issubclass(RefUsageError, RefError)
|
|
468
|
+
with pytest.raises(RefError):
|
|
469
|
+
parse_ref("bad ref!!")
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def test_a_server_refusal_stays_an_api_error():
|
|
473
|
+
"""The other half of the contract: naming a policy that does not exist is
|
|
474
|
+
not a typo the caller can fix by re-reading their own command line, and it
|
|
475
|
+
keeps exit 1. Widening the usage class to cover it would have made every
|
|
476
|
+
"this does not exist" indistinguishable from a malformed flag."""
|
|
477
|
+
from fp_cli.enforcement import RefUsageError
|
|
478
|
+
|
|
479
|
+
with pytest.raises(RefError) as caught:
|
|
480
|
+
resolve_ref("ghost", latest={}, current={})
|
|
481
|
+
assert not isinstance(caught.value, RefUsageError)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Read/query completion: errors, eval-aggregate, events filters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import respx
|
|
9
|
+
|
|
10
|
+
from fp_cli.app import app
|
|
11
|
+
|
|
12
|
+
BASE = "http://dash.test"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@respx.mock
|
|
16
|
+
def test_errors_aggregate_json(logged_in, runner):
|
|
17
|
+
respx.get(f"{BASE}/api/events/error_summary").mock(
|
|
18
|
+
return_value=httpx.Response(
|
|
19
|
+
200,
|
|
20
|
+
json={"total": 5, "sessions": 2, "agents": 1, "last_ts": "2026-01-01T00:00:00Z", "bins": [1, 2]},
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
result = runner.invoke(app, ["--json", "errors", "--aggregate", "--since", "24h"])
|
|
24
|
+
assert result.exit_code == 0, result.output
|
|
25
|
+
assert json.loads(result.stdout)["total"] == 5
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@respx.mock
|
|
29
|
+
def test_errors_aggregate_card(logged_in, runner):
|
|
30
|
+
respx.get(f"{BASE}/api/events/error_summary").mock(
|
|
31
|
+
return_value=httpx.Response(
|
|
32
|
+
200, json={"total": 66, "sessions": 62, "agents": 6, "last_ts": "2026-01-01T00:00:00Z", "bins": []}
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
result = runner.invoke(app, ["errors", "--aggregate", "--since", "24h"])
|
|
36
|
+
assert result.exit_code == 0, result.output
|
|
37
|
+
assert "errors-aggregate" in result.stdout and "66" in result.stdout
|
|
38
|
+
assert "errored events" in result.stdout and "62 sessions" in result.stdout
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@respx.mock
|
|
42
|
+
def test_errors_list_fetches_errored_events(logged_in, runner):
|
|
43
|
+
# errors now reads the LIGHT, payload-free feed (/api/events/summary) — server-computed
|
|
44
|
+
# `summary`/`is_error`, no payload.
|
|
45
|
+
route = respx.get(f"{BASE}/api/events/summary").mock(
|
|
46
|
+
return_value=httpx.Response(
|
|
47
|
+
200,
|
|
48
|
+
json={
|
|
49
|
+
"events": [
|
|
50
|
+
{"id": 1, "session_id": "sess-1", "agent_id": "a", "event_type": "error", "ts": "t",
|
|
51
|
+
"environment": "prod", "summary": "TimeoutError: upstream timed out",
|
|
52
|
+
"is_error": True, "error_type": "TimeoutError", "output_tokens": None}
|
|
53
|
+
],
|
|
54
|
+
"next_cursor": None,
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
# bare `errors` now LISTS errored events (errored=true), not the summary
|
|
59
|
+
result = runner.invoke(app, ["--json", "errors", "--search", "boom", "--search", "oops", "--error-type", "TimeoutError"])
|
|
60
|
+
assert result.exit_code == 0, result.output
|
|
61
|
+
data = json.loads(result.stdout)
|
|
62
|
+
assert data["errors"][0]["event_type"] == "error" # keyed under "errors", light rows
|
|
63
|
+
assert data["errors"][0]["summary"] == "TimeoutError: upstream timed out" # server summary, no payload
|
|
64
|
+
assert "payload" not in data["errors"][0] or not data["errors"][0]["payload"] # never the fat column
|
|
65
|
+
params = route.calls.last.request.url.params
|
|
66
|
+
assert params["errored"] == "true"
|
|
67
|
+
assert params["error_type"] == "TimeoutError"
|
|
68
|
+
assert params.get_list("search") == ["boom", "oops"] # repeated free-text params
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@respx.mock
|
|
72
|
+
def test_errors_list_table(logged_in, runner):
|
|
73
|
+
respx.get(f"{BASE}/api/events/summary").mock(
|
|
74
|
+
return_value=httpx.Response(
|
|
75
|
+
200,
|
|
76
|
+
json={
|
|
77
|
+
"events": [
|
|
78
|
+
{"id": 1, "session_id": "sess-20260622-4b90b240", "agent_id": "agent-orderbot",
|
|
79
|
+
"event_type": "error", "ts": "2026-06-22T17:51:34Z",
|
|
80
|
+
"summary": "RateLimitError: upstream timed out", "is_error": True,
|
|
81
|
+
"error_type": "RateLimitError", "output_tokens": None, "environment": "prod"}
|
|
82
|
+
],
|
|
83
|
+
"next_cursor": None,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
result = runner.invoke(app, ["errors", "--since", "24h"])
|
|
88
|
+
assert result.exit_code == 0, result.output
|
|
89
|
+
# the 80-col CliRunner truncates cells; just assert the error-themed box rendered
|
|
90
|
+
assert "errors" in result.stdout and "newest first" in result.stdout
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@respx.mock
|
|
94
|
+
def test_evals_aggregate_json(logged_in, runner):
|
|
95
|
+
route = respx.get(f"{BASE}/api/evaluations/aggregate").mock(
|
|
96
|
+
return_value=httpx.Response(
|
|
97
|
+
200,
|
|
98
|
+
json={
|
|
99
|
+
"total": 10,
|
|
100
|
+
"status_counts": {"done": 8, "error": 1, "timeout": 1},
|
|
101
|
+
"score_stats": [{"key": "helpfulness", "count": 8, "avg": 0.7, "min": 0.1, "max": 1.0, "p50": 0.75}],
|
|
102
|
+
"timeline": {"bucket_unit": "hour", "from": None, "to": "t", "points": []},
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
result = runner.invoke(app, ["--json", "evals", "--aggregate", "--since", "7d", "--env", "prod"])
|
|
107
|
+
assert result.exit_code == 0, result.output
|
|
108
|
+
data = json.loads(result.stdout)
|
|
109
|
+
assert data["status_counts"]["done"] == 8
|
|
110
|
+
assert data["score_stats"][0]["key"] == "helpfulness"
|
|
111
|
+
assert route.calls.last.request.url.params["environment"] == "prod" # filters thread through
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@respx.mock
|
|
115
|
+
def test_evals_aggregate_table(logged_in, runner):
|
|
116
|
+
respx.get(f"{BASE}/api/evaluations/aggregate").mock(
|
|
117
|
+
return_value=httpx.Response(
|
|
118
|
+
200,
|
|
119
|
+
json={
|
|
120
|
+
"total": 10,
|
|
121
|
+
"status_counts": {"done": 8, "error": 2, "timeout": 0},
|
|
122
|
+
"score_stats": [{"key": "helpfulness", "count": 8, "avg": 0.66, "min": 0.1, "max": 1.0, "p50": 0.6}],
|
|
123
|
+
"timeline": {"bucket_unit": "hour", "from": None, "to": "t", "points": []},
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
result = runner.invoke(app, ["evals", "--aggregate", "--since", "7d"])
|
|
128
|
+
assert result.exit_code == 0, result.output
|
|
129
|
+
# totals card + score-stats panel
|
|
130
|
+
assert "eval-aggregate" in result.stdout and "evals" in result.stdout
|
|
131
|
+
assert "success rate" in result.stdout
|
|
132
|
+
assert "score stats" in result.stdout and "helpfulness" in result.stdout
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@respx.mock
|
|
136
|
+
def test_events_filters_threaded(logged_in, runner):
|
|
137
|
+
# A broad events read (no --session-id/--full) uses the light feed.
|
|
138
|
+
route = respx.get(f"{BASE}/api/events/summary").mock(
|
|
139
|
+
return_value=httpx.Response(200, json={"events": [], "next_cursor": None})
|
|
140
|
+
)
|
|
141
|
+
result = runner.invoke(
|
|
142
|
+
app,
|
|
143
|
+
["--json", "events", "--env", "prod,staging", "--event-type", "tool_use,tool_result", "--order", "asc"],
|
|
144
|
+
)
|
|
145
|
+
assert result.exit_code == 0, result.output
|
|
146
|
+
params = route.calls.last.request.url.params
|
|
147
|
+
assert params["environment"] == "prod,staging"
|
|
148
|
+
assert params["event_type"] == "tool_use,tool_result"
|
|
149
|
+
assert params["order"] == "asc"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_events_bad_order_usage_error(logged_in, runner):
|
|
153
|
+
result = runner.invoke(app, ["events", "--order", "sideways"])
|
|
154
|
+
assert result.exit_code == 2
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_events_nonpositive_limit_usage_error(logged_in, runner):
|
|
158
|
+
# --limit 0 / negative is rejected client-side (not passed through to the server).
|
|
159
|
+
assert runner.invoke(app, ["events", "--limit", "0"]).exit_code == 2
|
|
160
|
+
assert runner.invoke(app, ["events", "-n", "-5"]).exit_code == 2
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_sessions_nonpositive_limit_usage_error(logged_in, runner):
|
|
164
|
+
assert runner.invoke(app, ["sessions", "--limit", "0"]).exit_code == 2
|
|
165
|
+
assert runner.invoke(app, ["sessions", "-n", "-5"]).exit_code == 2
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_sessions_nonpositive_limit_usage_error(logged_in, runner):
|
|
169
|
+
assert runner.invoke(app, ["sessions", "--limit", "0"]).exit_code == 2
|
|
170
|
+
assert runner.invoke(app, ["sessions", "-n", "-5"]).exit_code == 2
|