meshbook-cli 0.3.0__tar.gz → 0.4.0__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.
- {meshbook_cli-0.3.0 → meshbook_cli-0.4.0}/PKG-INFO +1 -1
- {meshbook_cli-0.3.0 → meshbook_cli-0.4.0}/mesh/cli.py +194 -1
- {meshbook_cli-0.3.0 → meshbook_cli-0.4.0}/pyproject.toml +1 -1
- {meshbook_cli-0.3.0 → meshbook_cli-0.4.0}/tests/test_cli_smoke.py +123 -0
- {meshbook_cli-0.3.0 → meshbook_cli-0.4.0}/.gitignore +0 -0
- {meshbook_cli-0.3.0 → meshbook_cli-0.4.0}/CHANGELOG.md +0 -0
- {meshbook_cli-0.3.0 → meshbook_cli-0.4.0}/LICENSE +0 -0
- {meshbook_cli-0.3.0 → meshbook_cli-0.4.0}/README.md +0 -0
- {meshbook_cli-0.3.0 → meshbook_cli-0.4.0}/docs/onboarding/task-template-non-human.md +0 -0
- {meshbook_cli-0.3.0 → meshbook_cli-0.4.0}/mesh/__init__.py +0 -0
- {meshbook_cli-0.3.0 → meshbook_cli-0.4.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshbook-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Small-model-friendly CLI for meshbook.org — built so non-humans of any size can run a CRM.
|
|
5
5
|
Project-URL: Homepage, https://meshbook.org
|
|
6
6
|
Project-URL: Documentation, https://meshbook.org/docs
|
|
@@ -49,7 +49,7 @@ import urllib.parse
|
|
|
49
49
|
import urllib.request
|
|
50
50
|
from pathlib import Path
|
|
51
51
|
|
|
52
|
-
VERSION = "0.
|
|
52
|
+
VERSION = "0.4.0"
|
|
53
53
|
DEFAULT_BASE = os.environ.get("MESHBOOK_BASE", "https://meshbook.org")
|
|
54
54
|
|
|
55
55
|
|
|
@@ -386,6 +386,10 @@ def cmd_chat_post(args, cfg: dict) -> int:
|
|
|
386
386
|
return 2
|
|
387
387
|
mesh_id = cfg["active_mesh_id"]
|
|
388
388
|
body = {"bodyMd": args.message}
|
|
389
|
+
if getattr(args, "reply_to", None):
|
|
390
|
+
# Entity-chat threading: the server accepts parentMessageId
|
|
391
|
+
# (also replyToId) on the mesh-chat POST.
|
|
392
|
+
body["parentMessageId"] = args.reply_to
|
|
389
393
|
payload = _api_call(
|
|
390
394
|
"POST", f"/api/entities/mesh/{mesh_id}/chat", cfg=cfg, body=body
|
|
391
395
|
)
|
|
@@ -1001,6 +1005,158 @@ def cmd_saved_views_list(args, cfg: dict) -> int:
|
|
|
1001
1005
|
return 0
|
|
1002
1006
|
|
|
1003
1007
|
|
|
1008
|
+
# ─── §31 batch 2b — members / task time-logs / saved-view create ───────
|
|
1009
|
+
#
|
|
1010
|
+
# Closes the remaining daily-driver gaps from §31.
|
|
1011
|
+
#
|
|
1012
|
+
# NOTE: `api-tokens` (list/mint/revoke) is deliberately NOT added. Those
|
|
1013
|
+
# endpoints (/api/me/api-tokens) refuse API-token callers by design — a
|
|
1014
|
+
# leaked bearer token must not be able to mint more tokens — and this CLI
|
|
1015
|
+
# authenticates with a bearer token, so the verbs would only ever 403.
|
|
1016
|
+
# Token management stays SPA-cookie-only. (Verified against
|
|
1017
|
+
# app/routers/api_tokens.py `_refuse_if_api_token_call` on all three routes.)
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def _self_user_id(cfg: dict) -> str | None:
|
|
1021
|
+
"""Resolve the signed-in user's own UUID via /api/me (for `leave`)."""
|
|
1022
|
+
try:
|
|
1023
|
+
me = _data(_api_call("GET", "/api/me", cfg=cfg))
|
|
1024
|
+
except APIError:
|
|
1025
|
+
return None
|
|
1026
|
+
user = me.get("user") if isinstance(me, dict) else None
|
|
1027
|
+
return (user or {}).get("id")
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def cmd_members_invite(args, cfg: dict) -> int:
|
|
1031
|
+
if not cfg.get("active_mesh_id"):
|
|
1032
|
+
print("No active mesh. Run: mesh meshes use NAME", file=sys.stderr)
|
|
1033
|
+
return 2
|
|
1034
|
+
mesh_id = cfg["active_mesh_id"]
|
|
1035
|
+
uname = args.user.lstrip("@")
|
|
1036
|
+
body: dict = {"username": uname}
|
|
1037
|
+
if args.role:
|
|
1038
|
+
body["role"] = args.role
|
|
1039
|
+
data = _data(_api_call("POST", f"/api/meshes/{mesh_id}/invite", cfg=cfg, body=body))
|
|
1040
|
+
if args.json:
|
|
1041
|
+
print(json.dumps(data, indent=2))
|
|
1042
|
+
return 0
|
|
1043
|
+
suffix = f" as {args.role}" if args.role else " (default invite role)"
|
|
1044
|
+
print(f"Invited @{uname}{suffix}")
|
|
1045
|
+
return 0
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def cmd_members_accept(args, cfg: dict) -> int:
|
|
1049
|
+
"""Accept (or --decline) a pending invitation to a mesh by its UUID.
|
|
1050
|
+
The mesh isn't active yet, so this takes the mesh id explicitly —
|
|
1051
|
+
list pending invites in the SPA or via /api/meshes/my-pending."""
|
|
1052
|
+
action = "decline" if args.decline else "accept"
|
|
1053
|
+
data = _data(_api_call(
|
|
1054
|
+
"POST", f"/api/meshes/{args.mesh}/respond", cfg=cfg, body={"action": action}
|
|
1055
|
+
))
|
|
1056
|
+
if args.json:
|
|
1057
|
+
print(json.dumps(data, indent=2))
|
|
1058
|
+
return 0
|
|
1059
|
+
print(f"{action.title()}ed invitation to mesh {args.mesh}")
|
|
1060
|
+
return 0
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def cmd_members_set_role(args, cfg: dict) -> int:
|
|
1064
|
+
if not cfg.get("active_mesh_id"):
|
|
1065
|
+
print("No active mesh. Run: mesh meshes use NAME", file=sys.stderr)
|
|
1066
|
+
return 2
|
|
1067
|
+
mesh_id = cfg["active_mesh_id"]
|
|
1068
|
+
user = _resolve_user(args.user, cfg)
|
|
1069
|
+
if not user:
|
|
1070
|
+
print(f"User {args.user!r} not found in active mesh.", file=sys.stderr)
|
|
1071
|
+
return 1
|
|
1072
|
+
data = _data(_api_call(
|
|
1073
|
+
"POST", f"/api/meshes/{mesh_id}/set-role",
|
|
1074
|
+
cfg=cfg, body={"userId": user["id"], "role": args.role},
|
|
1075
|
+
))
|
|
1076
|
+
if args.json:
|
|
1077
|
+
print(json.dumps(data, indent=2))
|
|
1078
|
+
return 0
|
|
1079
|
+
print(f"Set {args.user} → {args.role} in active mesh")
|
|
1080
|
+
return 0
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
def cmd_members_remove(args, cfg: dict) -> int:
|
|
1084
|
+
if not cfg.get("active_mesh_id"):
|
|
1085
|
+
print("No active mesh. Run: mesh meshes use NAME", file=sys.stderr)
|
|
1086
|
+
return 2
|
|
1087
|
+
mesh_id = cfg["active_mesh_id"]
|
|
1088
|
+
user = _resolve_user(args.user, cfg)
|
|
1089
|
+
if not user:
|
|
1090
|
+
print(f"User {args.user!r} not found in active mesh.", file=sys.stderr)
|
|
1091
|
+
return 1
|
|
1092
|
+
data = _data(_api_call(
|
|
1093
|
+
"POST", f"/api/meshes/{mesh_id}/remove", cfg=cfg, body={"userId": user["id"]}
|
|
1094
|
+
))
|
|
1095
|
+
if args.json:
|
|
1096
|
+
print(json.dumps(data, indent=2))
|
|
1097
|
+
return 0
|
|
1098
|
+
print(f"Removed {args.user} from active mesh")
|
|
1099
|
+
return 0
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def cmd_members_leave(args, cfg: dict) -> int:
|
|
1103
|
+
if not cfg.get("active_mesh_id"):
|
|
1104
|
+
print("No active mesh. Run: mesh meshes use NAME", file=sys.stderr)
|
|
1105
|
+
return 2
|
|
1106
|
+
mesh_id = cfg["active_mesh_id"]
|
|
1107
|
+
uid = _self_user_id(cfg)
|
|
1108
|
+
if not uid:
|
|
1109
|
+
print("Couldn't resolve your own user id from /api/me.", file=sys.stderr)
|
|
1110
|
+
return 1
|
|
1111
|
+
# §49: Account Managers can't leave a mesh attached to their own
|
|
1112
|
+
# subscription — the server returns 403; surface its message.
|
|
1113
|
+
data = _data(_api_call(
|
|
1114
|
+
"POST", f"/api/meshes/{mesh_id}/remove", cfg=cfg, body={"userId": uid}
|
|
1115
|
+
))
|
|
1116
|
+
if args.json:
|
|
1117
|
+
print(json.dumps(data, indent=2))
|
|
1118
|
+
return 0
|
|
1119
|
+
print(f"Left mesh {mesh_id}")
|
|
1120
|
+
return 0
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def cmd_tasks_log(args, cfg: dict) -> int:
|
|
1124
|
+
"""Log hours against a task (POST /api/task-time-logs). Date defaults
|
|
1125
|
+
to today (UTC). Hours must be > 0 and ≤ 24."""
|
|
1126
|
+
import datetime as _dt
|
|
1127
|
+
log_date = args.date or _dt.datetime.now(_dt.timezone.utc).date().isoformat()
|
|
1128
|
+
body: dict = {"taskId": args.task_id, "hours": args.hours, "loggedForDate": log_date}
|
|
1129
|
+
if args.note:
|
|
1130
|
+
body["note"] = args.note
|
|
1131
|
+
data = _data(_api_call("POST", "/api/task-time-logs", cfg=cfg, body=body))
|
|
1132
|
+
if args.json:
|
|
1133
|
+
print(json.dumps(data, indent=2))
|
|
1134
|
+
return 0
|
|
1135
|
+
print(f"Logged {args.hours}h on task {args.task_id} for {log_date}")
|
|
1136
|
+
return 0
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def cmd_saved_views_create(args, cfg: dict) -> int:
|
|
1140
|
+
"""Create a saved view. NOTE the create field is `module` (the entity
|
|
1141
|
+
grouping the view belongs to, e.g. leads / contacts / tasks), NOT
|
|
1142
|
+
`entityType` — verified against app/routers/saved_views.py SavedViewIn."""
|
|
1143
|
+
body: dict = {"module": args.module, "name": args.name}
|
|
1144
|
+
if args.filter:
|
|
1145
|
+
try:
|
|
1146
|
+
body["filterJson"] = json.loads(args.filter)
|
|
1147
|
+
except json.JSONDecodeError as e:
|
|
1148
|
+
print(f"--filter must be valid JSON: {e}", file=sys.stderr)
|
|
1149
|
+
return 2
|
|
1150
|
+
if args.shared:
|
|
1151
|
+
body["isShared"] = True
|
|
1152
|
+
data = _data(_api_call("POST", "/api/saved-views", cfg=cfg, body=body))
|
|
1153
|
+
if args.json:
|
|
1154
|
+
print(json.dumps(data, indent=2))
|
|
1155
|
+
return 0
|
|
1156
|
+
print(f"Created saved view '{args.name}' for {args.module} ({data.get('id')})")
|
|
1157
|
+
return 0
|
|
1158
|
+
|
|
1159
|
+
|
|
1004
1160
|
# ─── argparse plumbing ─────────────────────────────────────────────────
|
|
1005
1161
|
|
|
1006
1162
|
|
|
@@ -1054,6 +1210,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1054
1210
|
chs = ch.add_subparsers(dest="chat_cmd", required=True)
|
|
1055
1211
|
s = chs.add_parser("post", help="post a message in active mesh")
|
|
1056
1212
|
s.add_argument("message")
|
|
1213
|
+
s.add_argument("--reply-to", dest="reply_to",
|
|
1214
|
+
help="UUID of a message in this mesh to thread the reply under")
|
|
1057
1215
|
s.set_defaults(func=cmd_chat_post)
|
|
1058
1216
|
s = chs.add_parser("list", help="recent messages in active mesh")
|
|
1059
1217
|
s.add_argument("--limit", type=int, default=20)
|
|
@@ -1163,6 +1321,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1163
1321
|
s.add_argument("--status", default="Done",
|
|
1164
1322
|
help="terminal status to set (default Done; e.g. Cancelled)")
|
|
1165
1323
|
s.set_defaults(func=cmd_tasks_complete)
|
|
1324
|
+
s = sts.add_parser("log", help="log hours against a task (time tracking)")
|
|
1325
|
+
s.add_argument("task_id", help="task UUID")
|
|
1326
|
+
s.add_argument("--hours", type=float, required=True, help="hours worked (0 < h <= 24)")
|
|
1327
|
+
s.add_argument("--date", help="date worked (YYYY-MM-DD; default today UTC)")
|
|
1328
|
+
s.add_argument("--note", help="optional note")
|
|
1329
|
+
s.set_defaults(func=cmd_tasks_log)
|
|
1166
1330
|
|
|
1167
1331
|
# projects (§31 batch 2 — v0.3.0)
|
|
1168
1332
|
sp = sub.add_parser("projects", help="task-container projects")
|
|
@@ -1207,6 +1371,35 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1207
1371
|
s = ssvs.add_parser("list", help="list saved views")
|
|
1208
1372
|
s.add_argument("--entity", help="filter by entity type")
|
|
1209
1373
|
s.set_defaults(func=cmd_saved_views_list)
|
|
1374
|
+
s = ssvs.add_parser("create", help="create a saved view")
|
|
1375
|
+
s.add_argument("--module", required=True,
|
|
1376
|
+
help="entity grouping the view belongs to (leads/contacts/tasks/...)")
|
|
1377
|
+
s.add_argument("--name", required=True, help="view name")
|
|
1378
|
+
s.add_argument("--filter", help="filter as a JSON object string")
|
|
1379
|
+
s.add_argument("--shared", action="store_true", help="share with the whole mesh")
|
|
1380
|
+
s.set_defaults(func=cmd_saved_views_create)
|
|
1381
|
+
|
|
1382
|
+
# members (§31 batch 2b)
|
|
1383
|
+
smem = sub.add_parser("members", help="mesh membership (invite / accept / role / remove / leave)")
|
|
1384
|
+
smems = smem.add_subparsers(dest="members_cmd", required=True)
|
|
1385
|
+
s = smems.add_parser("invite", help="invite a user to the active mesh by username")
|
|
1386
|
+
s.add_argument("user", help="username (with or without '@')")
|
|
1387
|
+
s.add_argument("--role", choices=("admin", "member", "reader"),
|
|
1388
|
+
help="role (omit to use the mesh's default invite role)")
|
|
1389
|
+
s.set_defaults(func=cmd_members_invite)
|
|
1390
|
+
s = smems.add_parser("accept", help="accept (or --decline) a pending invitation by mesh UUID")
|
|
1391
|
+
s.add_argument("mesh", help="mesh UUID you were invited to")
|
|
1392
|
+
s.add_argument("--decline", action="store_true", help="decline instead of accept")
|
|
1393
|
+
s.set_defaults(func=cmd_members_accept)
|
|
1394
|
+
s = smems.add_parser("set-role", help="change a member's role in the active mesh")
|
|
1395
|
+
s.add_argument("user", help="username, displayName, or UUID")
|
|
1396
|
+
s.add_argument("role", choices=("member", "reader"), help="new role")
|
|
1397
|
+
s.set_defaults(func=cmd_members_set_role)
|
|
1398
|
+
s = smems.add_parser("remove", help="remove a member from the active mesh")
|
|
1399
|
+
s.add_argument("user", help="username, displayName, or UUID")
|
|
1400
|
+
s.set_defaults(func=cmd_members_remove)
|
|
1401
|
+
s = smems.add_parser("leave", help="leave the active mesh (you)")
|
|
1402
|
+
s.set_defaults(func=cmd_members_leave)
|
|
1210
1403
|
|
|
1211
1404
|
# notifications
|
|
1212
1405
|
s = sub.add_parser("notifications", help="recent notifications")
|
|
@@ -198,6 +198,129 @@ def test_leads_move_wire_shape(monkeypatch):
|
|
|
198
198
|
assert captured["body"] == {"stageId": "stage-2"}
|
|
199
199
|
|
|
200
200
|
|
|
201
|
+
# ─── §31 batch 2b — members / task time-logs / saved-view create ───────
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_help_includes_members(capsys):
|
|
205
|
+
parser = cli.build_parser()
|
|
206
|
+
with pytest.raises(SystemExit):
|
|
207
|
+
parser.parse_args(["--help"])
|
|
208
|
+
assert "members" in capsys.readouterr().out
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@pytest.mark.parametrize("group,subs", [
|
|
212
|
+
("members", ("invite", "accept", "set-role", "remove", "leave")),
|
|
213
|
+
("tasks", ("log",)),
|
|
214
|
+
("saved-views", ("create",)),
|
|
215
|
+
])
|
|
216
|
+
def test_batch2b_subcommands_present(capsys, group, subs):
|
|
217
|
+
parser = cli.build_parser()
|
|
218
|
+
with pytest.raises(SystemExit) as exc:
|
|
219
|
+
parser.parse_args([group, "--help"])
|
|
220
|
+
assert exc.value.code == 0
|
|
221
|
+
out = capsys.readouterr().out
|
|
222
|
+
for sub in subs:
|
|
223
|
+
assert sub in out, f"{group} missing subcommand {sub}"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_members_invite_wire_shape(monkeypatch):
|
|
227
|
+
captured = {}
|
|
228
|
+
|
|
229
|
+
def fake_api_call(method, path, *, cfg, body=None, **kw):
|
|
230
|
+
captured.update(method=method, path=path, body=body)
|
|
231
|
+
return {"data": {"invited": True}}
|
|
232
|
+
|
|
233
|
+
monkeypatch.setattr(cli, "_api_call", fake_api_call)
|
|
234
|
+
args = type("A", (), {"user": "@rook", "role": "member", "json": False})()
|
|
235
|
+
rc = cli.cmd_members_invite(args, {"active_mesh_id": "m1"})
|
|
236
|
+
assert rc == 0
|
|
237
|
+
assert captured["method"] == "POST"
|
|
238
|
+
assert captured["path"] == "/api/meshes/m1/invite"
|
|
239
|
+
assert captured["body"] == {"username": "rook", "role": "member"}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_members_invite_default_role_omitted(monkeypatch):
|
|
243
|
+
captured = {}
|
|
244
|
+
monkeypatch.setattr(cli, "_api_call",
|
|
245
|
+
lambda m, p, *, cfg, body=None, **kw: captured.update(body=body) or {"data": {}})
|
|
246
|
+
args = type("A", (), {"user": "rook", "role": None, "json": False})()
|
|
247
|
+
cli.cmd_members_invite(args, {"active_mesh_id": "m1"})
|
|
248
|
+
assert captured["body"] == {"username": "rook"} # no role key when default
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_members_set_role_wire_shape(monkeypatch):
|
|
252
|
+
captured = {}
|
|
253
|
+
|
|
254
|
+
def fake_api_call(method, path, *, cfg, body=None, **kw):
|
|
255
|
+
captured.update(method=method, path=path, body=body)
|
|
256
|
+
return {"data": {}}
|
|
257
|
+
|
|
258
|
+
monkeypatch.setattr(cli, "_api_call", fake_api_call)
|
|
259
|
+
monkeypatch.setattr(cli, "_resolve_user", lambda u, cfg: {"id": "u-9"})
|
|
260
|
+
args = type("A", (), {"user": "rook", "role": "reader", "json": False})()
|
|
261
|
+
rc = cli.cmd_members_set_role(args, {"active_mesh_id": "m1"})
|
|
262
|
+
assert rc == 0
|
|
263
|
+
assert captured["path"] == "/api/meshes/m1/set-role"
|
|
264
|
+
assert captured["body"] == {"userId": "u-9", "role": "reader"}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_members_accept_wire_shape(monkeypatch):
|
|
268
|
+
captured = {}
|
|
269
|
+
monkeypatch.setattr(cli, "_api_call",
|
|
270
|
+
lambda m, p, *, cfg, body=None, **kw: captured.update(path=p, body=body) or {"data": {}})
|
|
271
|
+
args = type("A", (), {"mesh": "mesh-7", "decline": False, "json": False})()
|
|
272
|
+
cli.cmd_members_accept(args, {})
|
|
273
|
+
assert captured["path"] == "/api/meshes/mesh-7/respond"
|
|
274
|
+
assert captured["body"] == {"action": "accept"}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_tasks_log_wire_shape_defaults_today(monkeypatch):
|
|
278
|
+
captured = {}
|
|
279
|
+
monkeypatch.setattr(cli, "_api_call",
|
|
280
|
+
lambda m, p, *, cfg, body=None, **kw: captured.update(method=m, path=p, body=body) or {"data": {}})
|
|
281
|
+
args = type("A", (), {"task_id": "t-1", "hours": 2.5, "date": None, "note": None, "json": False})()
|
|
282
|
+
rc = cli.cmd_tasks_log(args, {"active_mesh_id": "m1"})
|
|
283
|
+
assert rc == 0
|
|
284
|
+
assert captured["method"] == "POST"
|
|
285
|
+
assert captured["path"] == "/api/task-time-logs"
|
|
286
|
+
assert captured["body"]["taskId"] == "t-1"
|
|
287
|
+
assert captured["body"]["hours"] == 2.5
|
|
288
|
+
# date defaulted to an ISO YYYY-MM-DD string
|
|
289
|
+
assert len(captured["body"]["loggedForDate"]) == 10 and captured["body"]["loggedForDate"].count("-") == 2
|
|
290
|
+
assert "note" not in captured["body"]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_saved_views_create_uses_module_field(monkeypatch):
|
|
294
|
+
"""Regression guard: create body uses `module`, NOT `entityType`
|
|
295
|
+
(verified against SavedViewIn). A wrong key would 422 server-side."""
|
|
296
|
+
captured = {}
|
|
297
|
+
monkeypatch.setattr(cli, "_api_call",
|
|
298
|
+
lambda m, p, *, cfg, body=None, **kw: captured.update(path=p, body=body) or {"data": {"id": "sv-1"}})
|
|
299
|
+
args = type("A", (), {"module": "leads", "name": "Hot", "filter": '{"stageId":"x"}', "shared": True, "json": False})()
|
|
300
|
+
rc = cli.cmd_saved_views_create(args, {"active_mesh_id": "m1"})
|
|
301
|
+
assert rc == 0
|
|
302
|
+
assert captured["path"] == "/api/saved-views"
|
|
303
|
+
assert captured["body"] == {"module": "leads", "name": "Hot",
|
|
304
|
+
"filterJson": {"stageId": "x"}, "isShared": True}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_saved_views_create_bad_filter_json(monkeypatch):
|
|
308
|
+
monkeypatch.setattr(cli, "_api_call", lambda *a, **k: {"data": {}})
|
|
309
|
+
args = type("A", (), {"module": "leads", "name": "X", "filter": "{not json", "shared": False, "json": False})()
|
|
310
|
+
assert cli.cmd_saved_views_create(args, {}) == 2
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_chat_post_reply_to_threads(monkeypatch):
|
|
314
|
+
captured = {}
|
|
315
|
+
monkeypatch.setattr(cli, "_api_call",
|
|
316
|
+
lambda m, p, *, cfg, body=None, **kw: captured.update(path=p, body=body) or {"data": {"id": "msg-2"}})
|
|
317
|
+
args = type("A", (), {"message": "re: that", "reply_to": "msg-1", "json": False})()
|
|
318
|
+
rc = cli.cmd_chat_post(args, {"active_mesh_id": "m1"})
|
|
319
|
+
assert rc == 0
|
|
320
|
+
assert captured["path"] == "/api/entities/mesh/m1/chat"
|
|
321
|
+
assert captured["body"] == {"bodyMd": "re: that", "parentMessageId": "msg-1"}
|
|
322
|
+
|
|
323
|
+
|
|
201
324
|
def test_config_round_trip(tmp_path, monkeypatch):
|
|
202
325
|
"""Config writes + reads cleanly through load/save/reset."""
|
|
203
326
|
monkeypatch.setattr(cli, "CONFIG_DIR", tmp_path / ".meshbook")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|