conduct-cli 0.4.40__tar.gz → 0.4.41__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.4.40 → conduct_cli-0.4.41}/PKG-INFO +1 -1
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/pyproject.toml +1 -1
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/src/conduct_cli/main.py +181 -0
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/src/conduct_cli.egg-info/SOURCES.txt +2 -1
- conduct_cli-0.4.41/tests/test_switch.py +215 -0
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/README.md +0 -0
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/setup.cfg +0 -0
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/setup.py +0 -0
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.40 → conduct_cli-0.4.41}/src/conduct_cli.egg-info/top_level.txt +0 -0
|
@@ -1289,6 +1289,175 @@ def _build_state(issue: dict, repo_full_name: str) -> dict:
|
|
|
1289
1289
|
return {"github_issue": trigger, "_trigger": trigger}
|
|
1290
1290
|
|
|
1291
1291
|
|
|
1292
|
+
def _atomic_write(path: Path, data: dict) -> None:
|
|
1293
|
+
"""Write data to path atomically via a .tmp sibling."""
|
|
1294
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1295
|
+
tmp = path.with_suffix(".tmp")
|
|
1296
|
+
tmp.write_text(json.dumps(data, indent=2))
|
|
1297
|
+
os.replace(tmp, path)
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def cmd_switch(args):
|
|
1301
|
+
cfg = _load_config()
|
|
1302
|
+
server = cfg.get("server", "").rstrip("/")
|
|
1303
|
+
api_key = cfg.get("api_key", "")
|
|
1304
|
+
token = cfg.get("token", "")
|
|
1305
|
+
|
|
1306
|
+
if not server or (not api_key and not token):
|
|
1307
|
+
print(f"{RED}Not logged in. Run: conduct login --server <url> --api-key <key>{RESET}")
|
|
1308
|
+
sys.exit(1)
|
|
1309
|
+
|
|
1310
|
+
hdrs = {"Content-Type": "application/json"}
|
|
1311
|
+
if api_key:
|
|
1312
|
+
hdrs["X-Api-Key"] = api_key
|
|
1313
|
+
elif token:
|
|
1314
|
+
hdrs["Authorization"] = f"Bearer {token}"
|
|
1315
|
+
|
|
1316
|
+
workspaces = api.req("GET", f"{server}/projects", hdrs)
|
|
1317
|
+
|
|
1318
|
+
current_id = cfg.get("workspace", "")
|
|
1319
|
+
target = getattr(args, "workspace", None)
|
|
1320
|
+
|
|
1321
|
+
if not target:
|
|
1322
|
+
# List mode — print numbered list with current marked
|
|
1323
|
+
if not workspaces:
|
|
1324
|
+
print("No workspaces found.")
|
|
1325
|
+
return
|
|
1326
|
+
print(f"\n{BOLD}Workspaces:{RESET}")
|
|
1327
|
+
for i, ws in enumerate(workspaces, 1):
|
|
1328
|
+
marker = f"{GREEN}*{RESET}" if str(ws.get("id", "")) == str(current_id) else " "
|
|
1329
|
+
wid = str(ws.get("id", ""))
|
|
1330
|
+
print(f" {marker} {i}. {ws['name']:<35} {GRAY}{wid}{RESET}")
|
|
1331
|
+
print()
|
|
1332
|
+
return
|
|
1333
|
+
|
|
1334
|
+
# Match workspace: exact name (case-insensitive) first
|
|
1335
|
+
target_lower = target.lower()
|
|
1336
|
+
|
|
1337
|
+
exact = [ws for ws in workspaces if ws["name"].lower() == target_lower]
|
|
1338
|
+
if not exact:
|
|
1339
|
+
# Partial name match
|
|
1340
|
+
partial = [ws for ws in workspaces if target_lower in ws["name"].lower()]
|
|
1341
|
+
if not partial:
|
|
1342
|
+
# UUID prefix match
|
|
1343
|
+
partial = [ws for ws in workspaces if str(ws.get("id", "")).startswith(target)]
|
|
1344
|
+
candidates = partial
|
|
1345
|
+
else:
|
|
1346
|
+
candidates = exact
|
|
1347
|
+
|
|
1348
|
+
if len(candidates) > 1:
|
|
1349
|
+
print(f"{YELLOW}Ambiguous — multiple matches for '{target}':{RESET}")
|
|
1350
|
+
for ws in candidates:
|
|
1351
|
+
print(f" {ws['name']} {GRAY}({ws['id']}){RESET}")
|
|
1352
|
+
print("Be more specific.")
|
|
1353
|
+
sys.exit(1)
|
|
1354
|
+
|
|
1355
|
+
if not candidates:
|
|
1356
|
+
print(f"{RED}No workspace matching '{target}' found. Available:{RESET}")
|
|
1357
|
+
for ws in workspaces:
|
|
1358
|
+
print(f" {ws['name']} {GRAY}({ws['id']}){RESET}")
|
|
1359
|
+
sys.exit(1)
|
|
1360
|
+
|
|
1361
|
+
chosen = candidates[0]
|
|
1362
|
+
new_id = str(chosen["id"])
|
|
1363
|
+
new_name = chosen["name"]
|
|
1364
|
+
|
|
1365
|
+
# Update ~/.conduct/config.json atomically
|
|
1366
|
+
cfg["workspace"] = new_id
|
|
1367
|
+
_atomic_write(CONFIG_PATH, cfg)
|
|
1368
|
+
|
|
1369
|
+
# Update ~/.conductguard/config.json atomically if it exists
|
|
1370
|
+
guard_cfg_path = Path.home() / ".conductguard" / "config.json"
|
|
1371
|
+
if guard_cfg_path.exists():
|
|
1372
|
+
try:
|
|
1373
|
+
guard_cfg = json.loads(guard_cfg_path.read_text())
|
|
1374
|
+
guard_cfg["workspace_id"] = new_id
|
|
1375
|
+
_atomic_write(guard_cfg_path, guard_cfg)
|
|
1376
|
+
except Exception:
|
|
1377
|
+
pass
|
|
1378
|
+
|
|
1379
|
+
# Re-sync Guard policies for the new workspace
|
|
1380
|
+
try:
|
|
1381
|
+
policy = _guard._req(
|
|
1382
|
+
"GET",
|
|
1383
|
+
f"{server}/guard/policies/sync?workspace_id={new_id}",
|
|
1384
|
+
api_key=api_key,
|
|
1385
|
+
)
|
|
1386
|
+
_guard._save_policy(policy)
|
|
1387
|
+
rule_count = len(policy.get("rules", []))
|
|
1388
|
+
print(f" {GRAY}Guard policies synced: {rule_count} rule(s){RESET}")
|
|
1389
|
+
except SystemExit:
|
|
1390
|
+
pass # Guard not configured for this workspace — skip silently
|
|
1391
|
+
except Exception:
|
|
1392
|
+
pass
|
|
1393
|
+
|
|
1394
|
+
print(f"{GREEN}✓ Switched to \"{new_name}\" ({new_id[:8]}){RESET}")
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
def cmd_whoami(args):
|
|
1398
|
+
cfg = _load_config()
|
|
1399
|
+
|
|
1400
|
+
workspace_id = cfg.get("workspace", "")
|
|
1401
|
+
server = cfg.get("server", "—")
|
|
1402
|
+
api_key = cfg.get("api_key", "")
|
|
1403
|
+
|
|
1404
|
+
# Try to resolve workspace name from /projects
|
|
1405
|
+
workspace_name = ""
|
|
1406
|
+
if workspace_id and server != "—" and api_key:
|
|
1407
|
+
try:
|
|
1408
|
+
hdrs = {"Content-Type": "application/json", "X-Api-Key": api_key}
|
|
1409
|
+
projects = api.req("GET", f"{server.rstrip('/')}/projects", hdrs)
|
|
1410
|
+
match = next((p for p in projects if str(p.get("id", "")) == str(workspace_id)), None)
|
|
1411
|
+
if match:
|
|
1412
|
+
workspace_name = match["name"]
|
|
1413
|
+
except Exception:
|
|
1414
|
+
pass
|
|
1415
|
+
|
|
1416
|
+
ws_display = workspace_name if workspace_name else workspace_id
|
|
1417
|
+
ws_id_hint = f" ({workspace_id[:8]})" if workspace_id else ""
|
|
1418
|
+
api_key_display = (api_key[:12] + "… (set)") if api_key else "not set"
|
|
1419
|
+
|
|
1420
|
+
print(f"\n{BOLD}Workspace:{RESET} {ws_display}{ws_id_hint}")
|
|
1421
|
+
print(f"{BOLD}Server:{RESET} {server}")
|
|
1422
|
+
print(f"{BOLD}API key:{RESET} {api_key_display}")
|
|
1423
|
+
|
|
1424
|
+
# Guard section
|
|
1425
|
+
guard_cfg_path = Path.home() / ".conductguard" / "config.json"
|
|
1426
|
+
policy_path = Path.home() / ".conductguard" / "policy.json"
|
|
1427
|
+
hook_path = Path.home() / ".conductguard" / "hook.py"
|
|
1428
|
+
|
|
1429
|
+
if guard_cfg_path.exists():
|
|
1430
|
+
try:
|
|
1431
|
+
gcfg = json.loads(guard_cfg_path.read_text())
|
|
1432
|
+
user_email = gcfg.get("user_email", "")
|
|
1433
|
+
rule_count = 0
|
|
1434
|
+
if policy_path.exists():
|
|
1435
|
+
try:
|
|
1436
|
+
rule_count = len(json.loads(policy_path.read_text()).get("rules", []))
|
|
1437
|
+
except Exception:
|
|
1438
|
+
pass
|
|
1439
|
+
hook_status = "hook installed" if hook_path.exists() else "hook missing"
|
|
1440
|
+
email_part = f" | member: {user_email}" if user_email else ""
|
|
1441
|
+
print(f"{BOLD}Guard:{RESET} {GREEN}✓ {hook_status}{RESET} | policy: {rule_count} rules{email_part}")
|
|
1442
|
+
except Exception:
|
|
1443
|
+
print(f"{BOLD}Guard:{RESET} {YELLOW}config unreadable{RESET}")
|
|
1444
|
+
else:
|
|
1445
|
+
print(f"{BOLD}Guard:{RESET} not configured")
|
|
1446
|
+
|
|
1447
|
+
# Booster section
|
|
1448
|
+
booster_paths = [
|
|
1449
|
+
Path.home() / ".booster" / "config.json",
|
|
1450
|
+
Path.home() / ".agent-booster" / "config.json",
|
|
1451
|
+
]
|
|
1452
|
+
booster_found = any(p.exists() for p in booster_paths)
|
|
1453
|
+
if booster_found:
|
|
1454
|
+
print(f"{BOLD}Booster:{RESET} {GREEN}✓ configured{RESET}")
|
|
1455
|
+
else:
|
|
1456
|
+
print(f"{BOLD}Booster:{RESET} not configured")
|
|
1457
|
+
|
|
1458
|
+
print()
|
|
1459
|
+
|
|
1460
|
+
|
|
1292
1461
|
def cmd_run(args):
|
|
1293
1462
|
server, workspace_id, api_key, token = _require_auth(args)
|
|
1294
1463
|
json_h = api.headers(workspace_id, token, "application/json", api_key)
|
|
@@ -1443,6 +1612,14 @@ def main():
|
|
|
1443
1612
|
run_p.add_argument("--input", action="append", metavar="key=value", help="Runtime input (repeatable)")
|
|
1444
1613
|
run_p.add_argument("--max-turns", dest="max_turns", type=int, metavar="N", help="Max agentic turns (default: auto)")
|
|
1445
1614
|
|
|
1615
|
+
# conduct switch [workspace]
|
|
1616
|
+
switch_p = sub.add_parser("switch", help="Switch active workspace (or list workspaces)")
|
|
1617
|
+
switch_p.add_argument("workspace", nargs="?", metavar="name_or_id",
|
|
1618
|
+
help="Workspace name or UUID prefix to switch to (omit to list)")
|
|
1619
|
+
|
|
1620
|
+
# conduct whoami
|
|
1621
|
+
sub.add_parser("whoami", help="Show current workspace, server, API key, and Guard/Booster status")
|
|
1622
|
+
|
|
1446
1623
|
# conduct guard
|
|
1447
1624
|
guard_p, _guard_sub = _guard.register_guard_parser(sub)
|
|
1448
1625
|
|
|
@@ -1495,6 +1672,10 @@ def main():
|
|
|
1495
1672
|
cmd_test(args)
|
|
1496
1673
|
elif args.command == "run":
|
|
1497
1674
|
cmd_run(args)
|
|
1675
|
+
elif args.command == "switch":
|
|
1676
|
+
cmd_switch(args)
|
|
1677
|
+
elif args.command == "whoami":
|
|
1678
|
+
cmd_whoami(args)
|
|
1498
1679
|
elif args.command == "guard":
|
|
1499
1680
|
_guard.dispatch_guard(args, guard_p)
|
|
1500
1681
|
elif args.command == "mcp":
|
|
@@ -12,4 +12,5 @@ src/conduct_cli.egg-info/SOURCES.txt
|
|
|
12
12
|
src/conduct_cli.egg-info/dependency_links.txt
|
|
13
13
|
src/conduct_cli.egg-info/entry_points.txt
|
|
14
14
|
src/conduct_cli.egg-info/requires.txt
|
|
15
|
-
src/conduct_cli.egg-info/top_level.txt
|
|
15
|
+
src/conduct_cli.egg-info/top_level.txt
|
|
16
|
+
tests/test_switch.py
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Tests for `conduct switch` and `conduct whoami` commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import types
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import MagicMock, patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Helpers
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
def _make_args(**kwargs):
|
|
17
|
+
ns = types.SimpleNamespace(**kwargs)
|
|
18
|
+
return ns
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _fake_workspaces():
|
|
22
|
+
return [
|
|
23
|
+
{"id": "ef0a7e36-0000-0000-0000-000000000001", "name": "Engineering", "owner_id": "u1", "workflow_count": 3},
|
|
24
|
+
{"id": "ab1b2c3d-0000-0000-0000-000000000002", "name": "Marketing", "owner_id": "u1", "workflow_count": 1},
|
|
25
|
+
{"id": "deadbeef-0000-0000-0000-000000000003", "name": "Eng Backup", "owner_id": "u1", "workflow_count": 0},
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# cmd_switch — list mode (no arg)
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
def test_switch_list_prints_workspaces(tmp_path, capsys):
|
|
34
|
+
"""conduct switch with no arg exits 0 and prints workspace list."""
|
|
35
|
+
from conduct_cli import main as m
|
|
36
|
+
|
|
37
|
+
config = {
|
|
38
|
+
"server": "https://api.conductai.ai",
|
|
39
|
+
"api_key": "cond_live_testkey",
|
|
40
|
+
"workspace": "ef0a7e36-0000-0000-0000-000000000001",
|
|
41
|
+
}
|
|
42
|
+
cfg_path = tmp_path / "config.json"
|
|
43
|
+
cfg_path.write_text(json.dumps(config))
|
|
44
|
+
|
|
45
|
+
args = _make_args(workspace=None)
|
|
46
|
+
|
|
47
|
+
with (
|
|
48
|
+
patch.object(m, "CONFIG_PATH", cfg_path),
|
|
49
|
+
patch.object(m.api, "req", return_value=_fake_workspaces()),
|
|
50
|
+
):
|
|
51
|
+
m.cmd_switch(args)
|
|
52
|
+
|
|
53
|
+
out = capsys.readouterr().out
|
|
54
|
+
assert "Engineering" in out
|
|
55
|
+
assert "Marketing" in out
|
|
56
|
+
assert "*" in out # current workspace marked
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# cmd_switch — exact match updates both config files
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def test_switch_exact_name_updates_configs(tmp_path, capsys):
|
|
64
|
+
"""conduct switch 'Marketing' updates ~/.conduct/config.json and guard config."""
|
|
65
|
+
from conduct_cli import main as m
|
|
66
|
+
from conduct_cli import guard as g
|
|
67
|
+
|
|
68
|
+
cfg_path = tmp_path / "conduct" / "config.json"
|
|
69
|
+
# Guard config lives at <home>/.conductguard/config.json; home is patched to tmp_path
|
|
70
|
+
guard_cfg_path = tmp_path / ".conductguard" / "config.json"
|
|
71
|
+
|
|
72
|
+
cfg_path.parent.mkdir(parents=True)
|
|
73
|
+
guard_cfg_path.parent.mkdir(parents=True)
|
|
74
|
+
|
|
75
|
+
cfg_path.write_text(json.dumps({
|
|
76
|
+
"server": "https://api.conductai.ai",
|
|
77
|
+
"api_key": "cond_live_testkey",
|
|
78
|
+
"workspace": "ef0a7e36-0000-0000-0000-000000000001",
|
|
79
|
+
}))
|
|
80
|
+
guard_cfg_path.write_text(json.dumps({
|
|
81
|
+
"workspace_id": "ef0a7e36-0000-0000-0000-000000000001",
|
|
82
|
+
"user_email": "dev@example.com",
|
|
83
|
+
}))
|
|
84
|
+
|
|
85
|
+
args = _make_args(workspace="Marketing")
|
|
86
|
+
|
|
87
|
+
fake_policy = {"version": "2", "rules": [{"rule_id": "r1", "action": "audit"}]}
|
|
88
|
+
|
|
89
|
+
with (
|
|
90
|
+
patch.object(m, "CONFIG_PATH", cfg_path),
|
|
91
|
+
patch("pathlib.Path.home", return_value=tmp_path),
|
|
92
|
+
patch.object(m.api, "req", return_value=_fake_workspaces()),
|
|
93
|
+
patch.object(g, "_req", return_value=fake_policy),
|
|
94
|
+
patch.object(g, "_save_policy") as mock_save_policy,
|
|
95
|
+
):
|
|
96
|
+
m.cmd_switch(args)
|
|
97
|
+
|
|
98
|
+
out = capsys.readouterr().out
|
|
99
|
+
assert "Marketing" in out
|
|
100
|
+
assert "ab1b2c3d" in out # first 8 chars of the new workspace id
|
|
101
|
+
|
|
102
|
+
updated_cfg = json.loads(cfg_path.read_text())
|
|
103
|
+
assert updated_cfg["workspace"] == "ab1b2c3d-0000-0000-0000-000000000002"
|
|
104
|
+
|
|
105
|
+
updated_guard = json.loads(guard_cfg_path.read_text())
|
|
106
|
+
assert updated_guard["workspace_id"] == "ab1b2c3d-0000-0000-0000-000000000002"
|
|
107
|
+
|
|
108
|
+
mock_save_policy.assert_called_once_with(fake_policy)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# cmd_switch — ambiguous partial match
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def test_switch_ambiguous_exits_1(tmp_path, capsys):
|
|
116
|
+
"""Partial match that hits multiple workspaces prints error and exits 1."""
|
|
117
|
+
from conduct_cli import main as m
|
|
118
|
+
|
|
119
|
+
cfg_path = tmp_path / "config.json"
|
|
120
|
+
cfg_path.write_text(json.dumps({
|
|
121
|
+
"server": "https://api.conductai.ai",
|
|
122
|
+
"api_key": "cond_live_testkey",
|
|
123
|
+
"workspace": "ef0a7e36-0000-0000-0000-000000000001",
|
|
124
|
+
}))
|
|
125
|
+
|
|
126
|
+
# "Eng" matches both "Engineering" and "Eng Backup"
|
|
127
|
+
args = _make_args(workspace="Eng")
|
|
128
|
+
|
|
129
|
+
with (
|
|
130
|
+
patch.object(m, "CONFIG_PATH", cfg_path),
|
|
131
|
+
patch.object(m.api, "req", return_value=_fake_workspaces()),
|
|
132
|
+
pytest.raises(SystemExit) as exc,
|
|
133
|
+
):
|
|
134
|
+
m.cmd_switch(args)
|
|
135
|
+
|
|
136
|
+
assert exc.value.code == 1
|
|
137
|
+
out = capsys.readouterr().out
|
|
138
|
+
assert "Ambiguous" in out or "ambiguous" in out.lower() or "more specific" in out
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# cmd_switch — no match exits 1
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def test_switch_no_match_exits_1(tmp_path, capsys):
|
|
146
|
+
"""conduct switch 'Nonexistent' exits 1 and lists available workspaces."""
|
|
147
|
+
from conduct_cli import main as m
|
|
148
|
+
|
|
149
|
+
cfg_path = tmp_path / "config.json"
|
|
150
|
+
cfg_path.write_text(json.dumps({
|
|
151
|
+
"server": "https://api.conductai.ai",
|
|
152
|
+
"api_key": "cond_live_testkey",
|
|
153
|
+
"workspace": "ef0a7e36-0000-0000-0000-000000000001",
|
|
154
|
+
}))
|
|
155
|
+
|
|
156
|
+
args = _make_args(workspace="Nonexistent")
|
|
157
|
+
|
|
158
|
+
with (
|
|
159
|
+
patch.object(m, "CONFIG_PATH", cfg_path),
|
|
160
|
+
patch.object(m.api, "req", return_value=_fake_workspaces()),
|
|
161
|
+
pytest.raises(SystemExit) as exc,
|
|
162
|
+
):
|
|
163
|
+
m.cmd_switch(args)
|
|
164
|
+
|
|
165
|
+
assert exc.value.code == 1
|
|
166
|
+
out = capsys.readouterr().out
|
|
167
|
+
assert "Engineering" in out # shows available list
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# cmd_whoami — basic output
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def test_whoami_prints_all_sections(tmp_path, capsys):
|
|
175
|
+
"""conduct whoami prints workspace, server, api_key, Guard, and Booster lines."""
|
|
176
|
+
from conduct_cli import main as m
|
|
177
|
+
|
|
178
|
+
cfg_path = tmp_path / "conduct" / "config.json"
|
|
179
|
+
cfg_path.parent.mkdir(parents=True)
|
|
180
|
+
cfg_path.write_text(json.dumps({
|
|
181
|
+
"server": "https://api.conductai.ai",
|
|
182
|
+
"api_key": "cond_live_88a4longkeyxxx",
|
|
183
|
+
"workspace": "ef0a7e36-0000-0000-0000-000000000001",
|
|
184
|
+
}))
|
|
185
|
+
|
|
186
|
+
guard_dir = tmp_path / ".conductguard"
|
|
187
|
+
guard_dir.mkdir()
|
|
188
|
+
(guard_dir / "config.json").write_text(json.dumps({
|
|
189
|
+
"workspace_id": "ef0a7e36-0000-0000-0000-000000000001",
|
|
190
|
+
"user_email": "sudhi@b2bsphere.com",
|
|
191
|
+
}))
|
|
192
|
+
(guard_dir / "policy.json").write_text(json.dumps({
|
|
193
|
+
"version": "1",
|
|
194
|
+
"rules": [{"rule_id": "r1"}, {"rule_id": "r2"}, {"rule_id": "r3"}],
|
|
195
|
+
}))
|
|
196
|
+
# No hook.py — hook_status should say "hook missing"
|
|
197
|
+
|
|
198
|
+
args = _make_args()
|
|
199
|
+
|
|
200
|
+
def fake_home():
|
|
201
|
+
return tmp_path
|
|
202
|
+
|
|
203
|
+
with (
|
|
204
|
+
patch.object(m, "CONFIG_PATH", cfg_path),
|
|
205
|
+
patch("pathlib.Path.home", return_value=tmp_path),
|
|
206
|
+
patch.object(m.api, "req", return_value=_fake_workspaces()),
|
|
207
|
+
):
|
|
208
|
+
m.cmd_whoami(args)
|
|
209
|
+
|
|
210
|
+
out = capsys.readouterr().out
|
|
211
|
+
assert "https://api.conductai.ai" in out
|
|
212
|
+
assert "cond_live_88" in out # first 12 chars of the api_key
|
|
213
|
+
assert "sudhi@b2bsphere.com" in out
|
|
214
|
+
assert "3 rules" in out
|
|
215
|
+
assert "Booster" 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
|