hap-cli 0.6.6__tar.gz → 0.6.7__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.
Files changed (73) hide show
  1. {hap_cli-0.6.6 → hap_cli-0.6.7}/PKG-INFO +1 -1
  2. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/__init__.py +1 -1
  3. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/contact_cmd.py +35 -21
  4. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/department_cmd.py +26 -23
  5. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/contact.py +40 -22
  6. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_core.py +35 -23
  7. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_integration_approval.py +1 -1
  8. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_integration_social.py +14 -7
  9. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli.egg-info/PKG-INFO +1 -1
  10. {hap_cli-0.6.6 → hap_cli-0.6.7}/setup.py +1 -1
  11. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/README.md +0 -0
  12. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/README_CN.md +0 -0
  13. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/__init__.py +0 -0
  14. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/app_cmd.py +0 -0
  15. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/auth_cmd.py +0 -0
  16. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/calendar_cmd.py +0 -0
  17. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/chat_cmd.py +0 -0
  18. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/instance_cmd.py +0 -0
  19. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/node_cmd.py +0 -0
  20. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/optionset_cmd.py +0 -0
  21. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/page_cmd.py +0 -0
  22. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/post_cmd.py +0 -0
  23. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/record_cmd.py +0 -0
  24. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/role_cmd.py +0 -0
  25. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/workflow_cmd.py +0 -0
  26. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/commands/worksheet_cmd.py +0 -0
  27. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/context.py +0 -0
  28. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/__init__.py +0 -0
  29. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/app.py +0 -0
  30. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/auth.py +0 -0
  31. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/calendar_mod.py +0 -0
  32. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/chat.py +0 -0
  33. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/department.py +0 -0
  34. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/flow_node.py +0 -0
  35. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/group.py +0 -0
  36. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/instance.py +0 -0
  37. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/optionset.py +0 -0
  38. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/page.py +0 -0
  39. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/post.py +0 -0
  40. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/record.py +0 -0
  41. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/response_crypto.py +0 -0
  42. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/role.py +0 -0
  43. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/session.py +0 -0
  44. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/token_crypto.py +0 -0
  45. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/workflow.py +0 -0
  46. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/core/worksheet.py +0 -0
  47. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/hap_cli.py +0 -0
  48. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/skills/SKILL.md +0 -0
  49. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/skills/__init__.py +0 -0
  50. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/__init__.py +0 -0
  51. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/conftest.py +0 -0
  52. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_full_e2e.py +0 -0
  53. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_integration.py +0 -0
  54. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_integration_calendar.py +0 -0
  55. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_integration_destructive.py +0 -0
  56. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_integration_misc.py +0 -0
  57. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_integration_post.py +0 -0
  58. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_integration_workflow.py +0 -0
  59. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
  60. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_org_id_cli.py +0 -0
  61. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_org_id_docs.py +0 -0
  62. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_parameter_conventions.py +0 -0
  63. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
  64. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/utils/__init__.py +0 -0
  65. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/utils/formatting.py +0 -0
  66. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/utils/options.py +0 -0
  67. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli/utils/parameter_mapping.py +0 -0
  68. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli.egg-info/SOURCES.txt +0 -0
  69. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli.egg-info/dependency_links.txt +0 -0
  70. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli.egg-info/entry_points.txt +0 -0
  71. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli.egg-info/requires.txt +0 -0
  72. {hap_cli-0.6.6 → hap_cli-0.6.7}/hap_cli.egg-info/top_level.txt +0 -0
  73. {hap_cli-0.6.6 → hap_cli-0.6.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hap-cli
3
- Version: 0.6.6
3
+ Version: 0.6.7
4
4
  Summary: CLI harness for MingDAO HAP - Enterprise no-code platform
5
5
  Author: hap-cli
6
6
  License: Apache-2.0
@@ -1,3 +1,3 @@
1
1
  """CLI harness for MingDAO HAP (hap-cli)."""
2
2
 
3
- __version__ = "0.6.6"
3
+ __version__ = "0.6.7"
@@ -51,14 +51,33 @@ def _simplify_user(u: dict[str, Any] | None) -> dict[str, Any] | None:
51
51
  v = u.get(src)
52
52
  if v:
53
53
  out[dst] = v
54
+ # Newer endpoints (User.GetContactUserList,
55
+ # User.GetProjectResignedUserList) nest the department under
56
+ # `departmentInfo`. Flatten it here so the rendered shape stays flat.
57
+ dept = u.get("departmentInfo") or {}
58
+ if isinstance(dept, dict):
59
+ if dept.get("departmentName") and not out.get("department"):
60
+ out["department"] = dept["departmentName"]
61
+ if dept.get("departmentId"):
62
+ out["department_id"] = dept["departmentId"]
54
63
  if u.get("accountStatus") and u["accountStatus"] != 0:
55
64
  out["status"] = u["accountStatus"]
56
65
  return out
57
66
 
58
67
 
59
68
  def _simplify_search_result(raw: dict[str, Any]) -> dict[str, Any]:
60
- users = [_simplify_user(u) for u in (raw.get("userResult") or [])]
61
- return {"users": [u for u in users if u]}
69
+ """Unified envelope for the two contact-search endpoints.
70
+
71
+ Both `User.GetContactUserList` (after extracting the `users`
72
+ sub-dict in the core layer) and `User.GetProjectResignedUserList`
73
+ hand us a ``{"list": [...], "allCount": N}`` shape — project it to
74
+ ``{"total": N, "users": [...]}`` so a single renderer handles both.
75
+ """
76
+ users = [_simplify_user(u) for u in (raw.get("list") or [])]
77
+ return {
78
+ "total": raw.get("allCount", 0),
79
+ "users": [u for u in users if u],
80
+ }
62
81
 
63
82
 
64
83
  def _simplify_friends_result(raw: dict[str, Any]) -> dict[str, Any]:
@@ -151,40 +170,35 @@ def _render_friend_requests(summary: dict[str, Any]) -> None:
151
170
  @contact.command("search")
152
171
  @click.argument("keywords")
153
172
  @org_id_option()
154
- @click.option(
155
- "--range", "range_", type=int, default=None,
156
- help="Server-defined range bucket (numeric)",
157
- )
158
173
  @click.option("--page-size", "-n", default=20, help="Items per page")
159
174
  @click.option("--page", "-p", default=1, help="Page number")
160
175
  @click.option(
161
- "--filter-other/--no-filter-other", "is_filter_other", default=None,
162
- help="Filter out third-party collaborators (friends + colleagues only)",
163
- )
164
- @click.option(
165
- "--take-count/--no-take-count", "take_total_count", default=None,
166
- help="Include total count in response",
176
+ "--resigned/--no-resigned",
177
+ "resigned",
178
+ default=False,
179
+ help="Search resigned members instead of active ones",
167
180
  )
168
181
  @pass_context
169
- def contact_search(
170
- ctx, keywords, org_id, range_, page_size, page, is_filter_other, take_total_count,
171
- ):
172
- """Search contacts by keyword within an organization."""
182
+ def contact_search(ctx, keywords, org_id, page_size, page, resigned):
183
+ """Search members of an organization by keyword.
184
+
185
+ Defaults to active members. Pass --resigned to search former
186
+ members. Uses the current organization when --org-id is omitted.
187
+ """
173
188
  try:
174
189
  session = ctx.get_session()
175
190
  pid = require_api_project_id(session, org_id)
176
- raw = contact_mod.search_contacts(
191
+ fn = contact_mod.search_resigned_users if resigned else contact_mod.search_contacts
192
+ raw = fn(
177
193
  session,
178
194
  keywords,
179
195
  project_id=pid,
180
- range_=range_,
181
196
  page_index=page,
182
197
  page_size=page_size,
183
- is_filter_other=is_filter_other,
184
- take_total_count=take_total_count,
185
198
  )
186
199
  summary = _simplify_search_result(raw)
187
- ctx.output(summary, lambda d: _render_users(d, "Matches"))
200
+ header = "Resigned matches" if resigned else "Matches"
201
+ ctx.output(summary, lambda d: _render_users(d, header))
188
202
  except Exception as e:
189
203
  ctx.handle_error(e)
190
204
 
@@ -34,8 +34,8 @@ def _simplify_department(d: dict[str, Any] | None) -> dict[str, Any] | None:
34
34
  "id": d.get("departmentId") or d.get("id"),
35
35
  "name": d.get("departmentName") or d.get("name"),
36
36
  }
37
- if d.get("userCount") is not None:
38
- out["user_count"] = d.get("userCount")
37
+ # userCount from these list/tree endpoints is unreliable (always 0) —
38
+ # use `department members` for a real count. Intentionally omitted.
39
39
  if d.get("haveSubDepartment") is not None:
40
40
  out["has_children"] = bool(d.get("haveSubDepartment"))
41
41
  if d.get("disabled"):
@@ -75,13 +75,23 @@ def _simplify_department_members(raw: dict[str, Any]) -> dict[str, Any]:
75
75
  }
