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,644 @@
|
|
|
1
|
+
"""Alerting: alert definitions + incident triage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import respx
|
|
9
|
+
|
|
10
|
+
from fp_cli import output
|
|
11
|
+
from fp_cli.app import app
|
|
12
|
+
from fp_cli.commands import _write
|
|
13
|
+
|
|
14
|
+
BASE = "http://dash.test"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# --- alerts -----------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@respx.mock
|
|
21
|
+
def test_alerts_list_json(logged_in, runner):
|
|
22
|
+
respx.get(f"{BASE}/api/alerts").mock(
|
|
23
|
+
return_value=httpx.Response(200, json=[{"id": "a1", "name": "p95", "trigger_kind": "metric_threshold", "severity": "warning", "enabled": True, "open_incidents": 0}])
|
|
24
|
+
)
|
|
25
|
+
result = runner.invoke(app, ["--json", "alerts", "list"])
|
|
26
|
+
assert result.exit_code == 0, result.output
|
|
27
|
+
assert json.loads(result.stdout)["alerts"][0]["id"] == "a1"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@respx.mock
|
|
31
|
+
def test_alerts_list_human_boxed(logged_in, runner):
|
|
32
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[
|
|
33
|
+
{"id": "a1", "name": "live", "trigger_kind": "custom_sql", "severity": "critical", "enabled": True,
|
|
34
|
+
"open_incidents": 1, "created_at": "2026-06-28T00:00:00Z", "last_attempted_at": "2026-06-28T00:00:00Z"},
|
|
35
|
+
{"id": "a2", "name": "old", "trigger_kind": "metric_threshold", "severity": "warning", "enabled": False,
|
|
36
|
+
"open_incidents": 0, "created_at": "2026-06-20T00:00:00Z", "last_attempted_at": None},
|
|
37
|
+
]))
|
|
38
|
+
result = runner.invoke(app, ["alerts", "list"])
|
|
39
|
+
assert result.exit_code == 0, result.output
|
|
40
|
+
out = result.stdout + result.stderr
|
|
41
|
+
assert "alerts" in out and "newest first" in out
|
|
42
|
+
for c in ("created", "name", "by", "trigger", "severity", "last alert"): # no status column
|
|
43
|
+
assert c in out
|
|
44
|
+
assert "status" not in out # the status column was removed
|
|
45
|
+
assert "never" in out # null last-alert
|
|
46
|
+
assert "on" in out and "off" in out # the on/off split is in the footer
|
|
47
|
+
assert "critical" in out and "warning" in out # footer severities
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@respx.mock
|
|
51
|
+
def test_alerts_show_by_name_json(logged_in, runner):
|
|
52
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[_FULL_ALERT]))
|
|
53
|
+
result = runner.invoke(app, ["--json", "alerts", "show", "p95"]) # by NAME
|
|
54
|
+
assert result.exit_code == 0, result.output
|
|
55
|
+
assert json.loads(result.stdout)["trigger_spec"] == {"metric": "latency", "op": ">", "value": 100}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@respx.mock
|
|
59
|
+
def test_alerts_show_not_found_exits_6(logged_in, runner):
|
|
60
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[_FULL_ALERT]))
|
|
61
|
+
result = runner.invoke(app, ["alerts", "show", "ghost"])
|
|
62
|
+
assert result.exit_code == 6
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@respx.mock
|
|
66
|
+
def test_alerts_show_human_cards(logged_in, runner):
|
|
67
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[_FULL_ALERT]))
|
|
68
|
+
result = runner.invoke(app, ["alerts", "show", "p95"])
|
|
69
|
+
assert result.exit_code == 0, result.output
|
|
70
|
+
out = result.stdout
|
|
71
|
+
assert "p95" in out and "warning" in out and "enabled" in out
|
|
72
|
+
assert "trigger" in out and "fire when" in out and "latency" in out # parsed metric_threshold
|
|
73
|
+
assert "evaluation" in out and "checks every" in out
|
|
74
|
+
assert "channels" in out and "email" in out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@respx.mock
|
|
78
|
+
def test_alerts_create_from_file(logged_in, runner, tmp_path):
|
|
79
|
+
payload = {"name": "p95", "trigger_kind": "metric_threshold", "trigger_spec": {"metric": "latency", "op": ">", "value": 100}, "severity": "warning", "eval_interval_secs": 60}
|
|
80
|
+
f = tmp_path / "alert.json"
|
|
81
|
+
f.write_text(json.dumps(payload))
|
|
82
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[])) # no name collision
|
|
83
|
+
route = respx.post(f"{BASE}/api/alerts").mock(return_value=httpx.Response(201, json={"id": "a9", "created_at": "t"}))
|
|
84
|
+
result = runner.invoke(app, ["--json", "alerts", "create", "p95", "--file", str(f)]) # name is positional
|
|
85
|
+
assert result.exit_code == 0, result.output
|
|
86
|
+
body = json.loads(route.calls.last.request.content)
|
|
87
|
+
assert body["name"] == "p95"
|
|
88
|
+
assert body["trigger_kind"] == "metric_threshold"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@respx.mock
|
|
92
|
+
def test_alerts_create_name_collision_exits_2(logged_in, runner):
|
|
93
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[{"id": "a1", "name": "errs"}]))
|
|
94
|
+
result = runner.invoke(app, ["--json", "alerts", "create", "errs", "--trigger-kind",
|
|
95
|
+
"metric_threshold", "--trigger-spec", '{"metric":"x","op":">","value":1}'])
|
|
96
|
+
assert result.exit_code == 2
|
|
97
|
+
assert "already exists" in json.loads(result.stdout)["error"]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@respx.mock
|
|
101
|
+
def test_alerts_create_human_renders_card(logged_in, runner, tmp_path):
|
|
102
|
+
# Human mode: pre-check (no collision) → POST → re-read the canonical alert → render the card.
|
|
103
|
+
payload = {"name": "errs", "trigger_kind": "metric_threshold",
|
|
104
|
+
"trigger_spec": {"metric": "error_count", "op": ">", "value": 50, "window_secs": 900},
|
|
105
|
+
"severity": "warning", "eval_interval_secs": 300}
|
|
106
|
+
f = tmp_path / "a.json"
|
|
107
|
+
f.write_text(json.dumps(payload))
|
|
108
|
+
respx.get(f"{BASE}/api/alerts").mock(side_effect=[
|
|
109
|
+
httpx.Response(200, json=[]), # pre-check: no collision
|
|
110
|
+
httpx.Response(200, json=[{"id": "a9", "name": "errs", "enabled": True, "trigger_kind": "metric_threshold",
|
|
111
|
+
"trigger_spec": payload["trigger_spec"], "severity": "warning",
|
|
112
|
+
"eval_interval_secs": 300, "min_breaches": 1, "eval_window": 1, "channels": []}]),
|
|
113
|
+
])
|
|
114
|
+
respx.post(f"{BASE}/api/alerts").mock(return_value=httpx.Response(201, json={"id": "a9", "created_at": "t"}))
|
|
115
|
+
result = runner.invoke(app, ["alerts", "create", "errs", "--file", str(f)]) # name positional
|
|
116
|
+
assert result.exit_code == 0, result.output
|
|
117
|
+
out = result.stdout
|
|
118
|
+
assert "alert created" in out and "errs" in out
|
|
119
|
+
assert "fire when error_count > 50 over 15m" in out and "channels" in out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@respx.mock
|
|
123
|
+
def test_alerts_update_rename_collision_exits_2(logged_in, runner):
|
|
124
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[
|
|
125
|
+
_FULL_ALERT, {**_FULL_ALERT, "id": "a2", "name": "taken"}]))
|
|
126
|
+
result = runner.invoke(app, ["--json", "alerts", "update", "p95", "--name", "taken", "--yes"])
|
|
127
|
+
assert result.exit_code == 2
|
|
128
|
+
assert "already exists" in json.loads(result.stdout)["error"]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@respx.mock
|
|
132
|
+
def test_alerts_update_human_renders_card(logged_in, runner):
|
|
133
|
+
# First GET resolves the name (old state); the post-PUT re-fetch returns the new state.
|
|
134
|
+
respx.get(f"{BASE}/api/alerts").mock(side_effect=[
|
|
135
|
+
httpx.Response(200, json=[_FULL_ALERT]),
|
|
136
|
+
httpx.Response(200, json=[{**_FULL_ALERT, "severity": "critical"}]),
|
|
137
|
+
])
|
|
138
|
+
respx.put(f"{BASE}/api/alerts/a1").mock(return_value=httpx.Response(200, json={"id": "a1", "updated_at": "t"}))
|
|
139
|
+
result = runner.invoke(app, ["alerts", "update", "p95", "--severity", "critical", "--yes"])
|
|
140
|
+
assert result.exit_code == 0, result.output
|
|
141
|
+
out = result.stdout
|
|
142
|
+
assert "alert updated" in out and "p95" in out
|
|
143
|
+
assert "critical" in out and "trigger" in out
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_alerts_create_validation_local(logged_in, runner, tmp_path):
|
|
147
|
+
# eval_interval_secs below 30 fails locally before any HTTP call.
|
|
148
|
+
payload = {"name": "x", "trigger_kind": "metric_threshold", "trigger_spec": {}, "eval_interval_secs": 10}
|
|
149
|
+
f = tmp_path / "a.json"
|
|
150
|
+
f.write_text(json.dumps(payload))
|
|
151
|
+
result = runner.invoke(app, ["alerts", "create", "--file", str(f)])
|
|
152
|
+
assert result.exit_code == 2
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_alerts_create_bad_trigger_kind(logged_in, runner):
|
|
156
|
+
result = runner.invoke(app, ["alerts", "create", "x", "--trigger-kind", "bogus", "--trigger-spec", "{}"])
|
|
157
|
+
assert result.exit_code == 2
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
_FULL_ALERT = {
|
|
161
|
+
"id": "a1",
|
|
162
|
+
"name": "p95",
|
|
163
|
+
"description": "latency guard",
|
|
164
|
+
"enabled": True,
|
|
165
|
+
"trigger_kind": "metric_threshold",
|
|
166
|
+
"trigger_spec": {"metric": "latency", "op": ">", "value": 100},
|
|
167
|
+
"min_breaches": 1,
|
|
168
|
+
"eval_window": 1,
|
|
169
|
+
"eval_interval_secs": 300,
|
|
170
|
+
"severity": "warning",
|
|
171
|
+
"channels": [{"kind": "email", "recipients": ["x@y.z"]}],
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@respx.mock
|
|
176
|
+
def test_alerts_update_flag_only_by_name_resends_full_body(logged_in, runner):
|
|
177
|
+
# A flag-only edit resolves the NAME via the list, then PUTs the WHOLE alert back with just
|
|
178
|
+
# the changed field — the server's PUT is a full replace.
|
|
179
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[_FULL_ALERT]))
|
|
180
|
+
put = respx.put(f"{BASE}/api/alerts/a1").mock(return_value=httpx.Response(200, json={"id": "a1", "updated_at": "t"}))
|
|
181
|
+
result = runner.invoke(app, ["--json", "alerts", "update", "p95", "--description", "off-hours", "--yes"])
|
|
182
|
+
assert result.exit_code == 0, result.output
|
|
183
|
+
assert put.called
|
|
184
|
+
body = json.loads(put.calls.last.request.content)
|
|
185
|
+
assert body["description"] == "off-hours" # the changed field
|
|
186
|
+
assert body["name"] == "p95" # everything else carried over
|
|
187
|
+
assert body["trigger_kind"] == "metric_threshold"
|
|
188
|
+
assert body["trigger_spec"] == {"metric": "latency", "op": ">", "value": 100}
|
|
189
|
+
assert body["severity"] == "warning"
|
|
190
|
+
assert body["channels"] == [{"kind": "email", "recipients": ["x@y.z"]}]
|
|
191
|
+
assert body["enabled"] is True # preserved (no enable/disable flag any more)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@respx.mock
|
|
195
|
+
def test_alerts_update_flag_only_can_change_severity(logged_in, runner):
|
|
196
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[_FULL_ALERT]))
|
|
197
|
+
put = respx.put(f"{BASE}/api/alerts/a1").mock(return_value=httpx.Response(200, json={"id": "a1", "updated_at": "t"}))
|
|
198
|
+
result = runner.invoke(app, ["--json", "alerts", "update", "p95", "--severity", "critical", "--yes"])
|
|
199
|
+
assert result.exit_code == 0, result.output
|
|
200
|
+
body = json.loads(put.calls.last.request.content)
|
|
201
|
+
assert body["severity"] == "critical"
|
|
202
|
+
assert body["enabled"] is True # untouched, preserved from the existing alert
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@respx.mock
|
|
206
|
+
def test_alerts_update_missing_alert_is_not_found(logged_in, runner):
|
|
207
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[_FULL_ALERT]))
|
|
208
|
+
put = respx.put(f"{BASE}/api/alerts/a1").mock(return_value=httpx.Response(200, json={}))
|
|
209
|
+
result = runner.invoke(app, ["alerts", "update", "nope", "--severity", "critical", "--yes"])
|
|
210
|
+
assert result.exit_code == 6, result.output
|
|
211
|
+
assert not put.called
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@respx.mock
|
|
215
|
+
def test_alerts_update_with_file_is_full_replace(logged_in, runner, tmp_path):
|
|
216
|
+
# The --file path is a straight replace (the list read only resolves the name → id).
|
|
217
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[_FULL_ALERT]))
|
|
218
|
+
payload = {"name": "renamed", "trigger_kind": "metric_threshold", "trigger_spec": {"metric": "latency", "op": ">", "value": 1}, "severity": "info", "eval_interval_secs": 60}
|
|
219
|
+
f = tmp_path / "alert.json"
|
|
220
|
+
f.write_text(json.dumps(payload))
|
|
221
|
+
put = respx.put(f"{BASE}/api/alerts/a1").mock(return_value=httpx.Response(200, json={"id": "a1", "updated_at": "t"}))
|
|
222
|
+
result = runner.invoke(app, ["--json", "alerts", "update", "p95", "--file", str(f), "--yes"])
|
|
223
|
+
assert result.exit_code == 0, result.output
|
|
224
|
+
body = json.loads(put.calls.last.request.content)
|
|
225
|
+
assert body["name"] == "renamed"
|
|
226
|
+
assert body["severity"] == "info"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@respx.mock
|
|
230
|
+
def test_alerts_delete_by_name(logged_in, runner):
|
|
231
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[_FULL_ALERT]))
|
|
232
|
+
route = respx.delete(f"{BASE}/api/alerts/a1").mock(return_value=httpx.Response(204))
|
|
233
|
+
result = runner.invoke(app, ["--json", "alerts", "delete", "p95", "--yes"])
|
|
234
|
+
assert result.exit_code == 0, result.output
|
|
235
|
+
assert route.called
|
|
236
|
+
body = json.loads(result.stdout)
|
|
237
|
+
assert body["deleted"] is True and body["name"] == "p95"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@respx.mock
|
|
241
|
+
def test_alerts_delete_not_found_exits_6(logged_in, runner):
|
|
242
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[_FULL_ALERT]))
|
|
243
|
+
result = runner.invoke(app, ["alerts", "delete", "ghost", "--yes"])
|
|
244
|
+
assert result.exit_code == 6
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@respx.mock
|
|
248
|
+
def test_alerts_test_sends_by_name(logged_in, runner):
|
|
249
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[_FULL_ALERT]))
|
|
250
|
+
respx.post(f"{BASE}/api/alerts/a1/test").mock(
|
|
251
|
+
return_value=httpx.Response(200, json={"ok": True, "synthetic_incident_id": "i1"})
|
|
252
|
+
)
|
|
253
|
+
result = runner.invoke(app, ["--json", "alerts", "test", "p95", "--yes"])
|
|
254
|
+
assert result.exit_code == 0, result.output
|
|
255
|
+
assert json.loads(result.stdout)["ok"] is True
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@respx.mock
|
|
259
|
+
def test_alerts_test_human_renders_dispatch(logged_in, runner):
|
|
260
|
+
respx.get(f"{BASE}/api/alerts").mock(return_value=httpx.Response(200, json=[_FULL_ALERT]))
|
|
261
|
+
respx.post(f"{BASE}/api/alerts/a1/test").mock(
|
|
262
|
+
return_value=httpx.Response(200, json={"ok": True, "synthetic_incident_id": "i1"})
|
|
263
|
+
)
|
|
264
|
+
result = runner.invoke(app, ["alerts", "test", "p95", "--yes"])
|
|
265
|
+
assert result.exit_code == 0, result.output
|
|
266
|
+
out = result.stdout + result.stderr
|
|
267
|
+
assert "test notification sent for" in out and "p95" in out
|
|
268
|
+
assert "dispatched to" in out and "email" in out # _FULL_ALERT's email channel
|
|
269
|
+
assert "delivery isn't confirmed" in out # honest note (issue #183)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# --- incidents --------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@respx.mock
|
|
276
|
+
def test_incidents_list_by_state(logged_in, runner):
|
|
277
|
+
route = respx.get(f"{BASE}/api/issues").mock(
|
|
278
|
+
return_value=httpx.Response(200, json=[{"id": "i1", "alert_name": "p95", "alert_severity": "warning", "state": "firing", "opened_at": "t", "assignees": []}])
|
|
279
|
+
)
|
|
280
|
+
result = runner.invoke(app, ["--json", "issues", "list", "--state", "firing"])
|
|
281
|
+
assert result.exit_code == 0, result.output
|
|
282
|
+
assert json.loads(result.stdout)["issues"][0]["id"] == "i1"
|
|
283
|
+
assert route.calls.last.request.url.params["state"] == "firing"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@respx.mock
|
|
287
|
+
def test_incidents_list_by_alert_uses_alert_path(logged_in, runner):
|
|
288
|
+
route = respx.get(f"{BASE}/api/alerts/a1/issues").mock(return_value=httpx.Response(200, json=[]))
|
|
289
|
+
result = runner.invoke(app, ["--json", "issues", "list", "--alert-id", "a1"])
|
|
290
|
+
assert result.exit_code == 0, result.output
|
|
291
|
+
assert route.called
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@respx.mock
|
|
295
|
+
def test_incidents_count_json(logged_in, runner):
|
|
296
|
+
respx.get(f"{BASE}/api/issues/count").mock(return_value=httpx.Response(200, json={"count": 3}))
|
|
297
|
+
result = runner.invoke(app, ["--json", "issues", "count"])
|
|
298
|
+
assert result.exit_code == 0, result.output
|
|
299
|
+
assert json.loads(result.stdout)["count"] == 3
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@respx.mock
|
|
303
|
+
def test_incident_ack(logged_in, runner):
|
|
304
|
+
route = respx.post(f"{BASE}/api/issues/i1/ack").mock(return_value=httpx.Response(200, json={}))
|
|
305
|
+
result = runner.invoke(app, ["--json", "issues", "ack", "i1"])
|
|
306
|
+
assert result.exit_code == 0, result.output
|
|
307
|
+
assert route.called
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@respx.mock
|
|
311
|
+
def test_incident_assign_sends_array(logged_in, runner):
|
|
312
|
+
route = respx.post(f"{BASE}/api/issues/i1/assign").mock(return_value=httpx.Response(200, json={}))
|
|
313
|
+
result = runner.invoke(app, ["--json", "issues", "assign", "i1", "--assignee", "a@x.com", "--assignee", "b@x.com"])
|
|
314
|
+
assert result.exit_code == 0, result.output
|
|
315
|
+
assert json.loads(route.calls.last.request.content) == {"assignees": ["a@x.com", "b@x.com"]}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@respx.mock
|
|
319
|
+
def test_incident_comment_add_from_body(logged_in, runner):
|
|
320
|
+
respx.post(f"{BASE}/api/issues/i1/comments").mock(
|
|
321
|
+
return_value=httpx.Response(201, json={"id": "c1", "incident_id": "i1", "author_email": "me@test", "body": "hi", "created_at": "t"})
|
|
322
|
+
)
|
|
323
|
+
result = runner.invoke(app, ["--json", "issues", "comment-add", "i1", "--body", "hi"])
|
|
324
|
+
assert result.exit_code == 0, result.output
|
|
325
|
+
assert json.loads(result.stdout)["id"] == "c1"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@respx.mock
|
|
329
|
+
def test_incident_resolve_requires_yes(logged_in, runner):
|
|
330
|
+
route = respx.post(f"{BASE}/api/issues/i1/resolve").mock(return_value=httpx.Response(200, json={}))
|
|
331
|
+
result = runner.invoke(app, ["--json", "issues", "resolve", "i1", "--yes"])
|
|
332
|
+
assert result.exit_code == 0, result.output
|
|
333
|
+
assert route.called
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@respx.mock
|
|
337
|
+
def test_incident_open_standalone(logged_in, runner):
|
|
338
|
+
respx.post(f"{BASE}/api/issues").mock(
|
|
339
|
+
return_value=httpx.Response(201, json={"id": "i9", "newly_opened": True, "state": "firing"})
|
|
340
|
+
)
|
|
341
|
+
result = runner.invoke(app, ["--json", "issues", "open", "--summary", "manual",
|
|
342
|
+
"--title", "checkout 500s", "--severity", "critical"])
|
|
343
|
+
assert result.exit_code == 0, result.output
|
|
344
|
+
assert json.loads(result.stdout)["id"] == "i9"
|
|
345
|
+
# The title has to reach the wire — the server REQUIRES it on the orphan
|
|
346
|
+
# path, and omitting it is what made this command 422 unconditionally.
|
|
347
|
+
sent = json.loads(respx.calls.last.request.content)
|
|
348
|
+
assert sent["title"] == "checkout 500s" and sent["severity"] == "critical"
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@respx.mock
|
|
352
|
+
def test_incident_open_standalone_without_title_exits_2(logged_in, runner):
|
|
353
|
+
"""A standalone open with no --title is rejected client-side, before the
|
|
354
|
+
request: the server can only answer 422, so spending a round-trip on a
|
|
355
|
+
known-bad body just turns a usage error into a server error."""
|
|
356
|
+
route = respx.post(f"{BASE}/api/issues").mock(
|
|
357
|
+
return_value=httpx.Response(201, json={"id": "i9"}))
|
|
358
|
+
result = runner.invoke(app, ["issues", "open", "--summary", "manual"])
|
|
359
|
+
assert result.exit_code == 2, result.output
|
|
360
|
+
assert "--title is required" in result.output
|
|
361
|
+
assert not route.called
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@respx.mock
|
|
365
|
+
def test_incident_open_linked_uses_alert_path(logged_in, runner):
|
|
366
|
+
route = respx.post(f"{BASE}/api/alerts/a1/issues").mock(
|
|
367
|
+
return_value=httpx.Response(201, json={"id": "i9", "newly_opened": True, "state": "firing"})
|
|
368
|
+
)
|
|
369
|
+
result = runner.invoke(app, ["--json", "issues", "open", "--summary", "x", "--alert-id", "a1"])
|
|
370
|
+
assert result.exit_code == 0, result.output
|
|
371
|
+
assert route.called
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# --- incidents: redesigned human UI + edge cases ----------------------------
|
|
375
|
+
|
|
376
|
+
_FULL_INCIDENT = {
|
|
377
|
+
"id": "1f5803aaaaaabbbbcccc000000009826",
|
|
378
|
+
"alert_id": "a1",
|
|
379
|
+
"alert_name": "p95 latency",
|
|
380
|
+
"alert_severity": "critical",
|
|
381
|
+
"state": "acknowledged",
|
|
382
|
+
"opened_at": "2026-06-28T00:00:00Z",
|
|
383
|
+
"acknowledged_by": "ops@example.com",
|
|
384
|
+
"assignees": ["a@example.com"],
|
|
385
|
+
"breach_summary": "p95 = 1240ms > 1000ms",
|
|
386
|
+
"comments": [
|
|
387
|
+
{"id": "c1", "author_email": "ops@example.com", "body": "looking into it", "created_at": "2026-06-28T00:01:00Z"},
|
|
388
|
+
{"id": "c2", "author_email": "x@example.com", "body": None, "created_at": "2026-06-28T00:02:00Z", "deleted_at": "2026-06-28T00:03:00Z"},
|
|
389
|
+
],
|
|
390
|
+
"subscribers": [{"email": "ops@example.com", "source": "ack", "subscribed_at": "2026-06-28T00:01:00Z"}],
|
|
391
|
+
"activity": [{"kind": "opened", "actor": "system", "at": "2026-06-28T00:00:00Z"},
|
|
392
|
+
{"kind": "acknowledged", "actor": "ops@example.com", "at": "2026-06-28T00:01:00Z"}],
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@respx.mock
|
|
397
|
+
def test_incidents_list_human_boxed(logged_in, runner):
|
|
398
|
+
respx.get(f"{BASE}/api/issues").mock(return_value=httpx.Response(200, json=[
|
|
399
|
+
{"id": "1f5803aaaaaabbbbcccc000000009826", "alert_name": "p95", "alert_severity": "critical",
|
|
400
|
+
"state": "firing", "opened_at": "2026-06-28T00:00:00Z", "assignees": ["a@example.com"]},
|
|
401
|
+
{"id": "z", "alert_name": None, "alert_severity": "info", "state": "resolved",
|
|
402
|
+
"opened_at": "2026-06-20T00:00:00Z", "assignees": []},
|
|
403
|
+
]))
|
|
404
|
+
result = runner.invoke(app, ["issues", "list"])
|
|
405
|
+
assert result.exit_code == 0, result.output
|
|
406
|
+
out = result.stdout + result.stderr
|
|
407
|
+
assert "issues" in out
|
|
408
|
+
for c in ("alert", "severity", "state", "opened", "assignees"):
|
|
409
|
+
assert c in out
|
|
410
|
+
assert "1f58" in out # short id is the handle
|
|
411
|
+
assert "firing" in out and "resolved" in out # state words + footer distribution
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@respx.mock
|
|
415
|
+
def test_incidents_list_invalid_state_exits_2(logged_in, runner):
|
|
416
|
+
result = runner.invoke(app, ["issues", "list", "--state", "bogus"])
|
|
417
|
+
assert result.exit_code == 2
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@respx.mock
|
|
421
|
+
def test_incidents_list_limit_zero_exits_2(logged_in, runner):
|
|
422
|
+
result = runner.invoke(app, ["issues", "list", "--limit", "0"])
|
|
423
|
+
assert result.exit_code == 2
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@respx.mock
|
|
427
|
+
def test_incidents_count_human_card(logged_in, runner):
|
|
428
|
+
respx.get(f"{BASE}/api/issues/count").mock(return_value=httpx.Response(200, json={"count": 4}))
|
|
429
|
+
result = runner.invoke(app, ["issues", "count", "--state", "firing"])
|
|
430
|
+
assert result.exit_code == 0, result.output
|
|
431
|
+
out = result.stdout
|
|
432
|
+
assert "issues" in out and "4" in out and "firing issues" in out
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@respx.mock
|
|
436
|
+
def test_incidents_show_human_cards(logged_in, runner):
|
|
437
|
+
respx.get(f"{BASE}/api/issues/i1").mock(return_value=httpx.Response(200, json=_FULL_INCIDENT))
|
|
438
|
+
result = runner.invoke(app, ["issues", "show", "i1"])
|
|
439
|
+
assert result.exit_code == 0, result.output
|
|
440
|
+
out = result.stdout
|
|
441
|
+
assert "p95 latency" in out and "critical" in out and "acknowledged" in out
|
|
442
|
+
assert "breach" in out and "1240ms" in out
|
|
443
|
+
assert "comments" in out and "looking into it" in out and "(deleted)" in out
|
|
444
|
+
assert "subscribers" in out and "ops@example.com" in out
|
|
445
|
+
assert "activity" in out and "opened" in out and "system" in out
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@respx.mock
|
|
449
|
+
def test_incidents_show_not_found_exits_6(logged_in, runner):
|
|
450
|
+
respx.get(f"{BASE}/api/issues/ghost").mock(return_value=httpx.Response(404, json={"error": "Not found."}))
|
|
451
|
+
result = runner.invoke(app, ["issues", "show", "ghost"])
|
|
452
|
+
assert result.exit_code == 6, result.output
|
|
453
|
+
assert "no issue" in result.stderr
|
|
454
|
+
assert "HTTP" not in (result.stdout + result.stderr) # never leak raw HTTP status
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@respx.mock
|
|
458
|
+
def test_incidents_show_not_found_json(logged_in, runner):
|
|
459
|
+
respx.get(f"{BASE}/api/issues/ghost").mock(return_value=httpx.Response(404, json={"error": "Not found."}))
|
|
460
|
+
result = runner.invoke(app, ["--json", "issues", "show", "ghost"])
|
|
461
|
+
assert result.exit_code == 6
|
|
462
|
+
assert "no issue" in json.loads(result.stdout)["error"]
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@respx.mock
|
|
466
|
+
def test_incidents_ack_human(logged_in, runner):
|
|
467
|
+
respx.post(f"{BASE}/api/issues/i1/ack").mock(return_value=httpx.Response(200, json={}))
|
|
468
|
+
result = runner.invoke(app, ["issues", "ack", "i1"])
|
|
469
|
+
assert result.exit_code == 0, result.output
|
|
470
|
+
assert "acknowledged issue" in result.stderr
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@respx.mock
|
|
474
|
+
def test_incidents_assign_non_operator_clean_error(logged_in, runner):
|
|
475
|
+
respx.post(f"{BASE}/api/issues/i1/assign").mock(
|
|
476
|
+
return_value=httpx.Response(422, json={"error": "a@x.com is not an operator"})
|
|
477
|
+
)
|
|
478
|
+
result = runner.invoke(app, ["issues", "assign", "i1", "--assignee", "a@x.com"])
|
|
479
|
+
assert result.exit_code == 1, result.output # ApiError → exit 1
|
|
480
|
+
assert "not an operator" in result.stderr
|
|
481
|
+
assert "HTTP" not in (result.stdout + result.stderr)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@respx.mock
|
|
485
|
+
def test_incidents_assign_non_operator_json(logged_in, runner):
|
|
486
|
+
respx.post(f"{BASE}/api/issues/i1/assign").mock(
|
|
487
|
+
return_value=httpx.Response(422, json={"error": "a@x.com is not an operator"})
|
|
488
|
+
)
|
|
489
|
+
result = runner.invoke(app, ["--json", "issues", "assign", "i1", "--assignee", "a@x.com"])
|
|
490
|
+
assert result.exit_code == 1
|
|
491
|
+
body = json.loads(result.stdout)
|
|
492
|
+
assert "not an operator" in body["error"] and body["status"] == 422
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@respx.mock
|
|
496
|
+
def test_incidents_resolve_human_yes(logged_in, runner):
|
|
497
|
+
respx.post(f"{BASE}/api/issues/i1/resolve").mock(return_value=httpx.Response(200, json={}))
|
|
498
|
+
result = runner.invoke(app, ["issues", "resolve", "i1", "--yes"])
|
|
499
|
+
assert result.exit_code == 0, result.output
|
|
500
|
+
assert "resolved issue" in result.stderr
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@respx.mock
|
|
504
|
+
def test_incidents_resolve_declined_emits_the_json_cancel_envelope(logged_in, runner, monkeypatch):
|
|
505
|
+
# Both docstrings promise `{cancelled: true}` under --json on a declined prompt, and
|
|
506
|
+
# every other write command emits it. `should_prompt` is forced here because --json
|
|
507
|
+
# normally auto-proceeds, which is what let these two paths drift to a stderr line
|
|
508
|
+
# only: a caller reading stdout would have got an empty document at exit 0.
|
|
509
|
+
monkeypatch.setattr(_write, "should_prompt", lambda *a, **k: True)
|
|
510
|
+
monkeypatch.setattr(output, "confirm_incident_resolve", lambda *a, **k: False)
|
|
511
|
+
respx.get(f"{BASE}/api/issues/i1").mock( # the prompt names the alert, so it reads first
|
|
512
|
+
return_value=httpx.Response(200, json={"id": "i1", "alert_name": "p95", "status": "open"}))
|
|
513
|
+
resolve = respx.post(f"{BASE}/api/issues/i1/resolve").mock(return_value=httpx.Response(200, json={}))
|
|
514
|
+
result = runner.invoke(app, ["--json", "issues", "resolve", "i1"])
|
|
515
|
+
assert result.exit_code == 0, result.output
|
|
516
|
+
assert json.loads(result.stdout) == {"cancelled": True}
|
|
517
|
+
assert not resolve.called
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@respx.mock
|
|
521
|
+
def test_incidents_comment_delete_declined_emits_the_json_cancel_envelope(logged_in, runner, monkeypatch):
|
|
522
|
+
monkeypatch.setattr(_write, "should_prompt", lambda *a, **k: True)
|
|
523
|
+
monkeypatch.setattr(output, "confirm_incident_comment_delete", lambda *a, **k: False)
|
|
524
|
+
respx.get(f"{BASE}/api/issues/i1/comments").mock(return_value=httpx.Response(200, json=[
|
|
525
|
+
{"id": "c1", "incident_id": "i1", "author_email": "me@test", "body": "typo", "created_at": "t"}]))
|
|
526
|
+
delete = respx.delete(f"{BASE}/api/issues/i1/comments/c1").mock(return_value=httpx.Response(204))
|
|
527
|
+
result = runner.invoke(app, ["--json", "issues", "comment-delete", "i1", "c1"])
|
|
528
|
+
assert result.exit_code == 0, result.output
|
|
529
|
+
assert json.loads(result.stdout) == {"cancelled": True}
|
|
530
|
+
assert not delete.called
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@respx.mock
|
|
534
|
+
def test_incidents_comment_add_human_card(logged_in, runner):
|
|
535
|
+
respx.post(f"{BASE}/api/issues/i1/comments").mock(
|
|
536
|
+
return_value=httpx.Response(201, json={"id": "c1", "incident_id": "i1", "author_email": "me@test", "body": "on it", "created_at": "t"})
|
|
537
|
+
)
|
|
538
|
+
result = runner.invoke(app, ["issues", "comment-add", "i1", "--body", "on it"])
|
|
539
|
+
assert result.exit_code == 0, result.output
|
|
540
|
+
out = result.stdout
|
|
541
|
+
assert "comment added" in out and "on it" in out
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def test_incidents_comment_add_neither_exits_2(logged_in, runner):
|
|
545
|
+
result = runner.invoke(app, ["issues", "comment-add", "i1"])
|
|
546
|
+
assert result.exit_code == 2
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@respx.mock
|
|
550
|
+
def test_incidents_comment_delete_not_found_exits_6(logged_in, runner):
|
|
551
|
+
respx.get(f"{BASE}/api/issues/i1/comments").mock(return_value=httpx.Response(200, json=[]))
|
|
552
|
+
result = runner.invoke(app, ["issues", "comment-delete", "i1", "cX", "--yes"])
|
|
553
|
+
assert result.exit_code == 6, result.output
|
|
554
|
+
assert "no comment" in result.stderr
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@respx.mock
|
|
558
|
+
def test_incidents_comment_delete_human_yes(logged_in, runner):
|
|
559
|
+
respx.get(f"{BASE}/api/issues/i1/comments").mock(return_value=httpx.Response(200, json=[
|
|
560
|
+
{"id": "c1", "incident_id": "i1", "author_email": "me@test", "body": "typo", "created_at": "2026-06-28T00:00:00Z"}]))
|
|
561
|
+
route = respx.delete(f"{BASE}/api/issues/i1/comments/c1").mock(return_value=httpx.Response(204))
|
|
562
|
+
result = runner.invoke(app, ["issues", "comment-delete", "i1", "c1", "--yes"])
|
|
563
|
+
assert result.exit_code == 0, result.output
|
|
564
|
+
assert route.called
|
|
565
|
+
assert "deleted comment" in result.stderr
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@respx.mock
|
|
569
|
+
def test_incidents_comment_delete_json(logged_in, runner):
|
|
570
|
+
respx.get(f"{BASE}/api/issues/i1/comments").mock(return_value=httpx.Response(200, json=[
|
|
571
|
+
{"id": "c1", "incident_id": "i1", "author_email": "me@test", "body": "typo", "created_at": "t"}]))
|
|
572
|
+
respx.delete(f"{BASE}/api/issues/i1/comments/c1").mock(return_value=httpx.Response(204))
|
|
573
|
+
result = runner.invoke(app, ["--json", "issues", "comment-delete", "i1", "c1", "--yes"])
|
|
574
|
+
assert result.exit_code == 0, result.output
|
|
575
|
+
body = json.loads(result.stdout)
|
|
576
|
+
assert body["deleted"] is True and body["id"] == "c1"
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
@respx.mock
|
|
580
|
+
def test_incidents_comment_list_human(logged_in, runner):
|
|
581
|
+
respx.get(f"{BASE}/api/issues/i1/comments").mock(return_value=httpx.Response(200, json=[
|
|
582
|
+
{"id": "c1", "author_email": "ops@example.com", "body": "db pool exhausted", "created_at": "2026-06-28T00:00:00Z"},
|
|
583
|
+
{"id": "c2", "author_email": "x@example.com", "body": None, "created_at": "2026-06-28T00:01:00Z", "deleted_at": "t"},
|
|
584
|
+
]))
|
|
585
|
+
result = runner.invoke(app, ["issues", "comment-list", "i1"])
|
|
586
|
+
assert result.exit_code == 0, result.output
|
|
587
|
+
out = result.stdout
|
|
588
|
+
assert "comments" in out and "db pool exhausted" in out and "(deleted)" in out
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
@respx.mock
|
|
592
|
+
def test_incidents_subscribers_human(logged_in, runner):
|
|
593
|
+
respx.get(f"{BASE}/api/issues/i1/subscribers").mock(return_value=httpx.Response(200, json=[
|
|
594
|
+
{"email": "ops@example.com", "source": "creator", "subscribed_at": "2026-06-28T00:00:00Z"}]))
|
|
595
|
+
result = runner.invoke(app, ["issues", "subscribers", "i1"])
|
|
596
|
+
assert result.exit_code == 0, result.output
|
|
597
|
+
out = result.stdout
|
|
598
|
+
assert "subscribers" in out and "ops@example.com" in out and "creator" in out
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
@respx.mock
|
|
602
|
+
def test_incidents_subscribe_human(logged_in, runner):
|
|
603
|
+
respx.post(f"{BASE}/api/issues/i1/subscribe").mock(return_value=httpx.Response(200, json={}))
|
|
604
|
+
result = runner.invoke(app, ["issues", "subscribe", "i1"])
|
|
605
|
+
assert result.exit_code == 0, result.output
|
|
606
|
+
assert "subscribed" in result.stderr and "you" in result.stderr
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@respx.mock
|
|
610
|
+
def test_incidents_open_human_card(logged_in, runner):
|
|
611
|
+
respx.post(f"{BASE}/api/issues").mock(
|
|
612
|
+
return_value=httpx.Response(201, json={"id": "i9", "newly_opened": True, "state": "firing"}))
|
|
613
|
+
respx.get(f"{BASE}/api/issues/i9").mock(return_value=httpx.Response(200, json={
|
|
614
|
+
"id": "i9", "state": "firing", "alert_severity": "critical", "title": "checkout 500s",
|
|
615
|
+
"opened_at": "2026-06-28T00:00:00Z"}))
|
|
616
|
+
result = runner.invoke(app, ["issues", "open", "--summary", "manual page",
|
|
617
|
+
"--title", "checkout 500s", "--severity", "critical"])
|
|
618
|
+
assert result.exit_code == 0, result.output
|
|
619
|
+
out = result.stdout
|
|
620
|
+
assert "issue opened" in out and "manual page" in out and "critical" in out
|
|
621
|
+
assert "checkout 500s" in out # the title is the card's hero line
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def test_incidents_open_bad_severity_exits_2(logged_in, runner):
|
|
625
|
+
result = runner.invoke(app, ["issues", "open", "--summary", "x", "--severity", "bogus"])
|
|
626
|
+
assert result.exit_code == 2
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
@respx.mock
|
|
630
|
+
def test_issues_show_malformed_id_is_not_found(logged_in, runner):
|
|
631
|
+
"""The mirror of test_audits_finding_malformed_id_is_not_found, and it did not pass.
|
|
632
|
+
|
|
633
|
+
The server's path extractor answers a non-UUID id with a 400 and a PLAIN-TEXT body, which
|
|
634
|
+
the dashboard converts to the generic {"error": "upstream returned non-JSON response"}.
|
|
635
|
+
`_fail` tested `status >= 500`, so the remap never fired: this exited 1 carrying that
|
|
636
|
+
internal phrase, while the audits sibling — the same code with `>= 400` — exited 6 with a
|
|
637
|
+
usable message. Anything branching on exit 6 to mean not-found took the wrong arm silently.
|
|
638
|
+
"""
|
|
639
|
+
respx.get(f"{BASE}/api/issues/not-a-uuid").mock(
|
|
640
|
+
return_value=httpx.Response(400, json={"error": "upstream returned non-JSON response"})
|
|
641
|
+
)
|
|
642
|
+
result = runner.invoke(app, ["--json", "issues", "show", "not-a-uuid"])
|
|
643
|
+
assert result.exit_code == 6
|
|
644
|
+
assert "no issue not-a-uuid" in json.loads(result.stdout)["error"]
|