conduct-cli 0.9.3__tar.gz → 0.9.5__tar.gz
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.
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/PKG-INFO +1 -1
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/pyproject.toml +1 -1
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/guard.py +71 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/main.py +4 -4
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli.egg-info/SOURCES.txt +1 -0
- conduct_cli-0.9.5/tests/test_guard_approvals.py +117 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_switch.py +30 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/README.md +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/setup.cfg +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/setup.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/guard_policy.md +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/hooks/__init__.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/hooks/base.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/hooks/posttooluse.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/hooks/precompact.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/hooks/pretooluse.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/hooks/session_parser.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/hooks/session_report_push.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/hooks/session_start.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/hooks/stop.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/log_util.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli/tool_groups.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_advisory_and_hook.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_api_client.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_bash_scan_target.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_blast_radius.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_cli_matrix.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_cmd_run.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_command_word_matcher.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_conductguard_mcp_stdio.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_drain_daemon_health.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_hook_dispatch.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_instruction_files.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_journal_drain.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_log_util.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_proxy_coverage.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_proxy_env.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_report_tool_coverage.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_signed_policy.py +0 -0
- {conduct_cli-0.9.3 → conduct_cli-0.9.5}/tests/test_token_lifecycle.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "conduct-cli"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.5"
|
|
8
8
|
description = "CLI for Conduct AI — secure, govern, install agents, manage projects, run tests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -2374,6 +2374,21 @@ def register_guard_parser(sub):
|
|
|
2374
2374
|
session_sub.add_parser("stop", help="End the current goal session")
|
|
2375
2375
|
p_session.set_defaults(func=cmd_guard_session)
|
|
2376
2376
|
|
|
2377
|
+
# conduct guard approvals list|approve|reject — HITL decide from CLI (#1140)
|
|
2378
|
+
approv_p = guard_sub.add_parser("approvals", help="List and decide Guard HITL approval requests")
|
|
2379
|
+
approv_sub = approv_p.add_subparsers(dest="approvals_command")
|
|
2380
|
+
list_p = approv_sub.add_parser("list", help="List approval requests")
|
|
2381
|
+
list_p.add_argument("--status", default="pending",
|
|
2382
|
+
choices=["pending", "approved", "rejected", "timed_out", "all"],
|
|
2383
|
+
help="Filter by status (default: pending)")
|
|
2384
|
+
list_p.add_argument("--limit", type=int, default=50, help="Max rows (default: 50)")
|
|
2385
|
+
approve_p = approv_sub.add_parser("approve", help="Approve a pending request by id")
|
|
2386
|
+
approve_p.add_argument("request_id", help="Approval request UUID")
|
|
2387
|
+
approve_p.add_argument("--reason", default=None, help="Optional decision reason")
|
|
2388
|
+
reject_p = approv_sub.add_parser("reject", help="Reject a pending request by id")
|
|
2389
|
+
reject_p.add_argument("request_id", help="Approval request UUID")
|
|
2390
|
+
reject_p.add_argument("--reason", default=None, help="Optional decision reason")
|
|
2391
|
+
|
|
2377
2392
|
return guard_p, guard_sub
|
|
2378
2393
|
|
|
2379
2394
|
|
|
@@ -3178,6 +3193,60 @@ def cmd_guard_simulate(args):
|
|
|
3178
3193
|
print("[simulation] identity : (none — token did not resolve to an AgentIdentity)")
|
|
3179
3194
|
|
|
3180
3195
|
|
|
3196
|
+
def cmd_guard_approvals(args, guard_p):
|
|
3197
|
+
"""List and decide Guard HITL approval requests (#1140)."""
|
|
3198
|
+
sub = getattr(args, "approvals_command", None)
|
|
3199
|
+
if sub not in ("list", "approve", "reject"):
|
|
3200
|
+
guard_p.print_help()
|
|
3201
|
+
sys.exit(1)
|
|
3202
|
+
|
|
3203
|
+
cfg = _require_guard_config()
|
|
3204
|
+
workspace_id = cfg.get("workspace_id")
|
|
3205
|
+
agent_token = cfg.get("agent_token", "")
|
|
3206
|
+
base_url = _api_url(cfg)
|
|
3207
|
+
|
|
3208
|
+
if sub == "list":
|
|
3209
|
+
url = f"{base_url}/guard/approvals?workspace_id={workspace_id}&status={args.status}&limit={args.limit}"
|
|
3210
|
+
resp = _req("GET", url, token=agent_token or None)
|
|
3211
|
+
items = resp.get("items", []) if isinstance(resp, dict) else []
|
|
3212
|
+
if not items:
|
|
3213
|
+
print(f"{GRAY}No approvals with status={args.status}.{RESET}")
|
|
3214
|
+
return
|
|
3215
|
+
id_w, req_w, rule_w, msg_w = 38, 26, 24, 40
|
|
3216
|
+
print()
|
|
3217
|
+
print(f"{BOLD}{'ID':<{id_w}} {'Requester':<{req_w}} {'Rule':<{rule_w}} {'Message':<{msg_w}} Status{RESET}")
|
|
3218
|
+
print("─" * (id_w + req_w + rule_w + msg_w + 12))
|
|
3219
|
+
colors = {"pending": YELLOW, "approved": GREEN, "rejected": RED, "timed_out": GRAY}
|
|
3220
|
+
for it in items:
|
|
3221
|
+
st = it.get("status", "?")
|
|
3222
|
+
print(
|
|
3223
|
+
f"{it.get('id',''):<{id_w}} "
|
|
3224
|
+
f"{(it.get('requester_email') or '—')[:req_w-1]:<{req_w}} "
|
|
3225
|
+
f"{(it.get('rule_id') or '—')[:rule_w-1]:<{rule_w}} "
|
|
3226
|
+
f"{(it.get('rule_message') or '—')[:msg_w-1]:<{msg_w}} "
|
|
3227
|
+
f"{colors.get(st, '')}{st}{RESET}"
|
|
3228
|
+
)
|
|
3229
|
+
return
|
|
3230
|
+
|
|
3231
|
+
# approve / reject share a body shape
|
|
3232
|
+
decision = "approved" if sub == "approve" else "rejected"
|
|
3233
|
+
body = {"decision": decision}
|
|
3234
|
+
if args.reason:
|
|
3235
|
+
body["reason"] = args.reason
|
|
3236
|
+
url = f"{base_url}/guard/approvals/{args.request_id}/decide?workspace_id={workspace_id}"
|
|
3237
|
+
resp = _req("POST", url, body=body, token=agent_token or None)
|
|
3238
|
+
req = resp.get("request") if isinstance(resp, dict) else None
|
|
3239
|
+
if not req:
|
|
3240
|
+
print(f"{RED}Decision failed — no response body.{RESET}")
|
|
3241
|
+
sys.exit(1)
|
|
3242
|
+
color = GREEN if decision == "approved" else RED
|
|
3243
|
+
print(f"{color}{decision.upper()}{RESET} — {req.get('rule_id','')} "
|
|
3244
|
+
f"by {req.get('decided_by_email','?')} "
|
|
3245
|
+
f"latency={req.get('latency_ms','?')}ms "
|
|
3246
|
+
f"run_resumed={resp.get('run_resumed')}")
|
|
3247
|
+
|
|
3248
|
+
|
|
3249
|
+
|
|
3181
3250
|
def dispatch_guard(args, guard_p):
|
|
3182
3251
|
"""Dispatch to the correct guard handler. Called from main()."""
|
|
3183
3252
|
guard_command = getattr(args, "guard_command", None)
|
|
@@ -3205,6 +3274,8 @@ def dispatch_guard(args, guard_p):
|
|
|
3205
3274
|
cmd_guard_session(args)
|
|
3206
3275
|
elif guard_command == "simulate":
|
|
3207
3276
|
cmd_guard_simulate(args)
|
|
3277
|
+
elif guard_command == "approvals":
|
|
3278
|
+
cmd_guard_approvals(args, guard_p)
|
|
3208
3279
|
else:
|
|
3209
3280
|
guard_p.print_help()
|
|
3210
3281
|
sys.exit(1)
|
|
@@ -343,7 +343,7 @@ def _report_tool_coverage() -> None:
|
|
|
343
343
|
"""Detect AI tools on this machine and POST coverage to Guard API. Silent on failure."""
|
|
344
344
|
try:
|
|
345
345
|
cfg = _load_config()
|
|
346
|
-
server = cfg.get("server"
|
|
346
|
+
server = (cfg.get("server") or cfg.get("api_url") or "").rstrip("/")
|
|
347
347
|
api_key = cfg.get("agent_token", "")
|
|
348
348
|
token = cfg.get("token", "")
|
|
349
349
|
email = cfg.get("email", "")
|
|
@@ -1354,7 +1354,7 @@ def _atomic_write(path: Path, data: dict) -> None:
|
|
|
1354
1354
|
|
|
1355
1355
|
def cmd_switch(args):
|
|
1356
1356
|
cfg = _load_config()
|
|
1357
|
-
server = cfg.get("server"
|
|
1357
|
+
server = (cfg.get("server") or cfg.get("api_url") or "").rstrip("/")
|
|
1358
1358
|
api_key = cfg.get("agent_token", "")
|
|
1359
1359
|
token = cfg.get("token", "")
|
|
1360
1360
|
|
|
@@ -1798,7 +1798,7 @@ def _render_table(rows: list[dict]) -> str:
|
|
|
1798
1798
|
|
|
1799
1799
|
def _fetch_runs(cfg: dict) -> list[dict]:
|
|
1800
1800
|
"""Fetch recent runs from GET /runs."""
|
|
1801
|
-
server = cfg.get("server"
|
|
1801
|
+
server = (cfg.get("server") or cfg.get("api_url") or "").rstrip("/")
|
|
1802
1802
|
api_key = cfg.get("agent_token", "")
|
|
1803
1803
|
ws_id = cfg.get("workspace", "")
|
|
1804
1804
|
if not all([server, api_key, ws_id]):
|
|
@@ -1814,7 +1814,7 @@ def _fetch_runs(cfg: dict) -> list[dict]:
|
|
|
1814
1814
|
|
|
1815
1815
|
def _fetch_guard_activity(cfg: dict, guard_cfg: dict) -> list[dict]:
|
|
1816
1816
|
"""Fetch recent Guard events grouped by developer."""
|
|
1817
|
-
server = cfg.get("server"
|
|
1817
|
+
server = (cfg.get("server") or cfg.get("api_url") or "").rstrip("/")
|
|
1818
1818
|
api_key = cfg.get("agent_token", "")
|
|
1819
1819
|
ws_id = cfg.get("workspace", "")
|
|
1820
1820
|
if not all([server, api_key, ws_id]):
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Tests for `conduct guard approvals list|approve|reject` CLI (#1140).
|
|
2
|
+
|
|
3
|
+
Mocks _req + _require_guard_config so no network. Verifies URL/body shape
|
|
4
|
+
for decide, output for list (populated + empty), and dispatcher routing.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest.mock import patch
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
HERE = Path(__file__).resolve()
|
|
16
|
+
CLI_PKG = HERE.parent.parent / "src"
|
|
17
|
+
if str(CLI_PKG) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(CLI_PKG))
|
|
19
|
+
|
|
20
|
+
_FAKE_CFG = {
|
|
21
|
+
"workspace_id": "ws-abc-123",
|
|
22
|
+
"agent_token": "cond_agt_test",
|
|
23
|
+
"api_url": "https://api.example.com",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(autouse=True)
|
|
28
|
+
def patch_config(monkeypatch):
|
|
29
|
+
import conduct_cli.guard as g
|
|
30
|
+
monkeypatch.setattr(g, "_require_guard_config", lambda: _FAKE_CFG)
|
|
31
|
+
monkeypatch.setattr(g, "_api_url", lambda cfg: cfg["api_url"])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _parse(*argv):
|
|
35
|
+
import conduct_cli.guard as g
|
|
36
|
+
p = argparse.ArgumentParser()
|
|
37
|
+
s = p.add_subparsers(dest="command")
|
|
38
|
+
guard_p, _ = g.register_guard_parser(s)
|
|
39
|
+
return p.parse_args(list(argv)), guard_p
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_parser_routes_list_approve_reject():
|
|
43
|
+
args, _ = _parse("guard", "approvals", "list", "--status", "approved", "--limit", "5")
|
|
44
|
+
assert args.approvals_command == "list"
|
|
45
|
+
assert args.status == "approved" and args.limit == 5
|
|
46
|
+
|
|
47
|
+
args, _ = _parse("guard", "approvals", "approve", "req-1", "--reason", "ok")
|
|
48
|
+
assert args.approvals_command == "approve" and args.request_id == "req-1" and args.reason == "ok"
|
|
49
|
+
|
|
50
|
+
args, _ = _parse("guard", "approvals", "reject", "req-2")
|
|
51
|
+
assert args.approvals_command == "reject" and args.reason is None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_list_renders_rows(capsys):
|
|
55
|
+
import conduct_cli.guard as g
|
|
56
|
+
resp = {"items": [
|
|
57
|
+
{"id":"req-1","requester_email":"a@x.io","rule_id":"r.one","rule_message":"m1","status":"pending"},
|
|
58
|
+
{"id":"req-2","requester_email":"b@x.io","rule_id":"r.two","rule_message":"m2","status":"approved"},
|
|
59
|
+
]}
|
|
60
|
+
args, guard_p = _parse("guard", "approvals", "list")
|
|
61
|
+
with patch.object(g, "_req", return_value=resp) as mock:
|
|
62
|
+
g.dispatch_guard(args, guard_p)
|
|
63
|
+
call_url = mock.call_args.args[1]
|
|
64
|
+
assert "/guard/approvals" in call_url and "status=pending" in call_url and "workspace_id=ws-abc-123" in call_url
|
|
65
|
+
out = capsys.readouterr().out
|
|
66
|
+
assert "req-1" in out and "req-2" in out
|
|
67
|
+
assert "pending" in out and "approved" in out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_list_empty_state(capsys):
|
|
71
|
+
import conduct_cli.guard as g
|
|
72
|
+
args, guard_p = _parse("guard", "approvals", "list", "--status", "all")
|
|
73
|
+
with patch.object(g, "_req", return_value={"items": []}):
|
|
74
|
+
g.dispatch_guard(args, guard_p)
|
|
75
|
+
out = capsys.readouterr().out
|
|
76
|
+
assert "No approvals" in out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_approve_sends_correct_body_and_url(capsys):
|
|
80
|
+
import conduct_cli.guard as g
|
|
81
|
+
decision_resp = {
|
|
82
|
+
"request": {"rule_id":"r.x","decided_by_email":"me@x.io","latency_ms":42,"status":"approved"},
|
|
83
|
+
"run_resumed": True,
|
|
84
|
+
}
|
|
85
|
+
args, guard_p = _parse("guard", "approvals", "approve", "req-9", "--reason", "looks ok")
|
|
86
|
+
with patch.object(g, "_req", return_value=decision_resp) as mock:
|
|
87
|
+
g.dispatch_guard(args, guard_p)
|
|
88
|
+
method, url = mock.call_args.args[0], mock.call_args.args[1]
|
|
89
|
+
body = mock.call_args.kwargs["body"]
|
|
90
|
+
assert method == "POST"
|
|
91
|
+
assert "/guard/approvals/req-9/decide" in url and "workspace_id=ws-abc-123" in url
|
|
92
|
+
assert body == {"decision": "approved", "reason": "looks ok"}
|
|
93
|
+
out = capsys.readouterr().out
|
|
94
|
+
assert "APPROVED" in out and "run_resumed=True" in out and "42ms" in out
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_reject_without_reason_omits_field(capsys):
|
|
98
|
+
import conduct_cli.guard as g
|
|
99
|
+
decision_resp = {
|
|
100
|
+
"request": {"rule_id":"r.x","decided_by_email":"me@x.io","latency_ms":10,"status":"rejected"},
|
|
101
|
+
"run_resumed": False,
|
|
102
|
+
}
|
|
103
|
+
args, guard_p = _parse("guard", "approvals", "reject", "req-9")
|
|
104
|
+
with patch.object(g, "_req", return_value=decision_resp) as mock:
|
|
105
|
+
g.dispatch_guard(args, guard_p)
|
|
106
|
+
body = mock.call_args.kwargs["body"]
|
|
107
|
+
assert body == {"decision": "rejected"} # no reason key when None
|
|
108
|
+
out = capsys.readouterr().out
|
|
109
|
+
assert "REJECTED" in out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_bare_approvals_prints_help(capsys):
|
|
113
|
+
import conduct_cli.guard as g
|
|
114
|
+
args, guard_p = _parse("guard", "approvals")
|
|
115
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
116
|
+
g.dispatch_guard(args, guard_p)
|
|
117
|
+
assert excinfo.value.code == 1
|
|
@@ -197,3 +197,33 @@ def test_whoami_prints_all_sections(tmp_path, capsys):
|
|
|
197
197
|
assert "sudhi@b2bsphere.com" in out
|
|
198
198
|
assert "3 rules" in out
|
|
199
199
|
assert "Booster" in out
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
# Regression: cmd_switch must accept `api_url` when `server` is absent.
|
|
204
|
+
# `conduct login` writes `api_url`; older cmd_switch only read `server`
|
|
205
|
+
# and falsely reported "Not logged in".
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
def test_switch_accepts_api_url_key(tmp_path, capsys):
|
|
209
|
+
"""conduct switch works when config was written by `conduct login` (api_url only)."""
|
|
210
|
+
from conduct_cli import main as m
|
|
211
|
+
|
|
212
|
+
cfg_path = tmp_path / "config.json"
|
|
213
|
+
cfg_path.write_text(json.dumps({
|
|
214
|
+
"api_url": "https://api.conductai.ai",
|
|
215
|
+
"agent_token": "cond_agt_testkey",
|
|
216
|
+
"workspace": "ef0a7e36-0000-0000-0000-000000000001",
|
|
217
|
+
}))
|
|
218
|
+
|
|
219
|
+
args = _make_args(workspace=None)
|
|
220
|
+
|
|
221
|
+
with (
|
|
222
|
+
patch.object(m, "CONFIG_PATH", cfg_path),
|
|
223
|
+
patch.object(m.api, "req", return_value=_fake_workspaces()),
|
|
224
|
+
):
|
|
225
|
+
m.cmd_switch(args)
|
|
226
|
+
|
|
227
|
+
out = capsys.readouterr().out
|
|
228
|
+
assert "Not logged in" not in out
|
|
229
|
+
assert "Engineering" in out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|