76
76
 
77
77
 
78
+ def _simplify_dept_search_node(node: dict[str, Any] | None) -> dict[str, Any] | None:
79
+ # SearchDeptAndUsers returns departments as a recursive tree under
80
+ # the `subs` field (distinct from GetProjectSubDepartmentTree which
81
+ # uses `subDepartments`).
82
+ base = _simplify_department(node)
83
+ if base is None or not isinstance(node, dict):
84
+ return base
85
+ subs = node.get("subs") or []
86
+ children = [c for c in (_simplify_dept_search_node(s) for s in subs) if c]
87
+ if children:
88
+ base["children"] = children
89
+ return base
90
+
91
+
78
92
  def _simplify_dept_search(raw: dict[str, Any]) -> dict[str, Any]:
79
- users = [_simplify_user(u) for u in (raw.get("users") or [])]
80
- depts = [_simplify_department(d) for d in (raw.get("departments") or [])]
81
- return {
82
- "users": [u for u in users if u],
83
- "departments": [d for d in depts if d],
84
- }
93
+ depts = [_simplify_dept_search_node(d) for d in (raw.get("departments") or [])]
94
+ return {"departments": [d for d in depts if d]}
85
95
 
86
96
 
87
97
  # ── Human-readable renderers ───────────────────────────────────────────────
