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,950 @@
|
|
|
1
|
+
"""Audits: definitions CRUD + runs + findings triage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pytest
|
|
9
|
+
import respx
|
|
10
|
+
|
|
11
|
+
from fp_cli import analytics
|
|
12
|
+
from fp_cli.app import app
|
|
13
|
+
|
|
14
|
+
BASE = "http://dash.test"
|
|
15
|
+
|
|
16
|
+
_FULL_AUDIT = {
|
|
17
|
+
"id": "au1",
|
|
18
|
+
"name": "nightly-prod",
|
|
19
|
+
"description": "nightly failure sweep",
|
|
20
|
+
"enabled": True,
|
|
21
|
+
"schedule_interval_secs": 86400,
|
|
22
|
+
"window_mode": "since_last",
|
|
23
|
+
"lookback_window_secs": 604800,
|
|
24
|
+
"scope": {"environments": ["prod"]},
|
|
25
|
+
"ignore_error_types": ["TimeoutError"],
|
|
26
|
+
"llm_enabled": True,
|
|
27
|
+
"top_k": 50,
|
|
28
|
+
"sensitivity": "medium",
|
|
29
|
+
"channels": [{"kind": "slack"}],
|
|
30
|
+
"created_by": "ops@example.com",
|
|
31
|
+
"created_at": "2026-06-28T00:00:00Z",
|
|
32
|
+
"updated_at": "2026-06-28T00:00:00Z",
|
|
33
|
+
"open_findings": 3,
|
|
34
|
+
"last_run_status": "succeeded",
|
|
35
|
+
"last_run_finished_at": "2026-06-28T01:00:00Z",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_FULL_FINDING = {
|
|
39
|
+
"id": "1f5803aa-aaaa-bbbb-cccc-000000009826",
|
|
40
|
+
"audit_id": "au1",
|
|
41
|
+
"audit_name": "nightly-prod",
|
|
42
|
+
"fingerprint": "fp-1",
|
|
43
|
+
"title": "retry storm on checkout tool",
|
|
44
|
+
"category": "reliability",
|
|
45
|
+
"failure_type": "tool_error",
|
|
46
|
+
"description": "the checkout tool is retried until the budget is exhausted",
|
|
47
|
+
"root_cause_hypothesis": "upstream 502s are not treated as terminal",
|
|
48
|
+
"severity": "critical",
|
|
49
|
+
"magnitude": "big",
|
|
50
|
+
"priority": 0.92,
|
|
51
|
+
"status": "open",
|
|
52
|
+
"occurrences": 41,
|
|
53
|
+
"first_seen_at": "2026-06-20T00:00:00Z",
|
|
54
|
+
"last_seen_at": "2026-06-28T00:00:00Z",
|
|
55
|
+
"recommendation": "treat 502 as terminal and fail fast",
|
|
56
|
+
"expected_impact": "removes ~40 wasted calls a day",
|
|
57
|
+
"effort": "small",
|
|
58
|
+
"evidence": {"sessions": ["run-001"]},
|
|
59
|
+
"evidence_queries": ["SELECT 1"],
|
|
60
|
+
"scope": {"environments": ["prod"]},
|
|
61
|
+
"kind": "failure",
|
|
62
|
+
"assigned_to": None,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# --- audits: definitions ----------------------------------------------------
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@respx.mock
|
|
70
|
+
def test_audits_list_json(logged_in, runner):
|
|
71
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
72
|
+
result = runner.invoke(app, ["--json", "audits", "list"])
|
|
73
|
+
assert result.exit_code == 0, result.output
|
|
74
|
+
body = json.loads(result.stdout)["audits"][0]
|
|
75
|
+
assert body["id"] == "au1" and body["name"] == "nightly-prod"
|
|
76
|
+
assert body["scope"] == {"environments": ["prod"]} # opaque blob passes through untouched
|
|
77
|
+
assert body["open_findings"] == 3
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@respx.mock
|
|
81
|
+
def test_audits_list_human_boxed(logged_in, runner):
|
|
82
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[
|
|
83
|
+
_FULL_AUDIT,
|
|
84
|
+
{**_FULL_AUDIT, "id": "au2", "name": "paused", "enabled": False, "open_findings": 0,
|
|
85
|
+
"created_at": "2026-06-20T00:00:00Z", "last_run_status": None,
|
|
86
|
+
"last_run_finished_at": None},
|
|
87
|
+
]))
|
|
88
|
+
result = runner.invoke(app, ["audits", "list"])
|
|
89
|
+
assert result.exit_code == 0, result.output
|
|
90
|
+
out = result.stdout + result.stderr
|
|
91
|
+
assert "audits" in out and "newest first" in out
|
|
92
|
+
for c in ("created", "name", "every", "findings", "status", "last run"):
|
|
93
|
+
assert c in out
|
|
94
|
+
assert "nightly-prod" in out and "paused" in out
|
|
95
|
+
assert "1d" in out # humanized 86400s schedule
|
|
96
|
+
assert "never" in out # the paused audit has never run
|
|
97
|
+
assert "on" in out and "off" in out # the on/off split in the footer
|
|
98
|
+
assert "3 open findings" in out # footer findings roll-up
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@respx.mock
|
|
102
|
+
def test_audits_list_enabled_only(logged_in, runner):
|
|
103
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[
|
|
104
|
+
_FULL_AUDIT, {**_FULL_AUDIT, "id": "au2", "name": "paused", "enabled": False}]))
|
|
105
|
+
result = runner.invoke(app, ["--json", "audits", "list", "--enabled-only"])
|
|
106
|
+
assert result.exit_code == 0, result.output
|
|
107
|
+
audits = json.loads(result.stdout)["audits"]
|
|
108
|
+
assert [a["name"] for a in audits] == ["nightly-prod"]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@respx.mock
|
|
112
|
+
def test_audits_show_by_name_json(logged_in, runner):
|
|
113
|
+
# `show` resolves the NAME through the list endpoint (no GET by id needed).
|
|
114
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
115
|
+
result = runner.invoke(app, ["--json", "audits", "show", "nightly-prod"])
|
|
116
|
+
assert result.exit_code == 0, result.output
|
|
117
|
+
assert json.loads(result.stdout)["scope"] == {"environments": ["prod"]}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@respx.mock
|
|
121
|
+
def test_audits_show_human_cards(logged_in, runner):
|
|
122
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
123
|
+
result = runner.invoke(app, ["audits", "show", "nightly-prod"])
|
|
124
|
+
assert result.exit_code == 0, result.output
|
|
125
|
+
out = result.stdout
|
|
126
|
+
assert "nightly-prod" in out and "enabled" in out and "3 open findings" in out
|
|
127
|
+
assert "schedule" in out and "runs every" in out and "1d" in out
|
|
128
|
+
assert "scope" in out and "prod" in out and "TimeoutError" in out # covers + ignores
|
|
129
|
+
assert "analysis" in out and "sensitivity" in out and "medium" in out
|
|
130
|
+
assert "channels" in out and "slack" in out
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@respx.mock
|
|
134
|
+
def test_audits_show_not_found_exits_6(logged_in, runner):
|
|
135
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
136
|
+
result = runner.invoke(app, ["audits", "show", "ghost"])
|
|
137
|
+
assert result.exit_code == 6
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@respx.mock
|
|
141
|
+
def test_audits_create_sends_definition(logged_in, runner):
|
|
142
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[])) # no collision
|
|
143
|
+
route = respx.post(f"{BASE}/api/audits").mock(
|
|
144
|
+
return_value=httpx.Response(201, json={"id": "au9", "created_at": "t"}))
|
|
145
|
+
result = runner.invoke(app, [
|
|
146
|
+
"--json", "audits", "create", "nightly-prod",
|
|
147
|
+
"--scope", '{"environments":["prod"]}',
|
|
148
|
+
"--schedule-interval-secs", "86400",
|
|
149
|
+
"--sensitivity", "high",
|
|
150
|
+
"--ignore-error-type", "TimeoutError,ValueError",
|
|
151
|
+
])
|
|
152
|
+
assert result.exit_code == 0, result.output
|
|
153
|
+
body = json.loads(route.calls.last.request.content)
|
|
154
|
+
assert body["name"] == "nightly-prod"
|
|
155
|
+
assert body["scope"] == {"environments": ["prod"]}
|
|
156
|
+
assert body["sensitivity"] == "high"
|
|
157
|
+
assert body["ignore_error_types"] == ["TimeoutError", "ValueError"] # CSV → list
|
|
158
|
+
assert json.loads(result.stdout)["id"] == "au9"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@respx.mock
|
|
162
|
+
def test_audits_create_from_file(logged_in, runner, tmp_path):
|
|
163
|
+
payload = {"name": "weekly", "schedule_interval_secs": 604800, "sensitivity": "low"}
|
|
164
|
+
f = tmp_path / "audit.json"
|
|
165
|
+
f.write_text(json.dumps(payload))
|
|
166
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[]))
|
|
167
|
+
route = respx.post(f"{BASE}/api/audits").mock(
|
|
168
|
+
return_value=httpx.Response(201, json={"id": "au9", "created_at": "t"}))
|
|
169
|
+
result = runner.invoke(app, ["--json", "audits", "create", "weekly", "--file", str(f),
|
|
170
|
+
"--sensitivity", "high"]) # a flag overrides the file
|
|
171
|
+
assert result.exit_code == 0, result.output
|
|
172
|
+
body = json.loads(route.calls.last.request.content)
|
|
173
|
+
assert body["schedule_interval_secs"] == 604800 and body["sensitivity"] == "high"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@respx.mock
|
|
177
|
+
def test_audits_create_sends_context_in_the_same_request(logged_in, runner):
|
|
178
|
+
"""Context goes with the definition, not in a follow-up request.
|
|
179
|
+
|
|
180
|
+
Creating an enabled audit queues its first run due immediately, so a second
|
|
181
|
+
request can lose the race: the run an operator watches would argue without
|
|
182
|
+
the brief they just attached. No `PUT .../context` route is registered here
|
|
183
|
+
deliberately — if the command made that call, respx would fail the test."""
|
|
184
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[]))
|
|
185
|
+
route = respx.post(f"{BASE}/api/audits").mock(
|
|
186
|
+
return_value=httpx.Response(201, json={"id": "au9", "created_at": "t", "sources": 2}))
|
|
187
|
+
other = "https://docs.example.com/slo"
|
|
188
|
+
result = runner.invoke(app, [
|
|
189
|
+
"--json", "audits", "create", "nightly-prod",
|
|
190
|
+
"--text", "checkout agent for a retail store",
|
|
191
|
+
"--url", _DOC_URL, "--url", other,
|
|
192
|
+
])
|
|
193
|
+
assert result.exit_code == 0, result.output
|
|
194
|
+
body = json.loads(route.calls.last.request.content)
|
|
195
|
+
assert body["context"] == {
|
|
196
|
+
"text": "checkout agent for a retail store",
|
|
197
|
+
"urls": [_DOC_URL, other],
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@respx.mock
|
|
202
|
+
def test_audits_create_without_context_sends_no_context_key(logged_in, runner):
|
|
203
|
+
"""An omitted brief must not become an empty one — `context` absent, not `{}`."""
|
|
204
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[]))
|
|
205
|
+
route = respx.post(f"{BASE}/api/audits").mock(
|
|
206
|
+
return_value=httpx.Response(201, json={"id": "au9", "created_at": "t"}))
|
|
207
|
+
result = runner.invoke(app, ["--json", "audits", "create", "nightly-prod"])
|
|
208
|
+
assert result.exit_code == 0, result.output
|
|
209
|
+
assert "context" not in json.loads(route.calls.last.request.content)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@respx.mock
|
|
213
|
+
def test_audits_create_reads_the_brief_from_a_file(logged_in, runner, tmp_path):
|
|
214
|
+
f = tmp_path / "brief.md"
|
|
215
|
+
f.write_text("# Checkout agent\n\nRetries are expected on 429.")
|
|
216
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[]))
|
|
217
|
+
route = respx.post(f"{BASE}/api/audits").mock(
|
|
218
|
+
return_value=httpx.Response(201, json={"id": "au9", "created_at": "t", "sources": 0}))
|
|
219
|
+
result = runner.invoke(app, ["--json", "audits", "create", "nightly-prod",
|
|
220
|
+
"--text-file", str(f)])
|
|
221
|
+
assert result.exit_code == 0, result.output
|
|
222
|
+
body = json.loads(route.calls.last.request.content)
|
|
223
|
+
assert body["context"]["text"].startswith("# Checkout agent")
|
|
224
|
+
assert body["context"]["urls"] == []
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_audits_create_rejects_too_many_urls(logged_in, runner):
|
|
228
|
+
# The server's cap, applied before the request goes out.
|
|
229
|
+
result = runner.invoke(app, ["audits", "create", "x"] + sum(
|
|
230
|
+
(["--url", f"https://docs.example.com/{i}"] for i in range(6)), []))
|
|
231
|
+
assert result.exit_code == 2, result.output
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_audits_create_rejects_text_and_text_file_together(logged_in, runner):
|
|
235
|
+
result = runner.invoke(app, ["audits", "create", "x", "--text", "a", "--text-file", "b.md"])
|
|
236
|
+
assert result.exit_code == 2, result.output
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@respx.mock
|
|
240
|
+
def test_audits_create_human_renders_card(logged_in, runner):
|
|
241
|
+
# Human mode: pre-check (no collision) → POST → re-read the canonical audit → render the card.
|
|
242
|
+
respx.get(f"{BASE}/api/audits").mock(side_effect=[
|
|
243
|
+
httpx.Response(200, json=[]), # pre-check: no collision
|
|
244
|
+
httpx.Response(200, json=[_FULL_AUDIT]), # re-fetch after the write
|
|
245
|
+
])
|
|
246
|
+
respx.post(f"{BASE}/api/audits").mock(
|
|
247
|
+
return_value=httpx.Response(201, json={"id": "au1", "created_at": "t"}))
|
|
248
|
+
result = runner.invoke(app, ["audits", "create", "nightly-prod"])
|
|
249
|
+
assert result.exit_code == 0, result.output
|
|
250
|
+
out = result.stdout
|
|
251
|
+
assert "audit created" in out and "nightly-prod" in out
|
|
252
|
+
assert "schedule" in out and "channels" in out # the same cards `show` renders
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@respx.mock
|
|
256
|
+
def test_audits_create_renders_from_body_when_refetch_misses(logged_in, runner):
|
|
257
|
+
# If the post-write re-read doesn't find the audit, the card still renders — built from the
|
|
258
|
+
# request body — rather than crashing on the missing canonical row.
|
|
259
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[]))
|
|
260
|
+
respx.post(f"{BASE}/api/audits").mock(
|
|
261
|
+
return_value=httpx.Response(201, json={"id": "au9", "created_at": "t"}))
|
|
262
|
+
result = runner.invoke(app, ["audits", "create", "weekly", "--sensitivity", "high",
|
|
263
|
+
"--scope", '{"environments":["prod"]}'])
|
|
264
|
+
assert result.exit_code == 0, result.output
|
|
265
|
+
out = result.stdout
|
|
266
|
+
assert "audit created" in out and "weekly" in out
|
|
267
|
+
assert "high" in out and "prod" in out
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@respx.mock
|
|
271
|
+
def test_audits_create_name_collision_exits_2(logged_in, runner):
|
|
272
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
273
|
+
result = runner.invoke(app, ["--json", "audits", "create", "nightly-prod"])
|
|
274
|
+
assert result.exit_code == 2
|
|
275
|
+
assert "already exists" in json.loads(result.stdout)["error"]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_audits_create_bad_interval_exits_2(logged_in, runner):
|
|
279
|
+
# Below the server's 1h floor — rejected locally, before any HTTP call.
|
|
280
|
+
result = runner.invoke(app, ["audits", "create", "x", "--schedule-interval-secs", "60"])
|
|
281
|
+
assert result.exit_code == 2
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def test_audits_create_bad_window_mode_exits_2(logged_in, runner):
|
|
285
|
+
result = runner.invoke(app, ["audits", "create", "x", "--window-mode", "bogus"])
|
|
286
|
+
assert result.exit_code == 2
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_audits_create_bad_sensitivity_exits_2(logged_in, runner):
|
|
290
|
+
result = runner.invoke(app, ["audits", "create", "x", "--sensitivity", "bogus"])
|
|
291
|
+
assert result.exit_code == 2
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@respx.mock
|
|
295
|
+
def test_audits_edit_flag_only_resends_full_body(logged_in, runner):
|
|
296
|
+
# A flag-only edit resolves the NAME via the list, then PUTs the WHOLE definition back with
|
|
297
|
+
# just the changed field — the server replaces the definition on update.
|
|
298
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
299
|
+
put = respx.put(f"{BASE}/api/audits/au1").mock(
|
|
300
|
+
return_value=httpx.Response(200, json={"id": "au1", "updated": True}))
|
|
301
|
+
result = runner.invoke(app, ["--json", "audits", "edit", "nightly-prod",
|
|
302
|
+
"--sensitivity", "high", "--yes"])
|
|
303
|
+
assert result.exit_code == 0, result.output
|
|
304
|
+
body = json.loads(put.calls.last.request.content)
|
|
305
|
+
assert body["sensitivity"] == "high" # the changed field
|
|
306
|
+
assert body["name"] == "nightly-prod" # everything else carried over
|
|
307
|
+
assert body["scope"] == {"environments": ["prod"]}
|
|
308
|
+
assert body["schedule_interval_secs"] == 86400
|
|
309
|
+
assert body["channels"] == [{"kind": "slack"}]
|
|
310
|
+
assert body["enabled"] is True
|
|
311
|
+
assert "open_findings" not in body # server-derived state never sent
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@respx.mock
|
|
315
|
+
def test_audits_edit_disable(logged_in, runner):
|
|
316
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
317
|
+
put = respx.put(f"{BASE}/api/audits/au1").mock(
|
|
318
|
+
return_value=httpx.Response(200, json={"id": "au1", "updated": True}))
|
|
319
|
+
result = runner.invoke(app, ["--json", "audits", "edit", "nightly-prod", "--disabled", "--yes"])
|
|
320
|
+
assert result.exit_code == 0, result.output
|
|
321
|
+
assert json.loads(put.calls.last.request.content)["enabled"] is False
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@respx.mock
|
|
325
|
+
def test_audits_edit_human_renders_card(logged_in, runner):
|
|
326
|
+
# First GET resolves the name (old state); the post-PUT re-fetch returns the new state.
|
|
327
|
+
respx.get(f"{BASE}/api/audits").mock(side_effect=[
|
|
328
|
+
httpx.Response(200, json=[_FULL_AUDIT]),
|
|
329
|
+
httpx.Response(200, json=[{**_FULL_AUDIT, "sensitivity": "high"}]),
|
|
330
|
+
])
|
|
331
|
+
respx.put(f"{BASE}/api/audits/au1").mock(
|
|
332
|
+
return_value=httpx.Response(200, json={"id": "au1", "updated": True}))
|
|
333
|
+
result = runner.invoke(app, ["audits", "edit", "nightly-prod", "--sensitivity", "high", "--yes"])
|
|
334
|
+
assert result.exit_code == 0, result.output
|
|
335
|
+
out = result.stdout
|
|
336
|
+
assert "audit updated" in out and "nightly-prod" in out and "high" in out
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@respx.mock
|
|
340
|
+
def test_audits_edit_rename_collision_exits_2(logged_in, runner):
|
|
341
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[
|
|
342
|
+
_FULL_AUDIT, {**_FULL_AUDIT, "id": "au2", "name": "taken"}]))
|
|
343
|
+
result = runner.invoke(app, ["--json", "audits", "edit", "nightly-prod", "--name", "taken", "--yes"])
|
|
344
|
+
assert result.exit_code == 2
|
|
345
|
+
assert "already exists" in json.loads(result.stdout)["error"]
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@respx.mock
|
|
349
|
+
def test_audits_edit_missing_audit_exits_6(logged_in, runner):
|
|
350
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
351
|
+
put = respx.put(f"{BASE}/api/audits/au1").mock(return_value=httpx.Response(200, json={}))
|
|
352
|
+
result = runner.invoke(app, ["audits", "edit", "ghost", "--sensitivity", "high", "--yes"])
|
|
353
|
+
assert result.exit_code == 6, result.output
|
|
354
|
+
assert not put.called
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@respx.mock
|
|
358
|
+
def test_audits_delete_by_name(logged_in, runner):
|
|
359
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
360
|
+
route = respx.delete(f"{BASE}/api/audits/au1").mock(return_value=httpx.Response(204))
|
|
361
|
+
result = runner.invoke(app, ["--json", "audits", "delete", "nightly-prod", "--yes"])
|
|
362
|
+
assert result.exit_code == 0, result.output
|
|
363
|
+
assert route.called
|
|
364
|
+
body = json.loads(result.stdout)
|
|
365
|
+
assert body["deleted"] is True and body["name"] == "nightly-prod"
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@respx.mock
|
|
369
|
+
def test_audits_delete_not_found_exits_6(logged_in, runner):
|
|
370
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
371
|
+
result = runner.invoke(app, ["audits", "delete", "ghost", "--yes"])
|
|
372
|
+
assert result.exit_code == 6
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# --- audits: run + runs -----------------------------------------------------
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@respx.mock
|
|
379
|
+
def test_audits_run_queues_202(logged_in, runner):
|
|
380
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
381
|
+
route = respx.post(f"{BASE}/api/audits/au1/run").mock(
|
|
382
|
+
return_value=httpx.Response(202, json={"queued": True}))
|
|
383
|
+
result = runner.invoke(app, ["--json", "audits", "run", "nightly-prod"])
|
|
384
|
+
assert result.exit_code == 0, result.output
|
|
385
|
+
assert route.called
|
|
386
|
+
assert json.loads(result.stdout)["queued"] is True
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@respx.mock
|
|
390
|
+
def test_audits_run_human(logged_in, runner):
|
|
391
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
392
|
+
respx.post(f"{BASE}/api/audits/au1/run").mock(
|
|
393
|
+
return_value=httpx.Response(202, json={"queued": True}))
|
|
394
|
+
result = runner.invoke(app, ["audits", "run", "nightly-prod"])
|
|
395
|
+
assert result.exit_code == 0, result.output
|
|
396
|
+
out = result.stdout + result.stderr
|
|
397
|
+
assert "queued a run for" in out and "nightly-prod" in out
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@respx.mock
|
|
401
|
+
def test_audits_run_conflict_409(logged_in, runner):
|
|
402
|
+
# A run already in progress → the server's 409 message, never a double-queue.
|
|
403
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
404
|
+
respx.post(f"{BASE}/api/audits/au1/run").mock(return_value=httpx.Response(
|
|
405
|
+
409, json={"error": "a run is already in progress; it must finish before another can be queued"}))
|
|
406
|
+
result = runner.invoke(app, ["audits", "run", "nightly-prod"])
|
|
407
|
+
assert result.exit_code == 1, result.output # ApiError → exit 1
|
|
408
|
+
assert "already in progress" in result.stderr
|
|
409
|
+
assert "HTTP" not in (result.stdout + result.stderr) # never leak the raw status
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@respx.mock
|
|
413
|
+
def test_audits_run_conflict_409_json(logged_in, runner):
|
|
414
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
415
|
+
respx.post(f"{BASE}/api/audits/au1/run").mock(return_value=httpx.Response(
|
|
416
|
+
409, json={"error": "a run is already in progress; it must finish before another can be queued"}))
|
|
417
|
+
result = runner.invoke(app, ["--json", "audits", "run", "nightly-prod"])
|
|
418
|
+
assert result.exit_code == 1
|
|
419
|
+
body = json.loads(result.stdout)
|
|
420
|
+
assert "already in progress" in body["error"] and body["status"] == 409
|
|
421
|
+
assert "audits runs" in body["hint"]
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@respx.mock
|
|
425
|
+
def test_audits_runs_json(logged_in, runner):
|
|
426
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
427
|
+
respx.get(f"{BASE}/api/audits/au1/runs").mock(return_value=httpx.Response(200, json=[
|
|
428
|
+
{"id": "r1", "status": "succeeded", "trigger_kind": "schedule",
|
|
429
|
+
"window_from": "2026-06-27T00:00:00Z", "window_to": "2026-06-28T00:00:00Z",
|
|
430
|
+
"started_at": "2026-06-28T00:00:00Z", "finished_at": "2026-06-28T00:00:30Z",
|
|
431
|
+
"stats": {"events": 100}, "findings_count": 4, "new_findings_count": 1,
|
|
432
|
+
"report": "all good", "error": None},
|
|
433
|
+
]))
|
|
434
|
+
result = runner.invoke(app, ["--json", "audits", "runs", "nightly-prod"])
|
|
435
|
+
assert result.exit_code == 0, result.output
|
|
436
|
+
run = json.loads(result.stdout)["runs"][0]
|
|
437
|
+
assert run["id"] == "r1" and run["findings_count"] == 4 and run["stats"] == {"events": 100}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@respx.mock
|
|
441
|
+
def test_audits_runs_human_boxed(logged_in, runner):
|
|
442
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
443
|
+
respx.get(f"{BASE}/api/audits/au1/runs").mock(return_value=httpx.Response(200, json=[
|
|
444
|
+
{"id": "r1", "status": "succeeded", "trigger_kind": "schedule",
|
|
445
|
+
"started_at": "2026-06-28T00:00:00Z", "finished_at": "2026-06-28T00:00:30Z",
|
|
446
|
+
"findings_count": 4, "new_findings_count": 1},
|
|
447
|
+
{"id": "r2", "status": "failed", "trigger_kind": "manual",
|
|
448
|
+
"started_at": "2026-06-27T00:00:00Z", "finished_at": "2026-06-27T00:00:10Z",
|
|
449
|
+
"findings_count": 0, "new_findings_count": 0, "error": "clickhouse timeout"},
|
|
450
|
+
]))
|
|
451
|
+
result = runner.invoke(app, ["audits", "runs", "nightly-prod"])
|
|
452
|
+
assert result.exit_code == 0, result.output
|
|
453
|
+
out = result.stdout + result.stderr
|
|
454
|
+
assert "runs" in out and "nightly-prod" in out
|
|
455
|
+
for c in ("started", "status", "trigger", "findings", "new", "took"):
|
|
456
|
+
assert c in out
|
|
457
|
+
assert "succeeded" in out and "failed" in out and "30s" in out # computed wall time
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@respx.mock
|
|
461
|
+
def test_audits_runs_limit_zero_exits_2(logged_in, runner):
|
|
462
|
+
result = runner.invoke(app, ["audits", "runs", "nightly-prod", "--limit", "0"])
|
|
463
|
+
assert result.exit_code == 2
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# --- audits: findings -------------------------------------------------------
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@respx.mock
|
|
470
|
+
def test_audits_findings_json_with_filters(logged_in, runner):
|
|
471
|
+
route = respx.get(f"{BASE}/api/audits/findings").mock(
|
|
472
|
+
return_value=httpx.Response(200, json=[_FULL_FINDING]))
|
|
473
|
+
result = runner.invoke(app, ["--json", "audits", "findings", "--status", "open,recurring",
|
|
474
|
+
"--run-id", "r1", "--limit", "20", "--offset", "5"])
|
|
475
|
+
assert result.exit_code == 0, result.output
|
|
476
|
+
params = route.calls.last.request.url.params
|
|
477
|
+
assert params["status"] == "open,recurring"
|
|
478
|
+
assert params["run_id"] == "r1"
|
|
479
|
+
assert params["limit"] == "20" and params["offset"] == "5"
|
|
480
|
+
assert json.loads(result.stdout)["findings"][0]["id"] == _FULL_FINDING["id"]
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
@respx.mock
|
|
484
|
+
def test_audits_findings_by_audit_name_resolves_id(logged_in, runner):
|
|
485
|
+
# `--audit` takes the audit NAME; the CLI resolves it to the id the server filter wants.
|
|
486
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
487
|
+
route = respx.get(f"{BASE}/api/audits/findings").mock(return_value=httpx.Response(200, json=[]))
|
|
488
|
+
result = runner.invoke(app, ["--json", "audits", "findings", "--audit", "nightly-prod"])
|
|
489
|
+
assert result.exit_code == 0, result.output
|
|
490
|
+
assert route.calls.last.request.url.params["audit_id"] == "au1"
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@respx.mock
|
|
494
|
+
def test_audits_findings_unknown_audit_exits_6(logged_in, runner):
|
|
495
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
496
|
+
result = runner.invoke(app, ["audits", "findings", "--audit", "ghost"])
|
|
497
|
+
assert result.exit_code == 6
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@respx.mock
|
|
501
|
+
def test_audits_findings_human_boxed(logged_in, runner):
|
|
502
|
+
respx.get(f"{BASE}/api/audits/findings").mock(return_value=httpx.Response(200, json=[
|
|
503
|
+
_FULL_FINDING,
|
|
504
|
+
{**_FULL_FINDING, "id": "22220000-aaaa-bbbb-cccc-000000001111", "title": "slow tool",
|
|
505
|
+
"severity": "warning", "status": "muted", "kind": "improvement", "occurrences": 2},
|
|
506
|
+
]))
|
|
507
|
+
result = runner.invoke(app, ["audits", "findings"])
|
|
508
|
+
assert result.exit_code == 0, result.output
|
|
509
|
+
out = result.stdout + result.stderr
|
|
510
|
+
assert "findings" in out and "highest priority first" in out
|
|
511
|
+
for c in ("title", "severity", "status", "kind", "seen", "last"):
|
|
512
|
+
assert c in out
|
|
513
|
+
assert "1f58" in out # short id is the handle
|
|
514
|
+
# The title truncates to the leftover width (80-col test console) so the fixed columns
|
|
515
|
+
# always survive — the full text is in `audits finding` / --json.
|
|
516
|
+
assert "retry" in out and "critical" in out
|
|
517
|
+
assert "open" in out and "muted" in out # status words + footer distribution
|
|
518
|
+
assert "1 critical" in out # footer severity roll-up
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def test_audits_findings_bad_status_exits_2(logged_in, runner):
|
|
522
|
+
result = runner.invoke(app, ["audits", "findings", "--status", "bogus"])
|
|
523
|
+
assert result.exit_code == 2
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def test_audits_findings_limit_zero_exits_2(logged_in, runner):
|
|
527
|
+
result = runner.invoke(app, ["audits", "findings", "--limit", "0"])
|
|
528
|
+
assert result.exit_code == 2
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_audits_findings_negative_offset_exits_2(logged_in, runner):
|
|
532
|
+
result = runner.invoke(app, ["audits", "findings", "--offset", "-1"])
|
|
533
|
+
assert result.exit_code == 2
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
@respx.mock
|
|
537
|
+
def test_audits_finding_show_json(logged_in, runner):
|
|
538
|
+
fid = _FULL_FINDING["id"]
|
|
539
|
+
respx.get(f"{BASE}/api/audits/findings/{fid}").mock(
|
|
540
|
+
return_value=httpx.Response(200, json=_FULL_FINDING))
|
|
541
|
+
result = runner.invoke(app, ["--json", "audits", "finding", fid])
|
|
542
|
+
assert result.exit_code == 0, result.output
|
|
543
|
+
body = json.loads(result.stdout)
|
|
544
|
+
assert body["title"] == "retry storm on checkout tool"
|
|
545
|
+
assert body["evidence"] == {"sessions": ["run-001"]}
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@respx.mock
|
|
549
|
+
def test_audits_finding_show_human_cards(logged_in, runner):
|
|
550
|
+
fid = _FULL_FINDING["id"]
|
|
551
|
+
respx.get(f"{BASE}/api/audits/findings/{fid}").mock(
|
|
552
|
+
return_value=httpx.Response(200, json=_FULL_FINDING))
|
|
553
|
+
result = runner.invoke(app, ["audits", "finding", fid])
|
|
554
|
+
assert result.exit_code == 0, result.output
|
|
555
|
+
out = result.stdout
|
|
556
|
+
assert "retry storm on checkout tool" in out and "critical" in out and "failure" in out
|
|
557
|
+
assert "seen" in out and "41" in out
|
|
558
|
+
assert "analysis" in out and "upstream 502s" in out # root-cause hypothesis
|
|
559
|
+
assert "recommendation" in out and "fail fast" in out and "small" in out
|
|
560
|
+
assert "evidence" in out and "run-001" in out
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@respx.mock
|
|
564
|
+
def test_audits_finding_not_found_exits_6(logged_in, runner):
|
|
565
|
+
fid = "1f5803aa-aaaa-bbbb-cccc-000000000000"
|
|
566
|
+
respx.get(f"{BASE}/api/audits/findings/{fid}").mock(
|
|
567
|
+
return_value=httpx.Response(404, json={"error": "not found"}))
|
|
568
|
+
result = runner.invoke(app, ["audits", "finding", fid])
|
|
569
|
+
assert result.exit_code == 6, result.output
|
|
570
|
+
assert "no finding" in result.stderr
|
|
571
|
+
assert "HTTP" not in (result.stdout + result.stderr)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
@respx.mock
|
|
575
|
+
def test_audits_finding_malformed_id_is_not_found(logged_in, runner):
|
|
576
|
+
# The server's path extractor answers a non-UUID id with a 400 — surface the calm not-found.
|
|
577
|
+
respx.get(f"{BASE}/api/audits/findings/not-a-uuid").mock(
|
|
578
|
+
return_value=httpx.Response(400, json={"error": "invalid uuid"}))
|
|
579
|
+
result = runner.invoke(app, ["--json", "audits", "finding", "not-a-uuid"])
|
|
580
|
+
assert result.exit_code == 6
|
|
581
|
+
assert "no finding" in json.loads(result.stdout)["error"]
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@respx.mock
|
|
585
|
+
def test_audits_finding_forbidden_exits_5(logged_in, runner):
|
|
586
|
+
fid = _FULL_FINDING["id"]
|
|
587
|
+
respx.get(f"{BASE}/api/audits/findings/{fid}").mock(return_value=httpx.Response(
|
|
588
|
+
403, json={"error": "forbidden", "required_permission": "audits:read"}))
|
|
589
|
+
result = runner.invoke(app, ["audits", "finding", fid])
|
|
590
|
+
assert result.exit_code == 5, result.output
|
|
591
|
+
assert "audits:read" in result.stderr # the denial names the missing permission
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@respx.mock
|
|
595
|
+
def test_audits_list_forbidden_exits_5(logged_in, runner):
|
|
596
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(
|
|
597
|
+
403, json={"error": "forbidden", "required_permission": "audits:read"}))
|
|
598
|
+
result = runner.invoke(app, ["--json", "audits", "list"])
|
|
599
|
+
assert result.exit_code == 5
|
|
600
|
+
assert "audits:read" in json.loads(result.stdout)["error"]
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
# --- audits: finding triage -------------------------------------------------
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
@respx.mock
|
|
607
|
+
def test_finding_ack(logged_in, runner):
|
|
608
|
+
fid = _FULL_FINDING["id"]
|
|
609
|
+
route = respx.post(f"{BASE}/api/audits/findings/{fid}/status").mock(
|
|
610
|
+
return_value=httpx.Response(200, json={"id": fid, "action": "ack", "ok": True}))
|
|
611
|
+
result = runner.invoke(app, ["--json", "audits", "ack", fid, "--reason", "known"])
|
|
612
|
+
assert result.exit_code == 0, result.output
|
|
613
|
+
body = json.loads(route.calls.last.request.content)
|
|
614
|
+
assert body == {"action": "ack", "reason": "known"}
|
|
615
|
+
assert json.loads(result.stdout)["ok"] is True
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@respx.mock
|
|
619
|
+
def test_finding_mute(logged_in, runner):
|
|
620
|
+
fid = _FULL_FINDING["id"]
|
|
621
|
+
route = respx.post(f"{BASE}/api/audits/findings/{fid}/status").mock(
|
|
622
|
+
return_value=httpx.Response(200, json={"id": fid, "action": "mute", "ok": True}))
|
|
623
|
+
result = runner.invoke(app, ["--json", "audits", "mute", fid, "--reason", "expected", "--yes"])
|
|
624
|
+
assert result.exit_code == 0, result.output
|
|
625
|
+
assert json.loads(route.calls.last.request.content)["action"] == "mute"
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
@respx.mock
|
|
629
|
+
def test_finding_dismiss(logged_in, runner):
|
|
630
|
+
fid = _FULL_FINDING["id"]
|
|
631
|
+
route = respx.post(f"{BASE}/api/audits/findings/{fid}/status").mock(
|
|
632
|
+
return_value=httpx.Response(200, json={"id": fid, "action": "dismiss", "ok": True}))
|
|
633
|
+
result = runner.invoke(app, ["--json", "audits", "dismiss", fid, "--yes"])
|
|
634
|
+
assert result.exit_code == 0, result.output
|
|
635
|
+
assert json.loads(route.calls.last.request.content) == {"action": "dismiss"}
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
@respx.mock
|
|
639
|
+
def test_finding_resolve_human(logged_in, runner):
|
|
640
|
+
fid = _FULL_FINDING["id"]
|
|
641
|
+
respx.post(f"{BASE}/api/audits/findings/{fid}/status").mock(
|
|
642
|
+
return_value=httpx.Response(200, json={"id": fid, "action": "resolve", "ok": True}))
|
|
643
|
+
result = runner.invoke(app, ["audits", "resolve", fid, "--yes"])
|
|
644
|
+
assert result.exit_code == 0, result.output
|
|
645
|
+
assert "resolved finding" in result.stderr
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@respx.mock
|
|
649
|
+
def test_finding_reopen(logged_in, runner):
|
|
650
|
+
fid = _FULL_FINDING["id"]
|
|
651
|
+
route = respx.post(f"{BASE}/api/audits/findings/{fid}/status").mock(
|
|
652
|
+
return_value=httpx.Response(200, json={"id": fid, "action": "reopen", "ok": True}))
|
|
653
|
+
result = runner.invoke(app, ["audits", "reopen", fid])
|
|
654
|
+
assert result.exit_code == 0, result.output
|
|
655
|
+
assert json.loads(route.calls.last.request.content) == {"action": "reopen"}
|
|
656
|
+
assert "reopened finding" in result.stderr
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
@respx.mock
|
|
660
|
+
def test_finding_assign_sends_assignee(logged_in, runner):
|
|
661
|
+
fid = _FULL_FINDING["id"]
|
|
662
|
+
route = respx.post(f"{BASE}/api/audits/findings/{fid}/status").mock(
|
|
663
|
+
return_value=httpx.Response(200, json={"id": fid, "action": "assign", "ok": True}))
|
|
664
|
+
result = runner.invoke(app, ["audits", "assign", fid, "--to", "alice@example.com"])
|
|
665
|
+
assert result.exit_code == 0, result.output
|
|
666
|
+
assert json.loads(route.calls.last.request.content) == {
|
|
667
|
+
"action": "assign", "assigned_to": "alice@example.com"}
|
|
668
|
+
assert "assigned finding" in result.stderr and "alice@example.com" in result.stderr
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def test_finding_assign_without_to_exits_2(logged_in, runner):
|
|
672
|
+
result = runner.invoke(app, ["audits", "assign", _FULL_FINDING["id"]])
|
|
673
|
+
assert result.exit_code == 2 # --to is required
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def test_finding_unknown_action_exits_2(logged_in, runner):
|
|
677
|
+
# The triage verbs are the command surface — an unknown action is a usage error, never a 422.
|
|
678
|
+
result = runner.invoke(app, ["audits", "snooze", _FULL_FINDING["id"]])
|
|
679
|
+
assert result.exit_code == 2
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def test_triage_action_validator_rejects_unknown():
|
|
683
|
+
# The shared validator behind every triage verb (defence in depth for the --action value).
|
|
684
|
+
import pytest
|
|
685
|
+
|
|
686
|
+
from fp_cli import _click_compat as click # the Click Typer is running
|
|
687
|
+
from fp_cli.commands import audits_cmds
|
|
688
|
+
|
|
689
|
+
with pytest.raises(click.BadParameter):
|
|
690
|
+
audits_cmds._validate_action("snooze")
|
|
691
|
+
assert audits_cmds._validate_action("ack") == "ack"
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
@respx.mock
|
|
695
|
+
def test_finding_triage_not_found_exits_6(logged_in, runner):
|
|
696
|
+
fid = "1f5803aa-aaaa-bbbb-cccc-000000000000"
|
|
697
|
+
respx.post(f"{BASE}/api/audits/findings/{fid}/status").mock(
|
|
698
|
+
return_value=httpx.Response(404, json={"error": "not found"}))
|
|
699
|
+
result = runner.invoke(app, ["audits", "ack", fid])
|
|
700
|
+
assert result.exit_code == 6, result.output
|
|
701
|
+
assert "no finding" in result.stderr
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
@respx.mock
|
|
705
|
+
def test_finding_triage_forbidden_exits_5(logged_in, runner):
|
|
706
|
+
fid = _FULL_FINDING["id"]
|
|
707
|
+
respx.post(f"{BASE}/api/audits/findings/{fid}/status").mock(return_value=httpx.Response(
|
|
708
|
+
403, json={"error": "forbidden", "required_permission": "audits:write"}))
|
|
709
|
+
result = runner.invoke(app, ["--json", "audits", "ack", fid])
|
|
710
|
+
assert result.exit_code == 5
|
|
711
|
+
assert "audits:write" in json.loads(result.stdout)["error"]
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
# --- audits: reference context ---
|
|
715
|
+
#
|
|
716
|
+
# This surface shipped with zero tests, and the first thing an operator does
|
|
717
|
+
# after `context-set` is the thing that crashed: `context-show` dereferenced a
|
|
718
|
+
# theme token that does not exist, so every human-mode render raised
|
|
719
|
+
# AttributeError. `--json` returns before the formatter, which is why nothing
|
|
720
|
+
# scripted noticed. The human-render assertions below are the ones that matter.
|
|
721
|
+
|
|
722
|
+
_DOC_URL = "https://docs.example.com/runbook"
|
|
723
|
+
|
|
724
|
+
_CTX_PENDING = {
|
|
725
|
+
"text": "checkout agent for a retail store",
|
|
726
|
+
"sources": [
|
|
727
|
+
{"id": "s1", "url": _DOC_URL, "position": 0,
|
|
728
|
+
"status": "pending", "final_url": None, "title": None, "content_type": None,
|
|
729
|
+
"preview": "", "preview_truncated": False, "chars": 0, "chars_total": 0,
|
|
730
|
+
"truncated": False, "redactions": 0, "injection_markers": [],
|
|
731
|
+
"error_code": None, "error_detail": None, "fetched_at": None,
|
|
732
|
+
"attempted_at": None, "changed_at": None},
|
|
733
|
+
],
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
@respx.mock
|
|
738
|
+
def test_context_show_renders_a_pending_source(logged_in, runner):
|
|
739
|
+
"""The regression test for the AttributeError: a non-`ok` status took the
|
|
740
|
+
branch that referenced a missing theme token."""
|
|
741
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
742
|
+
respx.get(f"{BASE}/api/audits/au1/context").mock(
|
|
743
|
+
return_value=httpx.Response(200, json=_CTX_PENDING))
|
|
744
|
+
result = runner.invoke(app, ["audits", "context-show", "nightly-prod"])
|
|
745
|
+
assert result.exit_code == 0, result.output
|
|
746
|
+
assert "pending" in result.stderr
|
|
747
|
+
assert "docs.example.com" in result.stderr
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
@respx.mock
|
|
751
|
+
def test_context_show_renders_an_ok_source_with_injection_markers(logged_in, runner):
|
|
752
|
+
"""The other crashing branch: a stored page that carries markers — i.e. the
|
|
753
|
+
exact case the feature exists to surface."""
|
|
754
|
+
ctx = json.loads(json.dumps(_CTX_PENDING))
|
|
755
|
+
ctx["sources"][0].update(status="ok", chars=1200, chars_total=1200,
|
|
756
|
+
injection_markers=["ignore previous instructions"])
|
|
757
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
758
|
+
respx.get(f"{BASE}/api/audits/au1/context").mock(return_value=httpx.Response(200, json=ctx))
|
|
759
|
+
result = runner.invoke(app, ["audits", "context-show", "nightly-prod"])
|
|
760
|
+
assert result.exit_code == 0, result.output
|
|
761
|
+
assert "review" in result.stderr
|
|
762
|
+
assert "instructions to an AI" in result.stderr
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
@respx.mock
|
|
766
|
+
def test_context_show_tolerates_an_unknown_status(logged_in, runner):
|
|
767
|
+
"""The server owns this vocabulary; a value we do not know must render, not raise."""
|
|
768
|
+
ctx = json.loads(json.dumps(_CTX_PENDING))
|
|
769
|
+
ctx["sources"][0]["status"] = "quarantined"
|
|
770
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
771
|
+
respx.get(f"{BASE}/api/audits/au1/context").mock(return_value=httpx.Response(200, json=ctx))
|
|
772
|
+
result = runner.invoke(app, ["audits", "context-show", "nightly-prod"])
|
|
773
|
+
assert result.exit_code == 0, result.output
|
|
774
|
+
assert "quarantined" in result.stderr
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
@respx.mock
|
|
778
|
+
def test_context_show_marks_a_retained_snapshot_as_still_used(logged_in, runner):
|
|
779
|
+
"""A `failed` re-read does not withdraw the snapshot we already hold — the
|
|
780
|
+
server still puts it in the prompt. Reporting it as simply "failed" told the
|
|
781
|
+
operator the opposite, and suppressed its injection markers with it."""
|
|
782
|
+
ctx = json.loads(json.dumps(_CTX_PENDING))
|
|
783
|
+
ctx["sources"][0].update(status="failed", chars=1500, chars_total=1500,
|
|
784
|
+
error_detail="connection reset",
|
|
785
|
+
injection_markers=["ignore previous instructions"])
|
|
786
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
787
|
+
respx.get(f"{BASE}/api/audits/au1/context").mock(return_value=httpx.Response(200, json=ctx))
|
|
788
|
+
result = runner.invoke(app, ["audits", "context-show", "nightly-prod"])
|
|
789
|
+
assert result.exit_code == 0, result.output
|
|
790
|
+
assert "review" in result.stderr, "a retained page with markers must still say review"
|
|
791
|
+
assert "still used" in result.stderr
|
|
792
|
+
assert "1500 chars" in result.stderr
|
|
793
|
+
assert "instructions to an AI" in result.stderr
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
@respx.mock
|
|
797
|
+
def test_context_show_does_not_claim_a_blocked_page_is_used(logged_in, runner):
|
|
798
|
+
"""`blocked` is the one status that overrides a retained snapshot: the guard
|
|
799
|
+
refused the URL, so it is not in the prompt and must not read as though it is."""
|
|
800
|
+
ctx = json.loads(json.dumps(_CTX_PENDING))
|
|
801
|
+
ctx["sources"][0].update(status="blocked", chars=900, error_detail="private address")
|
|
802
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
803
|
+
respx.get(f"{BASE}/api/audits/au1/context").mock(return_value=httpx.Response(200, json=ctx))
|
|
804
|
+
result = runner.invoke(app, ["audits", "context-show", "nightly-prod"])
|
|
805
|
+
assert result.exit_code == 0, result.output
|
|
806
|
+
assert "blocked" in result.stderr
|
|
807
|
+
assert "still used" not in result.stderr
|
|
808
|
+
assert "private address" in result.stderr
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
@respx.mock
|
|
812
|
+
def test_context_set_text_only_preserves_existing_urls(logged_in, runner):
|
|
813
|
+
"""`--text` alone used to send `"urls": []`, deleting every reference page and
|
|
814
|
+
its snapshot — while the brief was read-merged three lines away."""
|
|
815
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
816
|
+
respx.get(f"{BASE}/api/audits/au1/context").mock(
|
|
817
|
+
return_value=httpx.Response(200, json=_CTX_PENDING))
|
|
818
|
+
put = respx.put(f"{BASE}/api/audits/au1/context").mock(
|
|
819
|
+
return_value=httpx.Response(200, json={"id": "au1", "sources": 1, "queued": 0}))
|
|
820
|
+
result = runner.invoke(app, ["--json", "audits", "context-set", "nightly-prod",
|
|
821
|
+
"--text", "new brief"])
|
|
822
|
+
assert result.exit_code == 0, result.output
|
|
823
|
+
sent = json.loads(put.calls[0].request.content)
|
|
824
|
+
assert sent["text"] == "new brief"
|
|
825
|
+
assert sent["urls"] == [_DOC_URL], "URLs must survive a --text edit"
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
@respx.mock
|
|
829
|
+
def test_context_set_url_only_preserves_the_brief(logged_in, runner):
|
|
830
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
831
|
+
respx.get(f"{BASE}/api/audits/au1/context").mock(
|
|
832
|
+
return_value=httpx.Response(200, json=_CTX_PENDING))
|
|
833
|
+
put = respx.put(f"{BASE}/api/audits/au1/context").mock(
|
|
834
|
+
return_value=httpx.Response(200, json={"id": "au1", "sources": 1, "queued": 1}))
|
|
835
|
+
other = "https://docs.example.com/other"
|
|
836
|
+
result = runner.invoke(app, ["--json", "audits", "context-set", "nightly-prod",
|
|
837
|
+
"--url", other])
|
|
838
|
+
assert result.exit_code == 0, result.output
|
|
839
|
+
sent = json.loads(put.calls[0].request.content)
|
|
840
|
+
assert sent["text"] == _CTX_PENDING["text"]
|
|
841
|
+
assert sent["urls"] == [other]
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
@respx.mock
|
|
845
|
+
def test_context_set_clear_urls_sends_an_empty_list(logged_in, runner):
|
|
846
|
+
"""Removal has to stay expressible — it is now explicit rather than implied."""
|
|
847
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
848
|
+
respx.get(f"{BASE}/api/audits/au1/context").mock(
|
|
849
|
+
return_value=httpx.Response(200, json=_CTX_PENDING))
|
|
850
|
+
put = respx.put(f"{BASE}/api/audits/au1/context").mock(
|
|
851
|
+
return_value=httpx.Response(200, json={"id": "au1", "sources": 0, "queued": 0}))
|
|
852
|
+
result = runner.invoke(app, ["--json", "audits", "context-set", "nightly-prod",
|
|
853
|
+
"--clear-urls"])
|
|
854
|
+
assert result.exit_code == 0, result.output
|
|
855
|
+
assert json.loads(put.calls[0].request.content)["urls"] == []
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
@respx.mock
|
|
859
|
+
def test_context_set_rejects_url_and_clear_urls_together(logged_in, runner):
|
|
860
|
+
result = runner.invoke(app, ["--json", "audits", "context-set", "nightly-prod",
|
|
861
|
+
"--url", _DOC_URL, "--clear-urls"])
|
|
862
|
+
assert result.exit_code == 2, result.output
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
# --- audits: reference-context telemetry ---
|
|
866
|
+
#
|
|
867
|
+
# These pin the emitted PROPERTY NAMES, which nothing else does: the CLI sent this
|
|
868
|
+
# count as `count` while the dashboard sent `url_count`, so one event carried two
|
|
869
|
+
# names and neither answered "how much context is being attached?" on its own.
|
|
870
|
+
# They assert what actually leaves `record_action`, because it filters through the
|
|
871
|
+
# `_SAFE_PROP_KEYS` allowlist (`commands/_write.py`) first — a property missing from
|
|
872
|
+
# that list is dropped with no error, so a rename that forgets its allowlist entry
|
|
873
|
+
# reads as correct at the call site and silently emits nothing.
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
@pytest.fixture
|
|
877
|
+
def emitted(monkeypatch):
|
|
878
|
+
"""Every ``(event, properties)`` the command sends, as the allowlist leaves it."""
|
|
879
|
+
events = []
|
|
880
|
+
monkeypatch.setattr(
|
|
881
|
+
analytics, "capture",
|
|
882
|
+
lambda event, properties=None: events.append((event, properties or {})),
|
|
883
|
+
)
|
|
884
|
+
return events
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _props(emitted, event: str) -> dict:
|
|
888
|
+
"""The one ``event``'s properties — failing if it did not fire exactly once."""
|
|
889
|
+
matches = [props for name, props in emitted if name == event]
|
|
890
|
+
assert len(matches) == 1, f"expected one {event}, got {[name for name, _ in emitted]}"
|
|
891
|
+
return matches[0]
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
@respx.mock
|
|
895
|
+
def test_context_set_emits_url_count(logged_in, runner, emitted):
|
|
896
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
897
|
+
respx.get(f"{BASE}/api/audits/au1/context").mock(
|
|
898
|
+
return_value=httpx.Response(200, json=_CTX_PENDING))
|
|
899
|
+
respx.put(f"{BASE}/api/audits/au1/context").mock(
|
|
900
|
+
return_value=httpx.Response(200, json={"id": "au1", "sources": 2, "queued": 2}))
|
|
901
|
+
result = runner.invoke(app, ["--json", "audits", "context-set", "nightly-prod",
|
|
902
|
+
"--url", _DOC_URL, "--url", "https://docs.example.com/slo"])
|
|
903
|
+
assert result.exit_code == 0, result.output
|
|
904
|
+
props = _props(emitted, "audit_context_saved")
|
|
905
|
+
assert props["url_count"] == 2
|
|
906
|
+
assert "count" not in props, "the dashboard's name for this is url_count — one series, one name"
|
|
907
|
+
assert props["via"] == "cli"
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
@respx.mock
|
|
911
|
+
def test_audits_create_with_context_emits_url_count(logged_in, runner, emitted):
|
|
912
|
+
"""The second call site of the same event: context attached at creation."""
|
|
913
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[]))
|
|
914
|
+
respx.post(f"{BASE}/api/audits").mock(
|
|
915
|
+
return_value=httpx.Response(201, json={"id": "au9", "created_at": "t", "sources": 1}))
|
|
916
|
+
result = runner.invoke(app, ["--json", "audits", "create", "nightly-prod",
|
|
917
|
+
"--text", "checkout agent", "--url", _DOC_URL])
|
|
918
|
+
assert result.exit_code == 0, result.output
|
|
919
|
+
props = _props(emitted, "audit_context_saved")
|
|
920
|
+
assert props["url_count"] == 1 and "count" not in props
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
@respx.mock
|
|
924
|
+
def test_context_refresh_emits_url_count(logged_in, runner, emitted):
|
|
925
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
926
|
+
respx.post(f"{BASE}/api/audits/au1/context/refresh").mock(
|
|
927
|
+
return_value=httpx.Response(200, json={"queued": 3, "skipped": 1}))
|
|
928
|
+
result = runner.invoke(app, ["--json", "audits", "context-refresh", "nightly-prod"])
|
|
929
|
+
assert result.exit_code == 0, result.output
|
|
930
|
+
props = _props(emitted, "audit_context_refreshed")
|
|
931
|
+
assert props["url_count"] == 3 and "count" not in props
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
@respx.mock
|
|
935
|
+
def test_edit_from_show_json_round_trips(logged_in, runner, tmp_path):
|
|
936
|
+
"""`audits show --json` emits every field, including the two the definition
|
|
937
|
+
endpoint 422s. Feeding that straight back is the documented workflow, so the
|
|
938
|
+
read-only keys are stripped client-side."""
|
|
939
|
+
shown = dict(_FULL_AUDIT, additional_context="a brief", reference_url_count=2, run_count=9)
|
|
940
|
+
f = tmp_path / "audit.json"
|
|
941
|
+
f.write_text(json.dumps(shown))
|
|
942
|
+
respx.get(f"{BASE}/api/audits").mock(return_value=httpx.Response(200, json=[_FULL_AUDIT]))
|
|
943
|
+
put = respx.put(f"{BASE}/api/audits/au1").mock(
|
|
944
|
+
return_value=httpx.Response(200, json=_FULL_AUDIT))
|
|
945
|
+
result = runner.invoke(app, ["--json", "audits", "edit", "nightly-prod", "--file", str(f)])
|
|
946
|
+
assert result.exit_code == 0, result.output
|
|
947
|
+
sent = json.loads(put.calls[0].request.content)
|
|
948
|
+
for banned in ("additional_context", "reference_url_count", "run_count", "id", "open_findings"):
|
|
949
|
+
assert banned not in sent, f"{banned} must not reach the definition endpoint"
|
|
950
|
+
assert sent["name"] == "nightly-prod"
|