failproofai 1.0.12-beta.2 → 1.0.12
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 +4 -4
- 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.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 +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
- 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.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 +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- 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.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.nft.json +1 -1
- 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.nft.json +1 -1
- 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.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 +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- 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 +14 -14
- 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.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.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.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 +8 -8
- 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/_0tovk6q0miud6._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pwj2h2efe5f-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00l2u6j86cb8y._.js → [root-of-the-server]__167mipqmjprq5._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bseae9u7g98r._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0mouhaflye1ro._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1fw0_nzkae5ao._.js → _1d98fyfrcaqhy._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_20hn4gn87nsjc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr0vt64h._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1fv03uigni8s5.js → node_modules_html-to-image_es_index_0tqc05n832v7a.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0janj0i6dqko7._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- 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/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/{1c_jf6g5wnslk.js → 0el9n3h9_zp5m.js} +1 -1
- package/.next/standalone/.next/static/chunks/0k7g_8x3o6gd-.js +1 -0
- package/.next/standalone/.next/static/chunks/{2o6d37j425uci.js → 0umzct31duk5g.js} +1 -1
- package/.next/standalone/.next/static/chunks/{42xptz88mtq3x.js → 0wobqgosirs8p.js} +1 -1
- package/.next/standalone/.next/static/chunks/12rug3jilwqzs.js +1 -0
- package/.next/standalone/.next/static/chunks/{0ay__-d2h1xb5.js → 1kduj74-715jh.js} +1 -1
- package/.next/standalone/.next/static/chunks/{40oworn_-x12p.js → 2701amhrbsbot.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0tbm3vjm62h-2.js → 27cavoc8paelg.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1my3-5p47t7r-.js → 2ve23a7keupjw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{21hckpmsc4g70.js → 3bv6xaoitjpnh.js} +2 -2
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +19 -0
- package/.next/standalone/fp-cloud-cli/README.md +120 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/app.py +15 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +198 -7
- package/.next/standalone/fp-cloud-cli/fp_cli/cloud_output.py +523 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/_cloud.py +45 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +3 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/account_cmds.py +171 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +80 -7
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +23 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +2 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/contracts_cmds.py +67 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/dashboards_cmds.py +159 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/evaluators_cmds.py +380 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +14 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +82 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +4 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +17 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +143 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +70 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/sentiment_cmds.py +49 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +95 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +2 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +21 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +14 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +3 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/evaluator_spec.py +266 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +187 -81
- package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +21 -24
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +124 -23
- package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +2 -1
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +168 -16
- package/.next/standalone/fp-cloud-cli/tests/fake_cloud_smoke.py +312 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_account.py +125 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_agent.py +253 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_human_views.py +287 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_observability.py +116 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_transport.py +141 -0
- package/.next/standalone/fp-cloud-cli/tests/test_commands.py +3 -3
- package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +3 -3
- package/.next/standalone/fp-cloud-cli/tests/test_evaluator_authoring.py +449 -0
- package/.next/standalone/fp-cloud-cli/tests/test_facets.py +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +15 -0
- package/.next/standalone/fp-cloud-cli/tests/test_operator.py +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_output.py +37 -37
- package/.next/standalone/fp-cloud-cli/tests/test_round4_behaviour.py +144 -0
- package/.next/standalone/fp-cloud-cli/tests/test_screen_language.py +80 -0
- package/.next/standalone/fp-cloud-cli/tests/test_session_reevaluate.py +33 -0
- package/.next/standalone/fp-cloud-cli/tests/test_staging_parity.py +198 -0
- package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +1 -1
- package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +68 -176
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +2 -2
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +98 -115
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +3 -3
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +149 -171
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +2 -2
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +25 -25
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +10 -10
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +85 -37
- package/dist/worker.mjs +73 -28
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +9 -9
- package/src/hooks/agent-skill.ts +3 -0
- package/src/hooks/fp-home.ts +1 -1
- package/src/hooks/handler.ts +22 -3
- package/src/hooks/semantic/intent.ts +127 -24
- package/src/hooks/semantic/jev-review.ts +21 -2
- package/.next/standalone/.next/static/chunks/20py8k1gd_i1m.js +0 -1
- package/.next/standalone/.next/static/chunks/2hmex93ppj1ke.js +0 -1
- /package/.next/standalone/.next/static/{LHCtFxVG0mdvd4lsXAOFC → D7LY1YSrw96ZxKSpxxrFw}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{LHCtFxVG0mdvd4lsXAOFC → D7LY1YSrw96ZxKSpxxrFw}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{LHCtFxVG0mdvd4lsXAOFC → D7LY1YSrw96ZxKSpxxrFw}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from io import StringIO
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
import respx
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from fp_cli import output
|
|
11
|
+
from fp_cli.app import app
|
|
12
|
+
|
|
13
|
+
BASE = "http://dash.test"
|
|
14
|
+
RESULT = {"columns": [{"name": "total", "type": "UInt64"}], "rows": [[42]]}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def sse(events):
|
|
18
|
+
return "".join("data: " + json.dumps(e) + "\n\n" for e in events)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.parametrize("json_mode,approve,tty,input_text,decision", [
|
|
22
|
+
(True, False, False, "", "reject"),
|
|
23
|
+
(True, True, False, "", "approve"),
|
|
24
|
+
(False, False, False, "", "reject"),
|
|
25
|
+
(False, False, True, "y\n", "approve"),
|
|
26
|
+
(False, False, True, "n\n", "reject"),
|
|
27
|
+
])
|
|
28
|
+
def test_ask_artifacts_and_approvals(logged_in, runner, monkeypatch, json_mode, approve, tty, input_text, decision):
|
|
29
|
+
events = [
|
|
30
|
+
{"type": "ui-action", "action": "open_artifact", "artifact": {"title": "Event total", "sql": "SELECT count()"}},
|
|
31
|
+
{"type": "ask-user", "callId": "ask_1", "kind": "approval", "question": "Disable alert?", "details": "alert a1"},
|
|
32
|
+
{"type": "ask-user-answered", "callId": "ask_1", "decision": decision},
|
|
33
|
+
{"type": "text-delta", "text": "Here is the result."},
|
|
34
|
+
{"type": "done"},
|
|
35
|
+
]
|
|
36
|
+
# Patch the exact guard used by the command; CliRunner's stdin is otherwise non-TTY.
|
|
37
|
+
import sys
|
|
38
|
+
monkeypatch.setattr(sys.stdin, "isatty", lambda: tty)
|
|
39
|
+
# Runner replaces stdin during invocation, so patch the module's sys proxy instead.
|
|
40
|
+
from fp_cli.commands import agent_cmds
|
|
41
|
+
class Stdin:
|
|
42
|
+
def isatty(self):
|
|
43
|
+
return tty
|
|
44
|
+
class Sys:
|
|
45
|
+
stdin = Stdin()
|
|
46
|
+
stdout = sys.stdout
|
|
47
|
+
monkeypatch.setattr(agent_cmds, "sys", Sys())
|
|
48
|
+
with respx.mock as mock:
|
|
49
|
+
stream = mock.post(f"{BASE}/api/agent/chat").respond(200, text=sse(events), headers={"content-type": "text/event-stream"})
|
|
50
|
+
query = mock.post(f"{BASE}/api/queries/run").respond(200, json=RESULT)
|
|
51
|
+
answer = mock.post(f"{BASE}/api/agent/answer").respond(204)
|
|
52
|
+
mock.post(f"{BASE}/api/agent/conversations").respond(200, json={"id": "c1"})
|
|
53
|
+
saved = mock.put(f"{BASE}/api/agent/conversations/c1/messages").respond(204)
|
|
54
|
+
result = runner.invoke(app, [*(["--json"] if json_mode else []), "agent", "ask", "Count events",
|
|
55
|
+
*(["--approve"] if approve else [])], input=input_text)
|
|
56
|
+
assert result.exit_code == 0, result.output
|
|
57
|
+
assert json.loads(answer.calls[0].request.content) == {"callId": "ask_1", "decision": decision}
|
|
58
|
+
assert query.called
|
|
59
|
+
assert json.loads(stream.calls[0].request.content)["pageActions"] == [{"action": "open_artifact"}, {"action": "home"}]
|
|
60
|
+
assert json.loads(saved.calls[0].request.content)["messages"][-1]["content"]["artifacts"][0]["sql"] == "SELECT count()"
|
|
61
|
+
if json_mode:
|
|
62
|
+
data = json.loads(result.stdout)
|
|
63
|
+
assert data["answer"] == "Here is the result."
|
|
64
|
+
assert data["artifacts"][0]["result"] == RESULT
|
|
65
|
+
else:
|
|
66
|
+
assert "EVENT TOTAL" in result.output
|
|
67
|
+
assert "42" in result.output
|
|
68
|
+
assert result.stdout.index("failproof ai v") < result.stdout.index("EVENT TOTAL")
|
|
69
|
+
if tty:
|
|
70
|
+
assert "alert a1" in result.output
|
|
71
|
+
assert "Approve this change" in result.output
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_home_blocks(logged_in, runner):
|
|
75
|
+
stream = sse([{"type": "ui-action", "action": "home", "home": {"op": "replace", "blocks": [
|
|
76
|
+
{"key": "n", "kind": "stats", "title": "Count", "sql": "SELECT 42"},
|
|
77
|
+
{"key": "note", "kind": "note", "title": "Summary", "text": "All good"},
|
|
78
|
+
]}}, {"type": "done"}])
|
|
79
|
+
with respx.mock as mock:
|
|
80
|
+
mock.post(f"{BASE}/api/agent/chat").respond(200, text=stream)
|
|
81
|
+
query = mock.post(f"{BASE}/api/queries/run").respond(200, json=RESULT)
|
|
82
|
+
mock.post(f"{BASE}/api/agent/conversations").respond(200, json={"id": "c1"})
|
|
83
|
+
mock.put(f"{BASE}/api/agent/conversations/c1/messages").respond(204)
|
|
84
|
+
result = runner.invoke(app, ["--json", "agent", "ask", "Summarize"])
|
|
85
|
+
assert result.exit_code == 0, result.output
|
|
86
|
+
assert query.call_count == 1
|
|
87
|
+
assert len(json.loads(result.stdout)["artifacts"]) == 2
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.parametrize("json_mode", [False, True])
|
|
91
|
+
def test_brief(logged_in, runner, json_mode):
|
|
92
|
+
with respx.mock as mock:
|
|
93
|
+
route = mock.post(f"{BASE}/api/agent/home-brief").respond(200, json={
|
|
94
|
+
"greeting": "Hello", "summary": "Healthy", "blocks": [{"title": "Events", "sql": "SELECT count()"}]})
|
|
95
|
+
mock.post(f"{BASE}/api/queries/run").respond(200, json=RESULT)
|
|
96
|
+
result = runner.invoke(app, [*(["--json"] if json_mode else []), "agent", "brief", "--range", "7d"])
|
|
97
|
+
assert result.exit_code == 0, result.output
|
|
98
|
+
assert "Healthy" in result.output
|
|
99
|
+
assert json.loads(route.calls[0].request.content) == {"range": "7d"}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.mark.parametrize("argv,path,response", [
|
|
103
|
+
(["policies", "author", "Prevent force push"], "/api/agent/policy-author", "stream"),
|
|
104
|
+
(["policies", "backtest", "{file}"], "/api/agent/policy-backtest", {"matched": 3}),
|
|
105
|
+
(["policies", "candidacy", "--issue", "i1"], "/api/agent/policy-candidacy", {"candidate": True}),
|
|
106
|
+
])
|
|
107
|
+
def test_policy_commands(logged_in, runner, tmp_path, argv, path, response):
|
|
108
|
+
file = tmp_path / "policy.mjs"
|
|
109
|
+
file.write_text("export default {}")
|
|
110
|
+
with respx.mock(assert_all_called=False) as mock:
|
|
111
|
+
mock.get(f"{BASE}/api/issues/i1").respond(200, json={"id": "i1", "title": "Force pushes"})
|
|
112
|
+
if response == "stream":
|
|
113
|
+
route = mock.post(f"{BASE}{path}").respond(200, text=sse([{"type": "done", "result": {"source": "policy"}}]))
|
|
114
|
+
else:
|
|
115
|
+
route = mock.post(f"{BASE}{path}").respond(200, json=response)
|
|
116
|
+
result = runner.invoke(app, ["--json", *[v.format(file=file) for v in argv]])
|
|
117
|
+
assert result.exit_code == 0, result.output
|
|
118
|
+
assert route.called
|
|
119
|
+
json.loads(result.stdout)
|
|
120
|
+
if argv[1] == "author":
|
|
121
|
+
assert json.loads(route.calls[0].request.content)["issue"]["title"] == "Prevent force push"
|
|
122
|
+
elif argv[1] == "backtest":
|
|
123
|
+
assert json.loads(route.calls[0].request.content) == {"source": "export default {}"}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@pytest.mark.parametrize("argv", [
|
|
127
|
+
["policies", "author", "Prevent force push"], ["policies", "backtest", "p1"],
|
|
128
|
+
["policies", "candidacy", "--issue", "i1"], ["agent", "brief"],
|
|
129
|
+
])
|
|
130
|
+
def test_session_only(logged_in, runner, argv):
|
|
131
|
+
with respx.mock as mock:
|
|
132
|
+
result = runner.invoke(app, ["--json", "--api-key", "key", *argv])
|
|
133
|
+
assert not mock.calls
|
|
134
|
+
assert result.exit_code == 2
|
|
135
|
+
assert "needs a signed-in session" in result.stdout
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@pytest.mark.parametrize("argv", [
|
|
139
|
+
["policies", "author", "Prevent force push"], ["policies", "backtest", "p1"],
|
|
140
|
+
["policies", "candidacy", "--issue", "i1"], ["agent", "brief"], ["agent", "ask", "Hello"],
|
|
141
|
+
])
|
|
142
|
+
def test_404(logged_in, runner, argv):
|
|
143
|
+
with respx.mock as mock:
|
|
144
|
+
mock.route().respond(404, json={"error": "unknown route"})
|
|
145
|
+
result = runner.invoke(app, ["--json", *argv])
|
|
146
|
+
assert result.exit_code == 6, result.output
|
|
147
|
+
assert "not available on this FailproofAI Cloud yet" in result.stdout
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_backtest_published_policy_uses_latest_version(logged_in, runner):
|
|
151
|
+
with respx.mock as mock:
|
|
152
|
+
mock.get(f"{BASE}/api/enforcement/policies").respond(200, json=[
|
|
153
|
+
{"id": "p1", "version": 1, "source": "old"},
|
|
154
|
+
{"id": "p1", "version": 2, "source": "new"},
|
|
155
|
+
])
|
|
156
|
+
route = mock.post(f"{BASE}/api/agent/policy-backtest").respond(200, json={"matched": 1})
|
|
157
|
+
result = runner.invoke(app, ["--json", "policies", "backtest", "p1"])
|
|
158
|
+
assert result.exit_code == 0, result.output
|
|
159
|
+
assert json.loads(route.calls[0].request.content) == {"source": "new"}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.parametrize("json_mode", [False, True])
|
|
163
|
+
@pytest.mark.parametrize("failure,exit_code", [("404", 6), ("network", 3)])
|
|
164
|
+
def test_approval_answer_failure_is_clean(logged_in, runner, json_mode, failure, exit_code):
|
|
165
|
+
with respx.mock(assert_all_called=False) as mock:
|
|
166
|
+
mock.post(f"{BASE}/api/agent/chat").respond(200, text=sse([
|
|
167
|
+
{"type": "ask-user", "kind": "approval", "callId": "ask_1", "question": "Disable alert?"},
|
|
168
|
+
{"type": "text-delta", "text": "Must not reach this answer."},
|
|
169
|
+
{"type": "done"},
|
|
170
|
+
]))
|
|
171
|
+
answer = mock.post(f"{BASE}/api/agent/answer")
|
|
172
|
+
if failure == "404":
|
|
173
|
+
answer.respond(404, json={"error": "unknown route"})
|
|
174
|
+
else:
|
|
175
|
+
answer.mock(side_effect=httpx.ConnectError("connection refused"))
|
|
176
|
+
saved = mock.post(f"{BASE}/api/agent/conversations").respond(200, json={"id": "c1"})
|
|
177
|
+
result = runner.invoke(app, [*(["--json"] if json_mode else []), "agent", "ask", "Hello", "--approve"])
|
|
178
|
+
assert result.exit_code == exit_code, result.output
|
|
179
|
+
assert answer.called and not saved.called
|
|
180
|
+
assert "Traceback" not in result.output
|
|
181
|
+
assert "Must not reach this answer" not in result.output
|
|
182
|
+
if json_mode:
|
|
183
|
+
data = json.loads(result.stdout)
|
|
184
|
+
assert data["exit_code"] == exit_code
|
|
185
|
+
message = data["error"]
|
|
186
|
+
else:
|
|
187
|
+
assert len(result.stderr.strip().splitlines()) == 1
|
|
188
|
+
message = result.stderr
|
|
189
|
+
assert "Assistant approval answer failed" in message
|
|
190
|
+
assert "delivery unconfirmed" in message
|
|
191
|
+
if failure == "404":
|
|
192
|
+
assert "not available on this FailproofAI Cloud yet" in message
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@pytest.mark.parametrize("failure,exit_code", [("404", 6), ("network", 3)])
|
|
196
|
+
def test_approval_failure_restores_terminal_status(logged_in, runner, monkeypatch, failure, exit_code):
|
|
197
|
+
# The suite defaults to TERM=dumb, which suppresses live cursor control.
|
|
198
|
+
monkeypatch.setenv("TERM", "xterm-256color")
|
|
199
|
+
capture = StringIO()
|
|
200
|
+
terminal = Console(file=capture, force_terminal=True, color_system="truecolor", width=200)
|
|
201
|
+
original_configure = output.configure
|
|
202
|
+
|
|
203
|
+
def configure_terminal(**kwargs):
|
|
204
|
+
original_configure(**kwargs)
|
|
205
|
+
monkeypatch.setattr(output, "_stderr", terminal)
|
|
206
|
+
|
|
207
|
+
monkeypatch.setattr(output, "configure", configure_terminal)
|
|
208
|
+
with respx.mock as mock:
|
|
209
|
+
mock.post(f"{BASE}/api/agent/chat").respond(200, text=sse([
|
|
210
|
+
{"type": "ask-user", "kind": "approval", "callId": "ask_1", "question": "Disable alert?"},
|
|
211
|
+
]))
|
|
212
|
+
answer = mock.post(f"{BASE}/api/agent/answer")
|
|
213
|
+
if failure == "404":
|
|
214
|
+
answer.respond(404, json={"error": "unknown route"})
|
|
215
|
+
else:
|
|
216
|
+
answer.mock(side_effect=httpx.ConnectError("connection refused"))
|
|
217
|
+
result = runner.invoke(app, ["agent", "ask", "Hello", "--approve"])
|
|
218
|
+
assert result.exit_code == exit_code, result.output
|
|
219
|
+
rendered = capture.getvalue()
|
|
220
|
+
assert terminal._live_stack == []
|
|
221
|
+
assert "\x1b[?25l" in rendered
|
|
222
|
+
assert rendered.rfind("\x1b[?25h") > rendered.rfind("\x1b[?25l")
|
|
223
|
+
assert rendered.index("Assistant approval answer failed") > rendered.rfind("\x1b[?25h")
|
|
224
|
+
plain = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", rendered)
|
|
225
|
+
assert plain.count("Assistant approval answer failed") == 1
|
|
226
|
+
assert "Traceback" not in plain
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@pytest.mark.parametrize("json_mode", [False, True])
|
|
230
|
+
def test_ask_api_key_uses_session_only_wording(logged_in, runner, json_mode):
|
|
231
|
+
with respx.mock as mock:
|
|
232
|
+
result = runner.invoke(app, [
|
|
233
|
+
"--api-key", "key", *(["--json"] if json_mode else []),
|
|
234
|
+
"agent", "ask", "Hello",
|
|
235
|
+
])
|
|
236
|
+
assert not mock.calls
|
|
237
|
+
assert result.exit_code == 2, result.output
|
|
238
|
+
expected = "`fp agent ask` needs a signed-in session: run `fp login`"
|
|
239
|
+
if json_mode:
|
|
240
|
+
assert json.loads(result.stdout)["error"] == expected
|
|
241
|
+
else:
|
|
242
|
+
assert expected in result.stderr
|
|
243
|
+
assert len(result.stderr.strip().splitlines()) == 1
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_artifact_header_precedes_title_once(capsys):
|
|
247
|
+
from fp_cli.commands.agent_cmds import render_artifact
|
|
248
|
+
output.configure(no_color=True)
|
|
249
|
+
render_artifact({"title": "First artifact", "result": RESULT})
|
|
250
|
+
render_artifact({"title": "Second artifact", "result": RESULT})
|
|
251
|
+
rendered = capsys.readouterr().out
|
|
252
|
+
assert rendered.count("failproof ai v") == 1
|
|
253
|
+
assert rendered.index("failproof ai v") < rendered.index("FIRST ARTIFACT") < rendered.index("SECOND ARTIFACT")
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Real command output with server-shaped payloads at both terminal widths."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pytest
|
|
9
|
+
import respx
|
|
10
|
+
|
|
11
|
+
from fp_cli.app import app
|
|
12
|
+
|
|
13
|
+
BASE = "http://dash.test"
|
|
14
|
+
CODE = 'EvalResult(score=Score(0.7), reasoning="Tool usage")'
|
|
15
|
+
DEFINITION = {
|
|
16
|
+
"eval_key": "tool_usage", "display_name": "Tool usage", "eval_version": "1",
|
|
17
|
+
"result_kind": "score", "enabled": True, "labels": ["tools", "reliability"],
|
|
18
|
+
"condition_source": 'session.agent_id == "support-bot"', "evaluator_source": CODE,
|
|
19
|
+
"id": "b370402a-0ace-4e45-9c1c-d40e7062b5ea", "created_at": "2026-10-14T05:26:14Z",
|
|
20
|
+
}
|
|
21
|
+
ROLE = {"name": "reviewer", "is_builtin": False, "user_count": 3,
|
|
22
|
+
"permissions": ["events:read", "evaluations:read", "queries:run", "users:read"],
|
|
23
|
+
"description": "Review agent quality"}
|
|
24
|
+
DASHBOARD = {"id": "board1", "name": "Quality", "description": "Review production agents",
|
|
25
|
+
"tile_count": 1, "definition": {"filters": {"env": ["prod"]}}}
|
|
26
|
+
TILE = {"id": "tile1", "query_id": "query1", "title": "Traffic", "chart_type": "bar",
|
|
27
|
+
"chart_config": {"x": "agent", "y": "events"}, "param_bindings": {},
|
|
28
|
+
"pos_x": 0, "pos_y": 0, "pos_w": 6, "pos_h": 4}
|
|
29
|
+
DRY_RUN = {"runs": [{"session_id": "run-check", "ok": True, "mode": "run",
|
|
30
|
+
"result": {"score": {"value": 0.7}, "reasoning": "Seven tool calls"},
|
|
31
|
+
"duration_ms": 8}],
|
|
32
|
+
"summary": {"succeeded": 1, "failed": 0, "skipped": 0}}
|
|
33
|
+
QUERY = {"columns": [{"name": "agent", "type": "String"}, {"name": "events", "type": "UInt64"}],
|
|
34
|
+
"rows": [["support-bot", 7]], "elapsed_ms": 9, "truncated": False}
|
|
35
|
+
SESSION = {"session_id": "run-check", "agent_id": "support-bot", "environment": "prod",
|
|
36
|
+
"event_count": 7, "status": "done"}
|
|
37
|
+
CONTRACT = {"id": "contract1", "agent_id": "support-bot", "body": "# Purpose\nResolve requests",
|
|
38
|
+
"enabled": True, "purpose": "Resolve requests"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def response(request):
|
|
42
|
+
path = request.url.path
|
|
43
|
+
method = request.method
|
|
44
|
+
body = json.loads(request.content) if request.content else {}
|
|
45
|
+
data = {
|
|
46
|
+
"/api/evaluator-instances": {"instances": [
|
|
47
|
+
{"name": "failproof-managed", "kind": "managed", "enabled": True, "eval_count": 4,
|
|
48
|
+
"queued": 2, "id": "instance1"}]},
|
|
49
|
+
"/api/evaluator-schema-profile": {"window_days": 7, "sampled_events": 100, "event_types": [
|
|
50
|
+
{"event_type": "tool_use", "count": 100, "keys": [{"key": "tool_name", "type": "string", "presence": 1.0}]}]},
|
|
51
|
+
"/api/evaluation-jobs": {"jobs": [{"session_id": "run-check", "eval_key": "tool_usage",
|
|
52
|
+
"status": "queued", "id": "job1"}]},
|
|
53
|
+
"/api/evaluations/backfill": {"queued": True, "sessions": 12, "eval_key": "tool_usage"},
|
|
54
|
+
"/api/permission-sets": [ROLE],
|
|
55
|
+
"/api/permission-sets/reviewer": ROLE,
|
|
56
|
+
"/api/dashboards": {"dashboards": [DASHBOARD]},
|
|
57
|
+
"/api/dashboards/board1": {"dashboard": DASHBOARD, "tiles": [TILE]},
|
|
58
|
+
"/api/dashboards/board1/tiles": {"tiles": [TILE]},
|
|
59
|
+
"/api/dashboards/board1/tiles/tile1": TILE,
|
|
60
|
+
"/api/dashboards/board1/tiles/layout": {"updated": 1},
|
|
61
|
+
"/api/sessions": {"sessions": [SESSION]},
|
|
62
|
+
"/api/events/summary": {"events": [{"ts": "2026-10-14T05:26:14Z", "event_type": "tool_use",
|
|
63
|
+
"agent_id": "support-bot", "summary": "Read customer request"}]},
|
|
64
|
+
"/api/sessions/run-check/export": {**SESSION, "events": []},
|
|
65
|
+
"/api/sessions/run-check/evaluation-history": {"evaluations": [
|
|
66
|
+
{"eval_key": "tool_usage", "eval_version": "1", "status": "done",
|
|
67
|
+
"score": {"value": 0.7}, "finished_at": "2026-10-14T05:26:14Z"}]},
|
|
68
|
+
"/api/sessions/run-check/re-evaluate": {
|
|
69
|
+
"session_id": "run-check", "status": "queued", "v2_assignments_queued": 1,
|
|
70
|
+
"v2_assignment_ids": ["assignment-uuid"]},
|
|
71
|
+
"/api/events/latency_aggregate": {"p50": 42, "p95": 145, "models": [
|
|
72
|
+
{"model": "model1", "calls": 10, "p95_ms": 145}]},
|
|
73
|
+
"/api/events/sentiment": {"totals": {"messages": 10, "flagged": 2, "sessions": 3},
|
|
74
|
+
"averages": {"frustration": 0.2}, "timeline": [], "by_agent": []},
|
|
75
|
+
"/api/events/sentiment/messages": {"items": [{"session_id": "run-check",
|
|
76
|
+
"text": "Please try again", "scores": {"frustration": 0.8, "satisfaction": 0.1}}]},
|
|
77
|
+
"/api/events/sentiment/sessions": {"sessions": [{"session_id": "run-check",
|
|
78
|
+
"negative_score": 0.8, "message_count": 10, "feeling": "frustration"}]},
|
|
79
|
+
"/api/events/sentiment/sessions/run-check": {"session_id": "run-check",
|
|
80
|
+
"scores": {"frustration": 0.8}, "messages": []},
|
|
81
|
+
"/api/queries/run": QUERY,
|
|
82
|
+
"/api/alerts/recipients": {"recipients": [{"email": "ops@example.test", "channel": "email"}]},
|
|
83
|
+
"/api/alerts/alert1/issues": {"issues": [{"title": "Too many failures", "state": "open",
|
|
84
|
+
"severity": "warning", "id": "issue1"}]},
|
|
85
|
+
"/api/audits/contracts": {"contracts": [CONTRACT]},
|
|
86
|
+
"/api/audits/contracts/contract1": CONTRACT,
|
|
87
|
+
"/api/agent/policy-backtest": {"matched": 4, "allowed": 6, "denied": 4,
|
|
88
|
+
"examples": [{"session_id": "run-check", "reason": "Force push blocked"}]},
|
|
89
|
+
"/api/agent/policy-candidacy": {"candidate": True, "reason": "Force push can be blocked", "intent": "Prevent force push"},
|
|
90
|
+
"/api/issues/issue1": {"id": "issue1", "title": "Force push incident"},
|
|
91
|
+
"/api/users/defaults": {"permission_set": "reviewer", "permissions": ROLE["permissions"]},
|
|
92
|
+
"/api/usage/windows": {"windows": [{"from": "2026-09-01", "to": "2026-10-01",
|
|
93
|
+
"usage": {"events_ingested": 300, "evaluations": 40}}]},
|
|
94
|
+
"/api/offers/me": {"offers": [{"offer_key": "team_offer", "tier": "team",
|
|
95
|
+
"status": "available", "expires_at": "2026-10-31"}]},
|
|
96
|
+
"/api/team-trial/me": {"active": True, "ends_at": "2026-10-31"},
|
|
97
|
+
"/api/offers/claim": {"claimed": True, "grant": {"tier": "team", "expires_at": "2026-10-31"}},
|
|
98
|
+
"/api/settings/model-context-windows": {"model_context_windows": [
|
|
99
|
+
{"model_id": "provider/model", "context_window": 64000, "updated_at": "2026-10-14"}]},
|
|
100
|
+
"/api/otel/status": {"enabled": True, "events_24h": 10, "last_received_at": "2026-10-14",
|
|
101
|
+
"content_missing": False, "services": [{"service_name": "support-service", "events": 10}]},
|
|
102
|
+
"/api/integrations/slack": {"connected": True, "team_name": "Reliability", "channel_name": "agent-alerts"},
|
|
103
|
+
"/api/agent/home-brief": {"greeting": "Hello", "summary": "One session needs attention", "blocks": [
|
|
104
|
+
{"title": "Traffic", "kind": "table", "sql": "SELECT 1"},
|
|
105
|
+
{"title": "Next steps", "kind": "note", "text": "Inspect the failed session"}]},
|
|
106
|
+
}.get(path)
|
|
107
|
+
if path == "/api/evaluator-definitions":
|
|
108
|
+
data = [DEFINITION] if method == "GET" else {**DEFINITION, **body, "id": "new-definition"}
|
|
109
|
+
elif path.startswith("/api/evaluator-definitions/") and path.endswith("dry-run"):
|
|
110
|
+
data = DRY_RUN
|
|
111
|
+
elif path.startswith("/api/evaluator-definitions/"):
|
|
112
|
+
data = {**DEFINITION, **body}
|
|
113
|
+
elif path == "/api/agent/compose-eval":
|
|
114
|
+
data = {k: v for k, v in DEFINITION.items() if k not in ("id", "created_at", "enabled")}
|
|
115
|
+
elif path in ("/api/agent/compose-sql", "/api/agent/policy-author"):
|
|
116
|
+
draft = {"sql": "SELECT agent, events FROM traffic"} if path.endswith("compose-sql") else {
|
|
117
|
+
"source": "customPolicies.add({ name: 'no-force-push' });",
|
|
118
|
+
"verdict": {"tone": "good", "reason": "Blocks force pushes"}, "tests": {"ran": 4, "passed": 4}}
|
|
119
|
+
return httpx.Response(200, text="data: " + json.dumps({"type": "done", "result": draft}) + "\n\n",
|
|
120
|
+
headers={"content-type": "text/event-stream"})
|
|
121
|
+
elif path == "/api/orgs":
|
|
122
|
+
data = {"id": "org1", "name": body["name"], "slug": body["slug"], "plan": "free"}
|
|
123
|
+
elif path == "/api/users/user1":
|
|
124
|
+
data = {"deleted": True}
|
|
125
|
+
elif path == "/api/settings/otel.ingest_enabled":
|
|
126
|
+
data = {"key": "otel.ingest_enabled", **body}
|
|
127
|
+
elif method == "DELETE":
|
|
128
|
+
data = {"deleted": True}
|
|
129
|
+
elif method in ("POST", "PUT"):
|
|
130
|
+
if path == "/api/dashboards":
|
|
131
|
+
data = {**DASHBOARD, **body}
|
|
132
|
+
elif path.endswith("/tiles"):
|
|
133
|
+
data = {**TILE, **body}
|
|
134
|
+
elif path == "/api/permission-sets":
|
|
135
|
+
data = {**ROLE, **body}
|
|
136
|
+
elif path == "/api/audits/contracts":
|
|
137
|
+
data = {**CONTRACT, **body}
|
|
138
|
+
elif path == "/api/settings/model-context-windows":
|
|
139
|
+
data = body
|
|
140
|
+
elif path == "/api/alerts/alert1/issues":
|
|
141
|
+
data = {"id": "issue1", "state": "open", **body}
|
|
142
|
+
assert data is not None, f"missing realistic fake payload for {method} {path}"
|
|
143
|
+
return httpx.Response(200, json=data)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
CASES = [
|
|
147
|
+
(["evaluators", "list"], "tool_usage"),
|
|
148
|
+
(["evaluators", "show", "tool_usage@1"], "support-bot"),
|
|
149
|
+
(["evaluators", "create", "--key", "tool_usage", "--name", "Tool usage", "--code", "{code}"], "code"),
|
|
150
|
+
(["evaluators", "create", "--key", "resolution", "--name", "Resolution", "--judge",
|
|
151
|
+
"--criteria", "Resolved the request", "--step", "Assess relevance", "--threshold", "0.7"], "LLM judge"),
|
|
152
|
+
(["evaluators", "create", "--key", "frustration", "--name", "Frustration", "--jev", "noul",
|
|
153
|
+
"--instructions", "Read user messages", "--true", "Frustrated", "--false", "Calm"], "Jev noul"),
|
|
154
|
+
(["evaluators", "create", "--key", "quality", "--name", "Quality", "--jev", "score",
|
|
155
|
+
"--instructions", "Grade answer", "--level", "Wrong", "--level", "Partial", "--level", "Correct"], "level 3"),
|
|
156
|
+
(["evaluators", "deploy", "tool_usage", "--code", "{code}", "--dry-run-on", "run-check"], "disabled v1"),
|
|
157
|
+
(["evaluators", "draft", "Measure tools", "--out", "{draft}"], "create --file"),
|
|
158
|
+
(["evaluators", "dry-run", "--code", "{code}", "--session", "run-check"], "Seven tool calls"),
|
|
159
|
+
(["evaluators", "enable", "tool_usage@1"], "● enabled"),
|
|
160
|
+
(["evaluators", "disable", "tool_usage@1"], "○ disabled"),
|
|
161
|
+
(["evaluators", "label", "tool_usage@1", "--add", "qa"], "qa"),
|
|
162
|
+
(["evaluators", "instances"], "failproof-managed"),
|
|
163
|
+
(["evaluators", "schema"], "tool_name"),
|
|
164
|
+
(["evaluators", "jobs"], "queued"),
|
|
165
|
+
(["evaluators", "backfill", "--from", "2026-10-01T00:00:00Z", "--to", "2026-10-08T00:00:00Z", "--dry-run"], "12"),
|
|
166
|
+
(["dashboards", "list"], "Quality"),
|
|
167
|
+
(["dashboards", "show", "board1"], "Traffic"),
|
|
168
|
+
(["dashboards", "create", "--name", "Quality"], "Quality"),
|
|
169
|
+
(["dashboards", "update", "board1", "--name", "New quality"], "board1"),
|
|
170
|
+
(["dashboards", "delete", "board1"], "board1"),
|
|
171
|
+
(["dashboards", "tiles", "board1", "list"], "Traffic"),
|
|
172
|
+
(["dashboards", "tiles", "board1", "add", "--query", "query1", "--chart", "bar"], "query1"),
|
|
173
|
+
(["dashboards", "tiles", "board1", "update", "tile1", "--title", "New traffic"], "tile1"),
|
|
174
|
+
(["dashboards", "tiles", "board1", "remove", "tile1"], "tile1"),
|
|
175
|
+
(["dashboards", "tiles", "board1", "layout", "--file", "{layout}"], "tiles updated"),
|
|
176
|
+
(["roles", "list"], "reviewer"),
|
|
177
|
+
(["roles", "show", "reviewer"], "queries:run"),
|
|
178
|
+
(["roles", "create", "--name", "reviewer", "--permission", "events:read"], "events:read"),
|
|
179
|
+
(["roles", "update", "reviewer", "--add", "users:read"], "users:read"),
|
|
180
|
+
(["roles", "delete", "reviewer"], "members keep"),
|
|
181
|
+
(["sessions", "show", "run-check"], "Read customer"),
|
|
182
|
+
(["sessions", "export", "run-check", "--out", "{export}"], "file"),
|
|
183
|
+
(["sessions", "export", "run-check"], "run-check"),
|
|
184
|
+
(["sessions", "history", "run-check"], "tool_usage"),
|
|
185
|
+
(["sessions", "re-evaluate", "run-check"], "queued 1 evaluation for run-check"),
|
|
186
|
+
(["events", "latency"], "145"),
|
|
187
|
+
(["sentiment", "summary"], "frustration"),
|
|
188
|
+
(["sentiment", "messages"], "Please try again"),
|
|
189
|
+
(["sentiment", "sessions"], "run-check"),
|
|
190
|
+
(["sentiment", "session", "run-check"], "frustration"),
|
|
191
|
+
(["query", "compose", "Count events"], "SELECT"),
|
|
192
|
+
(["query", "compose", "Count events", "--run"], "support-bot"),
|
|
193
|
+
(["query", "export", "--sql", "SELECT 1", "--format", "csv"], "support-bot"),
|
|
194
|
+
(["query", "export", "--sql", "SELECT 1", "--format", "json"], "support-bot"),
|
|
195
|
+
(["query", "export", "--sql", "SELECT 1", "--out", "{export}"], "file"),
|
|
196
|
+
(["alerts", "recipients"], "ops@example.test"),
|
|
197
|
+
(["alerts", "issues", "alert1"], "Too many failures"),
|
|
198
|
+
(["alerts", "issues", "alert1", "--create", "--title", "Failure"], "Failure"),
|
|
199
|
+
(["audits", "contracts", "list"], "support-bot"),
|
|
200
|
+
(["audits", "contracts", "show", "contract1"], "Resolve requests"),
|
|
201
|
+
(["audits", "contracts", "create", "--agent-id", "support-bot", "--body", "Resolve requests"], "Resolve requests"),
|
|
202
|
+
(["audits", "contracts", "update", "contract1", "--body", "New purpose"], "support-bot"),
|
|
203
|
+
(["policies", "author", "Prevent force pushes"], "nothing published"),
|
|
204
|
+
(["policies", "backtest", "{policy}"], "Force push blocked"),
|
|
205
|
+
(["policies", "candidacy", "--issue", "issue1"], "can be blocked"),
|
|
206
|
+
(["agent", "brief"], "One session needs attention"),
|
|
207
|
+
(["orgs", "create", "--name", "Reliability"], "orgs switch"),
|
|
208
|
+
(["users", "delete", "user1"], "user1"),
|
|
209
|
+
(["users", "defaults"], "queries:run"),
|
|
210
|
+
(["plan"], "team_offer"),
|
|
211
|
+
(["plan", "claim", "team_offer"], "team_offer"),
|
|
212
|
+
(["otel", "status"], "support-service"),
|
|
213
|
+
(["otel", "enable"], "● enabled"),
|
|
214
|
+
(["otel", "disable"], "○ disabled"),
|
|
215
|
+
(["slack", "status"], "Reliability"),
|
|
216
|
+
(["slack", "disconnect"], "disconnected"),
|
|
217
|
+
(["settings", "context-windows", "list"], "64000"),
|
|
218
|
+
(["settings", "context-windows", "set", "provider/model", "64000"], "provider/model"),
|
|
219
|
+
(["settings", "context-windows", "delete", "provider/model"], "override removed"),
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@pytest.mark.parametrize("width", [80, 140])
|
|
224
|
+
@pytest.mark.parametrize("argv,expected", CASES, ids=lambda item: " ".join(item[:3]) if isinstance(item, list) else "")
|
|
225
|
+
def test_deliberate_human_command_views(logged_in, runner, monkeypatch, tmp_path, width, argv, expected):
|
|
226
|
+
monkeypatch.setenv("COLUMNS", str(width))
|
|
227
|
+
code, policy, layout = tmp_path / "source.py", tmp_path / "policy.mjs", tmp_path / "layout.json"
|
|
228
|
+
code.write_text(CODE)
|
|
229
|
+
policy.write_text("customPolicies.add({})")
|
|
230
|
+
layout.write_text(json.dumps({"tiles": [{"id": "tile1", "pos_x": 0, "pos_y": 0, "pos_w": 6, "pos_h": 4}]}))
|
|
231
|
+
paths = {"code": code, "policy": policy, "layout": layout, "draft": tmp_path / "draft.json", "export": tmp_path / "export.json"}
|
|
232
|
+
args = [v.format(**paths) for v in argv]
|
|
233
|
+
with respx.mock as mock:
|
|
234
|
+
mock.route().mock(side_effect=response)
|
|
235
|
+
result = runner.invoke(app, args)
|
|
236
|
+
assert result.exit_code == 0, result.output
|
|
237
|
+
assert expected in result.stdout, result.stdout
|
|
238
|
+
if argv[:2] != ["query", "export"] or "--out" in argv or "json" in argv:
|
|
239
|
+
assert result.stdout.lstrip().startswith("failproof ai"), result.stdout
|
|
240
|
+
assert "\nFIELD\n" not in result.stdout and "\nVALUE\n" not in result.stdout
|
|
241
|
+
for raw in ('"eval_key":', '"created":', '"permissions":', "dashboards []", "{'"):
|
|
242
|
+
assert raw not in result.stdout, result.stdout
|
|
243
|
+
if argv[:2] == ["evaluators", "deploy"]:
|
|
244
|
+
assert result.stdout.index("run-check · passed") < result.stdout.index("deployed tool_usage v2")
|
|
245
|
+
# Optional transcript capture for the manager, outside the repository.
|
|
246
|
+
if os.environ.get("FP_HUMAN_ARTIFACTS"):
|
|
247
|
+
directory = Path(os.environ["FP_HUMAN_ARTIFACTS"])
|
|
248
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
249
|
+
name = "-".join(argv[:3]).replace("/", "_").replace("{", "").replace("}", "")
|
|
250
|
+
with (directory / f"{width}-{name}.txt").open("a", encoding="utf-8") as dest:
|
|
251
|
+
dest.write("$ fp " + " ".join(argv) + "\n" + result.output + "\n")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@pytest.mark.parametrize("width", [80, 140])
|
|
255
|
+
@pytest.mark.parametrize("argv,path,next_step", [
|
|
256
|
+
(["dashboards", "list"], "/api/dashboards", "fp dashboards create --name"),
|
|
257
|
+
(["roles", "list"], "/api/permission-sets", "fp roles create"),
|
|
258
|
+
(["evaluators", "list"], "/api/evaluator-definitions", "fp evaluators create --help"),
|
|
259
|
+
(["sentiment", "messages"], "/api/events/sentiment/messages", "sentiment.enabled"),
|
|
260
|
+
(["settings", "context-windows", "list"], "/api/settings/model-context-windows", "context-windows set"),
|
|
261
|
+
])
|
|
262
|
+
def test_empty_views_give_next_step(logged_in, runner, monkeypatch, width, argv, path, next_step):
|
|
263
|
+
monkeypatch.setenv("COLUMNS", str(width))
|
|
264
|
+
data = {"dashboards": []} if argv[0] == "dashboards" else {"items": []} if argv[0] == "sentiment" else []
|
|
265
|
+
with respx.mock as mock:
|
|
266
|
+
mock.get(BASE + path).respond(200, json=data)
|
|
267
|
+
result = runner.invoke(app, argv)
|
|
268
|
+
assert result.exit_code == 0, result.output
|
|
269
|
+
assert "no " in result.stdout
|
|
270
|
+
assert next_step in result.stdout
|
|
271
|
+
assert "[]" not in result.stdout
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@pytest.mark.parametrize("width", [80, 140])
|
|
275
|
+
def test_roles_permissions_wrap_as_readable_text(logged_in, runner, monkeypatch, width):
|
|
276
|
+
monkeypatch.setenv("COLUMNS", str(width))
|
|
277
|
+
from fp_cli.permissions import ASSIGNABLE_PERMISSIONS
|
|
278
|
+
with respx.mock as mock:
|
|
279
|
+
mock.get(BASE + "/api/permission-sets/admin").respond(200, json={
|
|
280
|
+
"name": "admin", "is_builtin": True, "user_count": 1,
|
|
281
|
+
"permissions": ASSIGNABLE_PERMISSIONS,
|
|
282
|
+
})
|
|
283
|
+
result = runner.invoke(app, ["roles", "show", "admin"])
|
|
284
|
+
assert result.exit_code == 0
|
|
285
|
+
assert "permissions" in result.stdout
|
|
286
|
+
assert "[" not in result.stdout
|
|
287
|
+
assert "events:read" in result.stdout and "policies:write" in result.stdout
|