@@ -94,8 +104,6 @@ def _render_departments(depts: list[dict[str, Any]], header: str = "Departments"
94
104
  click.echo(f"{header} ({len(depts)}):")
95
105
  for d in depts:
96
106
  extras = []
97
- if d.get("user_count") is not None:
98
- extras.append(f"{d['user_count']} members")
99
107
  if d.get("has_children"):
100
108
  extras.append("has sub-departments")
101
109
  if d.get("disabled"):
@@ -111,8 +119,7 @@ def _render_tree(nodes: list[dict[str, Any]], depth: int = 0) -> None:
111
119
  return
112
120
  for n in nodes:
113
121
  indent = " " * depth
114
- count = f" ({n['user_count']} members)" if n.get("user_count") is not None else ""
115
- click.echo(f"{indent}• {n.get('name') or '?'}{count}")
122
+ click.echo(f"{indent} {n.get('name') or '?'}")
116
123
  click.echo(f"{indent} id: {n.get('id')}")
117
124
  if n.get("children"):
118
125
  _render_tree(n["children"], depth + 1)
@@ -123,8 +130,6 @@ def _render_department_info(info: dict[str, Any]) -> None:
123
130
  click.echo(f"ID: {info.get('id')}")
124
131
  if info.get("parent_name") or info.get("parent_id"):
125
132
  click.echo(f"Parent: {info.get('parent_name') or ''} ({info.get('parent_id') or ''})")
126
- if info.get("user_count") is not None:
127
- click.echo(f"Members: {info['user_count']}")
128
133
  if info.get("disabled"):
129
134
  click.echo("Status: disabled")
130
135
  chargers = info.get("charge_users") or []
@@ -136,14 +141,11 @@ def _render_department_info(info: dict[str, Any]) -> None:
136
141
 
137
142
  def _render_dept_search(summary: dict[str, Any]) -> None:
138
143
  depts = summary.get("departments") or []
139
- users = summary.get("users") or []
140
- if not depts and not users:
141
- click.echo("No matches.")
144
+ if not depts:
145
+ click.echo("No matching departments.")
142
146
  return
143
- if depts:
144
- _render_departments(depts, "Matching departments")
145
- if users:
146
- _render_users({"users": users}, "Matching users")
147
+ click.echo(f"Matching departments ({len(depts)}):")
148
+ _render_tree(depts)
147
149
 
148
150
 
149
151
  # ── Commands ───────────────────────────────────────────────────────────────
@@ -249,11 +251,12 @@ def department_members(
249
251
  "--include-disabled/--no-include-disabled",
250
252
  "include_disabled",
251
253
  default=None,
252
- help="Include disabled departments/users",
254
+ help="Include disabled departments",
253
255
  )
254
256
  @pass_context
255
257
  def department_search(ctx, keywords, org_id, include_disabled):
256
- """Search departments and users within the organization."""
258
+ """Search departments by keyword. Results render as a tree showing
259
+ each match with its sub-departments."""
257
260
  try:
258
261
  session = ctx.get_session()
259
262
  pid = require_api_project_id(session, org_id)
@@ -26,37 +26,55 @@ from hap_cli.core.session import Session
26
26
  def search_contacts(
27
27
  session: Session,
28
28
  keywords: str,
29
- project_id: str = "",
30
- range_: Optional[int] = None,
29
+ project_id: str,
31
30
  page_index: int = 1,
32
31
  page_size: int = 20,
33
- is_filter_other: Optional[bool] = None,
34
- take_total_count: Optional[bool] = None,
35
32
  ) -> dict[str, Any]:
36
- """Search address book contacts by keyword.
37
-
38
- Wraps `AddressBook.SearchAddressbookAndDepartment`
39
- (src/api/addressBook.js L73). Despite the endpoint name, the
40
- ``departmentResult`` field is always empty on current SaaS builds
41
- only ``userResult`` actually populates. Use
42
- :func:`hap_cli.core.department.search_dept_and_users` when you need
43
- department matches. `range_` is a numeric bucket; the CLI passes
44
- it through untouched because the server controls the enum.
33
+ """Search active members of an organization by keyword.
34
+
35
+ Wraps `User.GetContactUserList`. The server response carries both
36
+ ``oftenUsers`` (frequently-contacted shortcut) and ``users`` (full
37
+ matches); only the latter is useful for a general search, so this
38
+ function returns ``raw["users"]`` with shape
39
+ ``{"list": [...], "allCount": N}`` — the same envelope produced by
40
+ :func:`search_resigned_users` so both can share simplifiers.
41
+
42
+ ``dataRange`` is fixed at 2 (whole organization) because the
43
+ alternate buckets are not exposed via the CLI.
45
44
  """
46
45
  data: dict[str, Any] = {
47
46
  "keywords": keywords,
47
+ "projectId": project_id,
48
+ "dataRange": 2,
48
49
  "pageIndex": page_index,
49
50
  "pageSize": page_size,
50
51
  }
51
- if project_id:
52
- data["projectId"] = project_id
53
- if range_ is not None:
54
- data["range"] = range_
55
- if is_filter_other is not None:
56
- data["isFilterOther"] = is_filter_other
57
- if take_total_count is not None:
58
- data["takeTotalCount"] = take_total_count
59
- return session.api_call("AddressBook", "SearchAddressbookAndDepartment", data)
52
+ raw = session.api_call("User", "GetContactUserList", data) or {}
53
+ return raw.get("users") or {"list": [], "allCount": 0}
54
+
55
+
56
+ def search_resigned_users(
57
+ session: Session,
58
+ keywords: str,
59
+ project_id: str,
60
+ page_index: int = 1,
61
+ page_size: int = 20,
62
+ ) -> dict[str, Any]:
63
+ """Search resigned members of an organization by keyword.
64
+
65
+ Wraps `User.GetProjectResignedUserList`. Returns the raw
66
+ ``{"list": [...], "allCount": N}`` envelope — matches the shape
67
+ produced by :func:`search_contacts` on purpose so callers can feed
68
+ both through one simplifier.
69
+ """
70
+ data: dict[str, Any] = {
71
+ "projectId": project_id,
72
+ "pageIndex": page_index,
73
+ "pageSize": page_size,
74
+ }
75
+ if keywords:
76
+ data["keywords"] = keywords
77
+ return session.api_call("User", "GetProjectResignedUserList", data)
60
78
 
61
79
 
62
80
  def get_friends(
@@ -1621,44 +1621,56 @@ class TestPage:
1621
1621
  class TestContact:
1622
1622
  @patch("hap_cli.core.session.requests.post")
1623
1623
  def test_search_contacts_defaults(self, mock_post, mock_session):
1624
- """`AddressBook.SearchAddressbookAndDepartment` minimal call.
1625
-
1626
- Real fields per src/api/addressBook.js L73: keywords, projectId,
1627
- range, pageIndex, pageSize, isFilterOther, takeTotalCount.
1628
- """
1629
- mock_post.return_value = _mock_response([{"accountId": "u1", "fullname": "Alice"}])
1624
+ """`User.GetContactUserList` defaults + `users` sub-field extraction."""
1625
+ mock_post.return_value = _mock_response({
1626
+ "oftenUsers": {"list": [{"accountId": "skip"}]},
1627
+ "users": {"list": [{"accountId": "u1", "fullname": "Alice"}], "allCount": 1},
1628
+ })
1630
1629
  from hap_cli.core import contact as contact_mod
1631
1630
  result = contact_mod.search_contacts(mock_session, "Alice", project_id="proj1")
1632
- assert result == [{"accountId": "u1", "fullname": "Alice"}]
1631
+ # Only the `users` envelope is returned; `oftenUsers` is dropped.
1632
+ assert result == {"list": [{"accountId": "u1", "fullname": "Alice"}], "allCount": 1}
1633
1633
  args = mock_post.call_args
1634
- assert "AddressBook/SearchAddressbookAndDepartment" in args[0][0]
1634
+ assert "User/GetContactUserList" in args[0][0]
1635
1635
  body = args[1]["json"]
1636
1636
  assert body["keywords"] == "Alice"
1637
1637
  assert body["projectId"] == "proj1"
1638
- # Defaults
1638
+ assert body["dataRange"] == 2
1639
1639
  assert body["pageIndex"] == 1
1640
1640
  assert body["pageSize"] == 20
1641
1641
 
1642
1642
  @patch("hap_cli.core.session.requests.post")
1643
- def test_search_contacts_full_fields(self, mock_post, mock_session):
1644
- mock_post.return_value = _mock_response({"list": [], "allCount": 0})
1643
+ def test_search_contacts_empty_envelope(self, mock_post, mock_session):
1644
+ """Missing `users` sub-field degrades gracefully to an empty envelope."""
1645
+ mock_post.return_value = _mock_response({})
1645
1646
  from hap_cli.core import contact as contact_mod
1646
- contact_mod.search_contacts(
1647
- mock_session,
1648
- keywords="Zhang",
1649
- project_id="proj1",
1650
- range_=10,
1651
- page_index=2,
1652
- page_size=50,
1653
- is_filter_other=True,
1654
- take_total_count=True,
1647
+ result = contact_mod.search_contacts(
1648
+ mock_session, keywords="Zhang", project_id="proj1", page_index=2, page_size=50,
1655
1649
  )
1650
+ assert result == {"list": [], "allCount": 0}
1656
1651
  body = mock_post.call_args[1]["json"]
1657
- assert body["range"] == 10
1658
1652
  assert body["pageIndex"] == 2
1659
1653
  assert body["pageSize"] == 50
1660
- assert body["isFilterOther"] is True
1661
- assert body["takeTotalCount"] is True
1654
+
1655
+ @patch("hap_cli.core.session.requests.post")
1656
+ def test_search_resigned_users(self, mock_post, mock_session):
1657
+ """`User.GetProjectResignedUserList` — shape matches active search."""
1658
+ mock_post.return_value = _mock_response({
1659
+ "list": [{"accountId": "r1", "fullname": "Gone"}],
1660
+ "allCount": 1,
1661
+ })
1662
+ from hap_cli.core import contact as contact_mod
1663
+ result = contact_mod.search_resigned_users(
1664
+ mock_session, keywords="Gone", project_id="proj1", page_index=2, page_size=50,
1665
+ )
1666
+ assert result == {"list": [{"accountId": "r1", "fullname": "Gone"}], "allCount": 1}
1667
+ args = mock_post.call_args
1668
+ assert "User/GetProjectResignedUserList" in args[0][0]
1669
+ body = args[1]["json"]
1670
+ assert body["keywords"] == "Gone"
1671
+ assert body["projectId"] == "proj1"
1672
+ assert body["pageIndex"] == 2
1673
+ assert body["pageSize"] == 50
1662
1674
 
1663
1675
  @patch("hap_cli.core.session.requests.post")
1664
1676
  def test_get_friends_full_fields(self, mock_post, mock_session):
@@ -60,7 +60,7 @@ def _dummy_account_id(integration_session, project_id):
60
60
  res = contact.search_contacts(
61
61
  integration_session, project_id=project_id, keywords="a", page_size=1
62
62
  )
63
- users = (res or {}).get("userResult") or []
63
+ users = (res or {}).get("list") or []
64
64
  if not users:
65
65
  pytest.skip("project has no users matching 'a' — cannot run delegation tests")
66
66
  return users[0].get("accountId")
@@ -39,19 +39,26 @@ class TestChat:
39
39
  class TestContact:
40
40
 
41
41
  def test_01_search(self, integration_session, project_id):
42
- """Contact search returns ``{userResult}``. The endpoint is
43
- named ``SearchAddressbookAndDepartment`` and also emits an
44
- always-empty ``departmentResult`` alongside it — the CLI only
45
- surfaces the user matches, but both fields remain in the raw
46
- envelope, so we assert on ``userResult`` only."""
42
+ """`User.GetContactUserList` unified ``{list, allCount}`` envelope."""
47
43
  result = contact.search_contacts(
48
44
  integration_session,
49
45
  project_id=project_id,
50
46
  keywords="a",
51
47
  page_size=5,
52
48
  )
53
- assert_dict_like(result, ["userResult"])
54
- assert isinstance(result["userResult"], list)
49
+ assert_dict_like(result, ["list"])
50
+ assert isinstance(result["list"], list)
51
+
52
+ def test_01b_search_resigned(self, integration_session, project_id):
53
+ """`User.GetProjectResignedUserList` — same envelope as active search."""
54
+ result = contact.search_resigned_users(
55
+ integration_session,
56
+ project_id=project_id,
57
+ keywords="",
58
+ page_size=5,
59
+ )
60
+ assert_dict_like(result, ["list"])
61
+ assert isinstance(result["list"], list)
55
62
 
56
63
  def test_02_resolve(self, integration_session):
57
64
  """AddressBook.GetUserAddressbookByKeywords returns a paged dict."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hap-cli
3
- Version: 0.6.6
3
+ Version: 0.6.7
4
4
  Summary: CLI harness for MingDAO HAP - Enterprise no-code platform
5
5
  Author: hap-cli
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
4
4
 
5
5
  setup(
6
6
  name="hap-cli",
7
- version="0.6.6",
7
+ version="0.6.7",
8
8
  description="CLI harness for MingDAO HAP - Enterprise no-code platform",
9
9
  long_description=open("hap_cli/README.md").read(),
10
10
  long_description_content_type="text/markdown",
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