meshbook-cli 0.2.0__tar.gz → 0.3.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.2.0 → meshbook_cli-0.3.0}/PKG-INFO +1 -1
- {meshbook_cli-0.2.0 → meshbook_cli-0.3.0}/mesh/cli.py +274 -1
- {meshbook_cli-0.2.0 → meshbook_cli-0.3.0}/pyproject.toml +1 -1
- {meshbook_cli-0.2.0 → meshbook_cli-0.3.0}/tests/test_cli_smoke.py +88 -1
- {meshbook_cli-0.2.0 → meshbook_cli-0.3.0}/.gitignore +0 -0
- {meshbook_cli-0.2.0 → meshbook_cli-0.3.0}/CHANGELOG.md +0 -0
- {meshbook_cli-0.2.0 → meshbook_cli-0.3.0}/LICENSE +0 -0
- {meshbook_cli-0.2.0 → meshbook_cli-0.3.0}/README.md +0 -0
- {meshbook_cli-0.2.0 → meshbook_cli-0.3.0}/docs/onboarding/task-template-non-human.md +0 -0
- {meshbook_cli-0.2.0 → meshbook_cli-0.3.0}/mesh/__init__.py +0 -0
- {meshbook_cli-0.2.0 → meshbook_cli-0.3.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.3.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.3.0"
|
|
53
53
|
DEFAULT_BASE = os.environ.get("MESHBOOK_BASE", "https://meshbook.org")
|
|
54
54
|
|
|
55
55
|
|
|
@@ -165,6 +165,18 @@ def _data(payload: dict) -> object:
|
|
|
165
165
|
return payload
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
def _items(payload: object) -> list:
|
|
169
|
+
"""Normalise a list response through the envelope to a plain list.
|
|
170
|
+
|
|
171
|
+
Handles all three shapes the API emits: a bare list, {data: [...]},
|
|
172
|
+
and the ok_list paginated shape {data: {items: [...], total}}.
|
|
173
|
+
"""
|
|
174
|
+
data = payload.get("data", payload) if isinstance(payload, dict) else payload
|
|
175
|
+
if isinstance(data, dict) and "items" in data:
|
|
176
|
+
data = data["items"]
|
|
177
|
+
return data or []
|
|
178
|
+
|
|
179
|
+
|
|
168
180
|
# ─── command implementations ───────────────────────────────────────────
|
|
169
181
|
|
|
170
182
|
|
|
@@ -814,6 +826,181 @@ def cmd_notifications(args, cfg: dict) -> int:
|
|
|
814
826
|
return 0
|
|
815
827
|
|
|
816
828
|
|
|
829
|
+
# ─── §31 batch 2: CRM verbs (leads / tasks / projects / companies) ─────
|
|
830
|
+
#
|
|
831
|
+
# Batch 1 (v0.2.0) gave non-humans channels + DMs + reactions. Batch 2
|
|
832
|
+
# (v0.3.0) adds the daily-driver CRM verbs so the CLI is a full working
|
|
833
|
+
# client, not just a chat tool: read + create across the four core
|
|
834
|
+
# entities, plus lead stage-moves and task completion, plus read access
|
|
835
|
+
# to custom-field definitions and saved views. Same auth + envelope
|
|
836
|
+
# contract; every list uses _items() for envelope normalisation.
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def cmd_leads_list(args, cfg: dict) -> int:
|
|
840
|
+
params = {
|
|
841
|
+
"limit": args.limit, "pipelineId": args.pipeline,
|
|
842
|
+
"stageId": args.stage, "companyId": args.company,
|
|
843
|
+
}
|
|
844
|
+
items = _items(_api_call("GET", "/api/leads", cfg=cfg, params=params))
|
|
845
|
+
if args.json:
|
|
846
|
+
print(json.dumps(items, indent=2))
|
|
847
|
+
return 0
|
|
848
|
+
for ld in items:
|
|
849
|
+
val = ld.get("valueAmount")
|
|
850
|
+
money = f" {val} {ld.get('currency', '')}".rstrip() if val is not None else ""
|
|
851
|
+
stage = ld.get("stageName") or ld.get("stageId") or ""
|
|
852
|
+
print(f" {ld.get('title')}{money} [{stage}] {ld.get('id')}")
|
|
853
|
+
return 0
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def cmd_leads_create(args, cfg: dict) -> int:
|
|
857
|
+
body = {"title": args.title, "pipelineId": args.pipeline, "stageId": args.stage}
|
|
858
|
+
if args.value is not None:
|
|
859
|
+
body["valueAmount"] = args.value
|
|
860
|
+
if args.description:
|
|
861
|
+
body["description"] = args.description
|
|
862
|
+
data = _data(_api_call("POST", "/api/leads", cfg=cfg, body=body))
|
|
863
|
+
if args.json:
|
|
864
|
+
print(json.dumps(data, indent=2))
|
|
865
|
+
return 0
|
|
866
|
+
print(f"Created lead: {data.get('title')} ({data.get('id')})")
|
|
867
|
+
return 0
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def cmd_leads_move(args, cfg: dict) -> int:
|
|
871
|
+
data = _data(_api_call(
|
|
872
|
+
"POST", f"/api/leads/{args.lead_id}/move-stage",
|
|
873
|
+
cfg=cfg, body={"stageId": args.stage},
|
|
874
|
+
))
|
|
875
|
+
if args.json:
|
|
876
|
+
print(json.dumps(data, indent=2))
|
|
877
|
+
return 0
|
|
878
|
+
print(f"Moved lead {args.lead_id} → stage {args.stage}")
|
|
879
|
+
return 0
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def cmd_tasks_list(args, cfg: dict) -> int:
|
|
883
|
+
params = {
|
|
884
|
+
"limit": args.limit, "projectId": args.project,
|
|
885
|
+
"assigneeId": args.assignee, "status": args.status,
|
|
886
|
+
}
|
|
887
|
+
items = _items(_api_call("GET", "/api/tasks", cfg=cfg, params=params))
|
|
888
|
+
if args.json:
|
|
889
|
+
print(json.dumps(items, indent=2))
|
|
890
|
+
return 0
|
|
891
|
+
for tk in items:
|
|
892
|
+
print(f" [{tk.get('status', '?')}] {tk.get('title')} {tk.get('id')}")
|
|
893
|
+
return 0
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def cmd_tasks_create(args, cfg: dict) -> int:
|
|
897
|
+
body = {"projectId": args.project, "title": args.title}
|
|
898
|
+
if args.priority:
|
|
899
|
+
body["priority"] = args.priority
|
|
900
|
+
if args.description:
|
|
901
|
+
body["description"] = args.description
|
|
902
|
+
data = _data(_api_call("POST", "/api/tasks", cfg=cfg, body=body))
|
|
903
|
+
if args.json:
|
|
904
|
+
print(json.dumps(data, indent=2))
|
|
905
|
+
return 0
|
|
906
|
+
print(f"Created task: {data.get('title')} ({data.get('id')})")
|
|
907
|
+
return 0
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def cmd_tasks_complete(args, cfg: dict) -> int:
|
|
911
|
+
# Valid task statuses: NotStarted / InProgress / Blocked / Review /
|
|
912
|
+
# Done / Cancelled. "Done" is the completion terminal; --status lets
|
|
913
|
+
# a caller set a different terminal (e.g. Cancelled).
|
|
914
|
+
data = _data(_api_call(
|
|
915
|
+
"PATCH", f"/api/tasks/{args.task_id}",
|
|
916
|
+
cfg=cfg, body={"status": args.status},
|
|
917
|
+
))
|
|
918
|
+
if args.json:
|
|
919
|
+
print(json.dumps(data, indent=2))
|
|
920
|
+
return 0
|
|
921
|
+
print(f"Task {args.task_id} → {args.status}")
|
|
922
|
+
return 0
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def cmd_projects_list(args, cfg: dict) -> int:
|
|
926
|
+
params = {"limit": args.limit, "portfolioId": args.portfolio, "statusFilter": args.status}
|
|
927
|
+
items = _items(_api_call("GET", "/api/projects", cfg=cfg, params=params))
|
|
928
|
+
if args.json:
|
|
929
|
+
print(json.dumps(items, indent=2))
|
|
930
|
+
return 0
|
|
931
|
+
for pr in items:
|
|
932
|
+
code = f" ({pr.get('code')})" if pr.get("code") else ""
|
|
933
|
+
print(f" [{pr.get('status', '?')}] {pr.get('name')}{code} {pr.get('id')}")
|
|
934
|
+
return 0
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def cmd_projects_create(args, cfg: dict) -> int:
|
|
938
|
+
body = {"name": args.name}
|
|
939
|
+
if args.code:
|
|
940
|
+
body["code"] = args.code
|
|
941
|
+
if args.description:
|
|
942
|
+
body["description"] = args.description
|
|
943
|
+
if args.portfolio:
|
|
944
|
+
body["portfolioId"] = args.portfolio
|
|
945
|
+
data = _data(_api_call("POST", "/api/projects", cfg=cfg, body=body))
|
|
946
|
+
if args.json:
|
|
947
|
+
print(json.dumps(data, indent=2))
|
|
948
|
+
return 0
|
|
949
|
+
print(f"Created project: {data.get('name')} ({data.get('id')})")
|
|
950
|
+
return 0
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def cmd_companies_list(args, cfg: dict) -> int:
|
|
954
|
+
params = {"limit": args.limit, "search": args.search}
|
|
955
|
+
items = _items(_api_call("GET", "/api/companies", cfg=cfg, params=params))
|
|
956
|
+
if args.json:
|
|
957
|
+
print(json.dumps(items, indent=2))
|
|
958
|
+
return 0
|
|
959
|
+
for co in items:
|
|
960
|
+
ind = f" ({co.get('industry')})" if co.get("industry") else ""
|
|
961
|
+
print(f" {co.get('name')}{ind} {co.get('id')}")
|
|
962
|
+
return 0
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def cmd_companies_create(args, cfg: dict) -> int:
|
|
966
|
+
body = {"name": args.name}
|
|
967
|
+
for arg_name, wire in (("website", "website"), ("industry", "industry"),
|
|
968
|
+
("email", "email"), ("phone", "phone")):
|
|
969
|
+
val = getattr(args, arg_name)
|
|
970
|
+
if val:
|
|
971
|
+
body[wire] = val
|
|
972
|
+
data = _data(_api_call("POST", "/api/companies", cfg=cfg, body=body))
|
|
973
|
+
if args.json:
|
|
974
|
+
print(json.dumps(data, indent=2))
|
|
975
|
+
return 0
|
|
976
|
+
print(f"Created company: {data.get('name')} ({data.get('id')})")
|
|
977
|
+
return 0
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def cmd_custom_fields_list(args, cfg: dict) -> int:
|
|
981
|
+
params = {"entityType": args.entity}
|
|
982
|
+
items = _items(_api_call("GET", "/api/custom-fields", cfg=cfg, params=params))
|
|
983
|
+
if args.json:
|
|
984
|
+
print(json.dumps(items, indent=2))
|
|
985
|
+
return 0
|
|
986
|
+
for cf in items:
|
|
987
|
+
req = " *required" if cf.get("isRequired") else ""
|
|
988
|
+
print(f" {cf.get('entityType')}.{cf.get('fieldKey')} "
|
|
989
|
+
f"({cf.get('fieldType')}) — {cf.get('label')}{req} {cf.get('id')}")
|
|
990
|
+
return 0
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def cmd_saved_views_list(args, cfg: dict) -> int:
|
|
994
|
+
params = {"entityType": args.entity} if args.entity else None
|
|
995
|
+
items = _items(_api_call("GET", "/api/saved-views", cfg=cfg, params=params))
|
|
996
|
+
if args.json:
|
|
997
|
+
print(json.dumps(items, indent=2))
|
|
998
|
+
return 0
|
|
999
|
+
for sv in items:
|
|
1000
|
+
print(f" {sv.get('entityType')}: {sv.get('name')} {sv.get('id')}")
|
|
1001
|
+
return 0
|
|
1002
|
+
|
|
1003
|
+
|
|
817
1004
|
# ─── argparse plumbing ─────────────────────────────────────────────────
|
|
818
1005
|
|
|
819
1006
|
|
|
@@ -935,6 +1122,92 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
935
1122
|
s.add_argument("message", help="message body (markdown)")
|
|
936
1123
|
s.set_defaults(func=cmd_dm_send)
|
|
937
1124
|
|
|
1125
|
+
# leads (§31 batch 2 — v0.3.0)
|
|
1126
|
+
sl = sub.add_parser("leads", help="CRM leads")
|
|
1127
|
+
sls = sl.add_subparsers(dest="leads_cmd", required=True)
|
|
1128
|
+
s = sls.add_parser("list", help="list leads in active mesh")
|
|
1129
|
+
s.add_argument("--limit", type=int, default=50)
|
|
1130
|
+
s.add_argument("--pipeline", help="filter by pipeline UUID")
|
|
1131
|
+
s.add_argument("--stage", help="filter by stage UUID")
|
|
1132
|
+
s.add_argument("--company", help="filter by company UUID")
|
|
1133
|
+
s.set_defaults(func=cmd_leads_list)
|
|
1134
|
+
s = sls.add_parser("create", help="create a lead")
|
|
1135
|
+
s.add_argument("--title", required=True)
|
|
1136
|
+
s.add_argument("--pipeline", required=True, help="pipeline UUID")
|
|
1137
|
+
s.add_argument("--stage", required=True, help="stage UUID")
|
|
1138
|
+
s.add_argument("--value", type=float, help="value amount")
|
|
1139
|
+
s.add_argument("--description")
|
|
1140
|
+
s.set_defaults(func=cmd_leads_create)
|
|
1141
|
+
s = sls.add_parser("move", help="move a lead to a different pipeline stage")
|
|
1142
|
+
s.add_argument("lead_id", help="lead UUID")
|
|
1143
|
+
s.add_argument("stage", help="target stage UUID")
|
|
1144
|
+
s.set_defaults(func=cmd_leads_move)
|
|
1145
|
+
|
|
1146
|
+
# tasks (§31 batch 2 — v0.3.0)
|
|
1147
|
+
st = sub.add_parser("tasks", help="project tasks")
|
|
1148
|
+
sts = st.add_subparsers(dest="tasks_cmd", required=True)
|
|
1149
|
+
s = sts.add_parser("list", help="list tasks in active mesh")
|
|
1150
|
+
s.add_argument("--limit", type=int, default=50)
|
|
1151
|
+
s.add_argument("--project", help="filter by project UUID")
|
|
1152
|
+
s.add_argument("--assignee", help="filter by assignee UUID")
|
|
1153
|
+
s.add_argument("--status", help="filter by status (NotStarted/InProgress/Blocked/Review/Done/Cancelled)")
|
|
1154
|
+
s.set_defaults(func=cmd_tasks_list)
|
|
1155
|
+
s = sts.add_parser("create", help="create a task")
|
|
1156
|
+
s.add_argument("--project", required=True, help="project UUID")
|
|
1157
|
+
s.add_argument("--title", required=True)
|
|
1158
|
+
s.add_argument("--priority", help="Low/Normal/High/Urgent")
|
|
1159
|
+
s.add_argument("--description")
|
|
1160
|
+
s.set_defaults(func=cmd_tasks_create)
|
|
1161
|
+
s = sts.add_parser("complete", help="mark a task done (or another terminal status)")
|
|
1162
|
+
s.add_argument("task_id", help="task UUID")
|
|
1163
|
+
s.add_argument("--status", default="Done",
|
|
1164
|
+
help="terminal status to set (default Done; e.g. Cancelled)")
|
|
1165
|
+
s.set_defaults(func=cmd_tasks_complete)
|
|
1166
|
+
|
|
1167
|
+
# projects (§31 batch 2 — v0.3.0)
|
|
1168
|
+
sp = sub.add_parser("projects", help="task-container projects")
|
|
1169
|
+
sps = sp.add_subparsers(dest="projects_cmd", required=True)
|
|
1170
|
+
s = sps.add_parser("list", help="list projects in active mesh")
|
|
1171
|
+
s.add_argument("--limit", type=int, default=50)
|
|
1172
|
+
s.add_argument("--portfolio", help="filter by portfolio UUID")
|
|
1173
|
+
s.add_argument("--status", help="filter by status (Planning/Active/OnHold/Complete/Cancelled)")
|
|
1174
|
+
s.set_defaults(func=cmd_projects_list)
|
|
1175
|
+
s = sps.add_parser("create", help="create a project")
|
|
1176
|
+
s.add_argument("--name", required=True)
|
|
1177
|
+
s.add_argument("--code", help="short project code")
|
|
1178
|
+
s.add_argument("--description")
|
|
1179
|
+
s.add_argument("--portfolio", help="portfolio UUID")
|
|
1180
|
+
s.set_defaults(func=cmd_projects_create)
|
|
1181
|
+
|
|
1182
|
+
# companies (§31 batch 2 — v0.3.0)
|
|
1183
|
+
sco = sub.add_parser("companies", help="CRM companies")
|
|
1184
|
+
scos = sco.add_subparsers(dest="companies_cmd", required=True)
|
|
1185
|
+
s = scos.add_parser("list", help="list companies in active mesh")
|
|
1186
|
+
s.add_argument("--search", help="search term")
|
|
1187
|
+
s.add_argument("--limit", type=int, default=50)
|
|
1188
|
+
s.set_defaults(func=cmd_companies_list)
|
|
1189
|
+
s = scos.add_parser("create", help="create a company")
|
|
1190
|
+
s.add_argument("--name", required=True)
|
|
1191
|
+
s.add_argument("--website")
|
|
1192
|
+
s.add_argument("--industry")
|
|
1193
|
+
s.add_argument("--email")
|
|
1194
|
+
s.add_argument("--phone")
|
|
1195
|
+
s.set_defaults(func=cmd_companies_create)
|
|
1196
|
+
|
|
1197
|
+
# custom-fields (read) (§31 batch 2 — v0.3.0)
|
|
1198
|
+
scf = sub.add_parser("custom-fields", help="custom field definitions (read)")
|
|
1199
|
+
scfs = scf.add_subparsers(dest="custom_fields_cmd", required=True)
|
|
1200
|
+
s = scfs.add_parser("list", help="list custom field definitions")
|
|
1201
|
+
s.add_argument("--entity", help="filter by entity type (contact/company/lead/...)")
|
|
1202
|
+
s.set_defaults(func=cmd_custom_fields_list)
|
|
1203
|
+
|
|
1204
|
+
# saved-views (read) (§31 batch 2 — v0.3.0)
|
|
1205
|
+
ssv = sub.add_parser("saved-views", help="saved views (read)")
|
|
1206
|
+
ssvs = ssv.add_subparsers(dest="saved_views_cmd", required=True)
|
|
1207
|
+
s = ssvs.add_parser("list", help="list saved views")
|
|
1208
|
+
s.add_argument("--entity", help="filter by entity type")
|
|
1209
|
+
s.set_defaults(func=cmd_saved_views_list)
|
|
1210
|
+
|
|
938
1211
|
# notifications
|
|
939
1212
|
s = sub.add_parser("notifications", help="recent notifications")
|
|
940
1213
|
s.set_defaults(func=cmd_notifications)
|
|
@@ -30,7 +30,8 @@ def test_help_runs(capsys):
|
|
|
30
30
|
out = capsys.readouterr().out
|
|
31
31
|
for cmd in ("login", "logout", "whoami", "doctor",
|
|
32
32
|
"meshes", "contacts", "chat", "channels", "dm",
|
|
33
|
-
"
|
|
33
|
+
"leads", "tasks", "projects", "companies",
|
|
34
|
+
"custom-fields", "saved-views", "notifications"):
|
|
34
35
|
assert cmd in out
|
|
35
36
|
|
|
36
37
|
|
|
@@ -111,6 +112,92 @@ def test_resolve_channel_strips_hash(monkeypatch):
|
|
|
111
112
|
assert cli._resolve_channel("nope", {"active_mesh_id": "m"}) is None
|
|
112
113
|
|
|
113
114
|
|
|
115
|
+
# ─── §31 batch 2 (v0.3.0) — CRM verbs ──────────────────────────────────
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.mark.parametrize("group,subs", [
|
|
119
|
+
("leads", ("list", "create", "move")),
|
|
120
|
+
("tasks", ("list", "create", "complete")),
|
|
121
|
+
("projects", ("list", "create")),
|
|
122
|
+
("companies", ("list", "create")),
|
|
123
|
+
("custom-fields", ("list",)),
|
|
124
|
+
("saved-views", ("list",)),
|
|
125
|
+
])
|
|
126
|
+
def test_batch2_subcommands_present(capsys, group, subs):
|
|
127
|
+
parser = cli.build_parser()
|
|
128
|
+
with pytest.raises(SystemExit) as exc:
|
|
129
|
+
parser.parse_args([group, "--help"])
|
|
130
|
+
assert exc.value.code == 0
|
|
131
|
+
out = capsys.readouterr().out
|
|
132
|
+
for sub in subs:
|
|
133
|
+
assert sub in out, f"{group} missing subcommand {sub}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_items_envelope_shapes():
|
|
137
|
+
"""_items must normalise bare-list, {data:[...]}, and
|
|
138
|
+
{data:{items:[...]}} into a plain list."""
|
|
139
|
+
assert cli._items([1, 2, 3]) == [1, 2, 3]
|
|
140
|
+
assert cli._items({"data": [1, 2]}) == [1, 2]
|
|
141
|
+
assert cli._items({"data": {"items": [9], "total": 1}}) == [9]
|
|
142
|
+
assert cli._items({"data": None}) == []
|
|
143
|
+
assert cli._items(None) == []
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_leads_create_wire_shape(monkeypatch):
|
|
147
|
+
"""Lead create must send camelCase pipelineId/stageId + title."""
|
|
148
|
+
captured = {}
|
|
149
|
+
|
|
150
|
+
def fake_api_call(method, path, *, cfg, body=None, **kw):
|
|
151
|
+
captured.update(method=method, path=path, body=body)
|
|
152
|
+
return {"data": {"id": "lead-1", "title": "Big deal"}}
|
|
153
|
+
|
|
154
|
+
monkeypatch.setattr(cli, "_api_call", fake_api_call)
|
|
155
|
+
args = type("A", (), {
|
|
156
|
+
"title": "Big deal", "pipeline": "pid", "stage": "sid",
|
|
157
|
+
"value": 1000.0, "description": None, "json": False,
|
|
158
|
+
})()
|
|
159
|
+
rc = cli.cmd_leads_create(args, {"active_mesh_id": "m"})
|
|
160
|
+
assert rc == 0
|
|
161
|
+
assert captured["method"] == "POST"
|
|
162
|
+
assert captured["path"] == "/api/leads"
|
|
163
|
+
assert captured["body"] == {
|
|
164
|
+
"title": "Big deal", "pipelineId": "pid", "stageId": "sid",
|
|
165
|
+
"valueAmount": 1000.0,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_tasks_complete_wire_shape(monkeypatch):
|
|
170
|
+
"""tasks complete defaults to PATCH status=Done on /api/tasks/{id}."""
|
|
171
|
+
captured = {}
|
|
172
|
+
|
|
173
|
+
def fake_api_call(method, path, *, cfg, body=None, **kw):
|
|
174
|
+
captured.update(method=method, path=path, body=body)
|
|
175
|
+
return {"data": {"id": "t-1", "status": "Done"}}
|
|
176
|
+
|
|
177
|
+
monkeypatch.setattr(cli, "_api_call", fake_api_call)
|
|
178
|
+
args = type("A", (), {"task_id": "t-1", "status": "Done", "json": False})()
|
|
179
|
+
rc = cli.cmd_tasks_complete(args, {"active_mesh_id": "m"})
|
|
180
|
+
assert rc == 0
|
|
181
|
+
assert captured["method"] == "PATCH"
|
|
182
|
+
assert captured["path"] == "/api/tasks/t-1"
|
|
183
|
+
assert captured["body"] == {"status": "Done"}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_leads_move_wire_shape(monkeypatch):
|
|
187
|
+
captured = {}
|
|
188
|
+
|
|
189
|
+
def fake_api_call(method, path, *, cfg, body=None, **kw):
|
|
190
|
+
captured.update(method=method, path=path, body=body)
|
|
191
|
+
return {"data": {"id": "lead-1"}}
|
|
192
|
+
|
|
193
|
+
monkeypatch.setattr(cli, "_api_call", fake_api_call)
|
|
194
|
+
args = type("A", (), {"lead_id": "lead-1", "stage": "stage-2", "json": False})()
|
|
195
|
+
rc = cli.cmd_leads_move(args, {"active_mesh_id": "m"})
|
|
196
|
+
assert rc == 0
|
|
197
|
+
assert captured["path"] == "/api/leads/lead-1/move-stage"
|
|
198
|
+
assert captured["body"] == {"stageId": "stage-2"}
|
|
199
|
+
|
|
200
|
+
|
|
114
201
|
def test_config_round_trip(tmp_path, monkeypatch):
|
|
115
202
|
"""Config writes + reads cleanly through load/save/reset."""
|
|
116
203
|
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
|