sophhub 0.4.21 → 0.4.22

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 (68) hide show
  1. package/package.json +1 -1
  2. package/skills/insurance-customer-policy/skill.json +12 -0
  3. package/skills/insurance-customer-policy/src/SKILL.md +121 -0
  4. package/skills/insurance-customer-policy/src/pyproject.toml +9 -0
  5. package/skills/insurance-customer-policy/src/scripts/cli.py +16 -0
  6. package/skills/insurance-customer-policy/src/scripts/cloud_insurance_full_test.py +785 -0
  7. package/skills/insurance-customer-policy/src/scripts/dashboard_all.py +205 -0
  8. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__init__.py +0 -0
  9. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__main__.py +4 -0
  10. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/cli.py +816 -0
  11. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/db.py +181 -0
  12. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/insurance_paths.py +184 -0
  13. package/skills/insurance-customer-policy/src/scripts/run_e2e_smoke.py +164 -0
  14. package/skills/insurance-customer-policy/src/scripts/test_cloud_zero_config.py +217 -0
  15. package/skills/insurance-customer-policy/src/scripts/test_dashboard_all.py +113 -0
  16. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__init__.py +0 -0
  17. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__main__.py +4 -0
  18. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/cli.py +816 -0
  19. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/db.py +181 -0
  20. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/insurance_paths.py +184 -0
  21. package/skills/insurance-product-analysis/skill.json +12 -0
  22. package/skills/insurance-product-analysis/src/SKILL.md +99 -0
  23. package/skills/insurance-product-analysis/src/pyproject.toml +9 -0
  24. package/skills/insurance-product-analysis/src/scripts/cli.py +16 -0
  25. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__init__.py +0 -0
  26. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__main__.py +4 -0
  27. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/cli.py +545 -0
  28. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/db.py +180 -0
  29. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/orchestrator.py +163 -0
  30. package/skills/insurance-product-analysis/src/scripts/run_e2e_smoke.py +202 -0
  31. package/skills/insurance-product-analysis/src/src/insurance_product_cli/__init__.py +0 -0
  32. package/skills/insurance-product-analysis/src/src/insurance_product_cli/__main__.py +4 -0
  33. package/skills/insurance-product-analysis/src/src/insurance_product_cli/cli.py +545 -0
  34. package/skills/insurance-product-analysis/src/src/insurance_product_cli/db.py +180 -0
  35. package/skills/insurance-product-analysis/src/src/insurance_product_cli/orchestrator.py +163 -0
  36. package/skills/insurance-sales-pipeline/skill.json +12 -0
  37. package/skills/insurance-sales-pipeline/src/SKILL.md +102 -0
  38. package/skills/insurance-sales-pipeline/src/pyproject.toml +9 -0
  39. package/skills/insurance-sales-pipeline/src/scripts/cli.py +16 -0
  40. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__init__.py +0 -0
  41. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__main__.py +4 -0
  42. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/cli.py +496 -0
  43. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/db.py +180 -0
  44. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/orchestrator.py +36 -0
  45. package/skills/insurance-sales-pipeline/src/scripts/run_e2e_smoke.py +208 -0
  46. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__init__.py +0 -0
  47. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__main__.py +4 -0
  48. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/cli.py +496 -0
  49. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/db.py +180 -0
  50. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/orchestrator.py +36 -0
  51. package/skills/insurance-schedule-renewal/skill.json +12 -0
  52. package/skills/insurance-schedule-renewal/src/SKILL.md +94 -0
  53. package/skills/insurance-schedule-renewal/src/pyproject.toml +9 -0
  54. package/skills/insurance-schedule-renewal/src/scripts/cli.py +16 -0
  55. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__init__.py +0 -0
  56. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__main__.py +4 -0
  57. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/cli.py +429 -0
  58. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/db.py +180 -0
  59. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/orchestrator.py +94 -0
  60. package/skills/insurance-schedule-renewal/src/scripts/run_e2e_smoke.py +218 -0
  61. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__init__.py +0 -0
  62. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__main__.py +4 -0
  63. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/cli.py +429 -0
  64. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/db.py +180 -0
  65. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/orchestrator.py +94 -0
  66. package/skills/insurance-shared-data/skill.json +20 -0
  67. package/skills/insurance-shared-data/src/SKILL.md +33 -0
  68. package/skills/insurance-shared-data/src/scripts/cloud_insurance_super_test.py +246 -0
@@ -0,0 +1,785 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """保险小老板 4-Skill 云端「一键全覆盖」集成测试。
4
+
5
+ 把你看到的整个文件保存到云端任意路径(例如 /tmp/cloud_insurance_full_test.py),然后执行::
6
+
7
+ python3 /tmp/cloud_insurance_full_test.py
8
+
9
+ 或在仓库内::
10
+
11
+ python3 /app/skills/insurance-customer-policy/src/scripts/cloud_insurance_full_test.py
12
+
13
+ 特性
14
+ ----
15
+ - **自动探测** skills 根目录:``INSURANCE_SKILLS_ROOT`` / ``/app/skills`` / 当前目录下的 ``skills`` 等。
16
+ - **优先使用** 各 skill 的 ``src/scripts/cli.py`` wrapper;若旧镜像没有 wrapper,则自动回退到
17
+ ``PYTHONPATH=<pkg> python -m insurance_*_cli``。
18
+ - **隔离数据库**:默认把所有 SQLite 写到 ``/tmp/insurance_cloud_test_<pid>/``,不污染业务数据
19
+ (也可用环境变量 ``INSURANCE_CLOUD_TEST_DIR`` 指定目录)。
20
+ - **覆盖** insurance-customer-policy / insurance-product-analysis /
21
+ insurance-schedule-renewal / insurance-sales-pipeline 的**全部子命令**,
22
+ 以及 ``dashboard_all.py`` 的 JSON 聚合;可选 ``--smoke`` 只跑快速冒烟。
23
+ - ``-v`` / ``--verbose``:逐步打印每条子进程命令、退出码与 stdout(默认截断);
24
+ ``--verbose-json`` 与 ``-v`` 联用时不截断 JSON(输出可能很长)。
25
+
26
+ 环境变量(均可选)
27
+ -----------------
28
+ INSURANCE_SKILLS_ROOT 直接指定 skills 目录(包含四个 insurance-* 子目录)
29
+ INSURANCE_CLOUD_TEST_DIR 测试用的 SQLite 父目录
30
+ INSURANCE_CLOUD_TEST_SMOKE=1 等价于 ``--smoke``
31
+ """
32
+ from __future__ import annotations
33
+
34
+ import argparse
35
+ import json
36
+ import os
37
+ import shlex
38
+ import subprocess
39
+ import sys
40
+ import tempfile
41
+ import time
42
+ from dataclasses import dataclass
43
+ from pathlib import Path
44
+ from typing import Any
45
+
46
+ # 由 main() 设置:详细日志走 stderr,避免与 stdout JSON 混淆
47
+ VERBOSE = False
48
+ VERBOSE_FULL_JSON = False
49
+ VERBOSE_JSON_MAX = 4000
50
+
51
+
52
+ def _log_invocation(title: str, cmd: list[str], r: subprocess.CompletedProcess[str]) -> None:
53
+ if not VERBOSE:
54
+ return
55
+ print(f"\n[VERBOSE] === {title} ===", file=sys.stderr)
56
+ print(f" $ {' '.join(shlex.quote(str(x)) for x in cmd)}", file=sys.stderr)
57
+ print(f" exit_code={r.returncode}", file=sys.stderr)
58
+ out = (r.stdout or "").strip()
59
+ if out:
60
+ if VERBOSE_FULL_JSON or len(out) <= VERBOSE_JSON_MAX:
61
+ print(f" stdout:\n{out}", file=sys.stderr)
62
+ else:
63
+ print(f" stdout (truncated {VERBOSE_JSON_MAX} chars):\n{out[:VERBOSE_JSON_MAX]}…", file=sys.stderr)
64
+ err = (r.stderr or "").strip()
65
+ if err:
66
+ print(f" stderr:\n{err[:4000]}", file=sys.stderr)
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # 路径探测
71
+ # ---------------------------------------------------------------------------
72
+
73
+ SKILL_FOLDERS = {
74
+ "customer": "insurance-customer-policy",
75
+ "product": "insurance-product-analysis",
76
+ "schedule": "insurance-schedule-renewal",
77
+ "pipeline": "insurance-sales-pipeline",
78
+ }
79
+
80
+ MODULES = {
81
+ "customer": "insurance_customer_cli",
82
+ "product": "insurance_product_cli",
83
+ "schedule": "insurance_schedule_cli",
84
+ "pipeline": "insurance_pipeline_cli",
85
+ }
86
+
87
+
88
+ def _has_customer_skill(root: Path) -> bool:
89
+ d = root / SKILL_FOLDERS["customer"]
90
+ return d.is_dir() and (
91
+ (d / "src" / "src" / "insurance_customer_cli" / "__init__.py").is_file()
92
+ or (d / "src" / "scripts" / "insurance_customer_cli" / "__init__.py").is_file()
93
+ or (d / "scripts" / "insurance_customer_cli" / "__init__.py").is_file()
94
+ or (d / "insurance_customer_cli" / "__init__.py").is_file()
95
+ )
96
+
97
+
98
+ def discover_skills_root() -> Path:
99
+ env = os.environ.get("INSURANCE_SKILLS_ROOT") or os.environ.get("SKILLS_ROOT")
100
+ if env:
101
+ p = Path(env).expanduser().resolve()
102
+ if _has_customer_skill(p):
103
+ return p
104
+ raise SystemExit(
105
+ f"[FAIL] INSURANCE_SKILLS_ROOT={p} 下未找到完整的 insurance-customer-policy 包。"
106
+ )
107
+
108
+ script_here = Path(__file__).resolve().parent
109
+ candidates = [
110
+ script_here.parent.parent.parent, # .../skills(脚本在 repo 标准位置时)
111
+ Path("/app/skills"),
112
+ Path.cwd() / "skills",
113
+ Path.cwd(),
114
+ Path.home() / "sophclaw-skills" / "skills",
115
+ ]
116
+ for c in candidates:
117
+ try:
118
+ if c and _has_customer_skill(c.resolve()):
119
+ return c.resolve()
120
+ except OSError:
121
+ continue
122
+
123
+ raise SystemExit(
124
+ "[FAIL] 无法自动定位 skills 根目录。\n"
125
+ "请确认云端已部署包含四个 insurance-* skill 的目录树,然后设置:\n"
126
+ " export INSURANCE_SKILLS_ROOT=/实际路径/skills\n"
127
+ "常见布局示例:/app/skills/insurance-customer-policy/src/src/insurance_customer_cli/...\n"
128
+ "若文件不存在,说明镜像未更新:请在 CI/CD 或挂载卷中包含 feat/insurance-4-skills 分支代码。"
129
+ )
130
+
131
+
132
+ def resolve_pkg_parent(skills_root: Path, key: str) -> Path | None:
133
+ folder = SKILL_FOLDERS[key]
134
+ pkg = MODULES[key]
135
+ d = skills_root / folder
136
+ candidates = [
137
+ d / "src" / "scripts",
138
+ d / "src" / "src",
139
+ d / "src",
140
+ d / "scripts",
141
+ Path("/app/skills") / folder / "src" / "scripts",
142
+ Path("/app/skills") / folder / "src" / "src",
143
+ Path("/app/skills") / folder / "src",
144
+ Path("/app/skills") / folder / "scripts",
145
+ d,
146
+ ]
147
+ for c in candidates:
148
+ try:
149
+ if (c / pkg / "__init__.py").is_file():
150
+ return c
151
+ except OSError:
152
+ continue
153
+ return None
154
+
155
+
156
+ def resolve_cli_wrapper(skills_root: Path, key: str) -> Path | None:
157
+ folder = SKILL_FOLDERS[key]
158
+ d = skills_root / folder
159
+ candidates = [
160
+ d / "src" / "scripts" / "cli.py",
161
+ d / "scripts" / "cli.py",
162
+ Path("/app/skills") / folder / "src" / "scripts" / "cli.py",
163
+ ]
164
+ for c in candidates:
165
+ try:
166
+ if c.is_file():
167
+ return c
168
+ except OSError:
169
+ continue
170
+ return None
171
+
172
+
173
+ def resolve_dashboard_all(skills_root: Path) -> Path | None:
174
+ folder = SKILL_FOLDERS["customer"]
175
+ d = skills_root / folder
176
+ candidates = [
177
+ d / "src" / "scripts" / "dashboard_all.py",
178
+ d / "scripts" / "dashboard_all.py",
179
+ Path("/app/skills") / folder / "src" / "scripts" / "dashboard_all.py",
180
+ Path("/app/skills") / folder / "scripts" / "dashboard_all.py",
181
+ ]
182
+ for c in candidates:
183
+ try:
184
+ if c.is_file():
185
+ return c
186
+ except OSError:
187
+ continue
188
+ return None
189
+
190
+
191
+ @dataclass
192
+ class Layout:
193
+ skills_root: Path
194
+ customer_pkg: Path
195
+ product_pkg: Path
196
+ schedule_pkg: Path
197
+ pipeline_pkg: Path
198
+ customer_cli: Path | None
199
+ product_cli: Path | None
200
+ schedule_cli: Path | None
201
+ pipeline_cli: Path | None
202
+ dashboard_all: Path | None
203
+
204
+
205
+ def resolve_layout(skills_root: Path) -> Layout:
206
+ pkgs = {k: resolve_pkg_parent(skills_root, k) for k in SKILL_FOLDERS}
207
+ missing = [k for k, p in pkgs.items() if p is None]
208
+ if missing:
209
+ raise SystemExit(
210
+ f"[FAIL] 下列 skill 的包目录解析失败(缺少 insurance_*_cli 包): {missing}\n"
211
+ f"skills_root={skills_root}"
212
+ )
213
+ return Layout(
214
+ skills_root=skills_root,
215
+ customer_pkg=pkgs["customer"], # type: ignore[arg-type]
216
+ product_pkg=pkgs["product"], # type: ignore[arg-type]
217
+ schedule_pkg=pkgs["schedule"], # type: ignore[arg-type]
218
+ pipeline_pkg=pkgs["pipeline"], # type: ignore[arg-type]
219
+ customer_cli=resolve_cli_wrapper(skills_root, "customer"),
220
+ product_cli=resolve_cli_wrapper(skills_root, "product"),
221
+ schedule_cli=resolve_cli_wrapper(skills_root, "schedule"),
222
+ pipeline_cli=resolve_cli_wrapper(skills_root, "pipeline"),
223
+ dashboard_all=resolve_dashboard_all(skills_root),
224
+ )
225
+
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # 子进程调用
229
+ # ---------------------------------------------------------------------------
230
+
231
+ def _base_env(db_dir: Path) -> dict[str, str]:
232
+ env = dict(os.environ)
233
+ env["PYTHONIOENCODING"] = "utf-8"
234
+ # 去掉 PYTHONPATH,强制走 wrapper 或我们显式注入的路径
235
+ env.pop("PYTHONPATH", None)
236
+ # 共享单库模式:四个 skill 共用一个 sqlite 文件。
237
+ shared = str(db_dir / "insurance.sqlite3")
238
+ env["INSURANCE_DB_PATH"] = shared
239
+ # 兼容旧逻辑:显式给各 skill 变量同一个值,避免老代码路径分叉。
240
+ env["INSURANCE_CUSTOMER_DB_PATH"] = shared
241
+ env["INSURANCE_PRODUCT_DB_PATH"] = shared
242
+ env["INSURANCE_SCHEDULE_DB_PATH"] = shared
243
+ env["INSURANCE_PIPELINE_DB_PATH"] = shared
244
+ return env
245
+
246
+
247
+ def invoke_skill(
248
+ layout: Layout,
249
+ key: str,
250
+ argv: list[str],
251
+ env: dict[str, str],
252
+ cwd: Path | None,
253
+ ) -> subprocess.CompletedProcess[str]:
254
+ pkg = {
255
+ "customer": layout.customer_pkg,
256
+ "product": layout.product_pkg,
257
+ "schedule": layout.schedule_pkg,
258
+ "pipeline": layout.pipeline_pkg,
259
+ }[key]
260
+ cli = {
261
+ "customer": layout.customer_cli,
262
+ "product": layout.product_cli,
263
+ "schedule": layout.schedule_cli,
264
+ "pipeline": layout.pipeline_cli,
265
+ }[key]
266
+ mod = MODULES[key]
267
+
268
+ if cli is not None and cli.is_file():
269
+ cmd = [sys.executable, str(cli), *argv]
270
+ r = subprocess.run(
271
+ cmd,
272
+ cwd=str(cwd) if cwd else None,
273
+ env=env,
274
+ capture_output=True,
275
+ text=True,
276
+ encoding="utf-8",
277
+ timeout=120,
278
+ )
279
+ preview = " ".join(shlex.quote(x) for x in argv)
280
+ if len(preview) > 280:
281
+ preview = preview[:280] + "…"
282
+ _log_invocation(f"{key} ({preview})", cmd, r)
283
+ return r
284
+
285
+ e2 = {**env, "PYTHONPATH": str(pkg)}
286
+ cmd = [sys.executable, "-m", mod, *argv]
287
+ r = subprocess.run(
288
+ cmd,
289
+ cwd=str(cwd) if cwd else None,
290
+ env=e2,
291
+ capture_output=True,
292
+ text=True,
293
+ encoding="utf-8",
294
+ timeout=120,
295
+ )
296
+ preview = " ".join(shlex.quote(x) for x in argv)
297
+ if len(preview) > 280:
298
+ preview = preview[:280] + "…"
299
+ _log_invocation(f"{key} via -m {mod} ({preview})", cmd, r)
300
+ return r
301
+
302
+
303
+ def parse_json_stdout(rc: int, stdout: str, stderr: str, label: str) -> dict[str, Any]:
304
+ out = (stdout or "").strip()
305
+ if not out:
306
+ raise AssertionError(f"{label}: 无 stdout,rc={rc}\nSTDERR:\n{stderr}")
307
+ try:
308
+ return json.loads(out)
309
+ except json.JSONDecodeError as e:
310
+ raise AssertionError(f"{label}: JSON 解析失败 {e}\nSTDOUT:\n{out[:2000]}\nSTDERR:\n{stderr}") from e
311
+
312
+
313
+ def must_ok(d: dict[str, Any], label: str) -> None:
314
+ if not d.get("ok"):
315
+ raise AssertionError(f"{label}: 期望 ok=true,实际={json.dumps(d, ensure_ascii=False)[:1200]}")
316
+
317
+
318
+ def run_customer(layout: Layout, env: dict[str, str], cwd: Path, args: list[str]) -> dict[str, Any]:
319
+ r = invoke_skill(layout, "customer", args, env, cwd)
320
+ if r.returncode not in (0, 1):
321
+ raise AssertionError(
322
+ f"customer {' '.join(args)} rc={r.returncode}\nSTDOUT:\n{r.stdout}\nSTDERR:\n{r.stderr}"
323
+ )
324
+ return parse_json_stdout(r.returncode, r.stdout, r.stderr, f"customer {' '.join(args)}")
325
+
326
+
327
+ def run_product(layout: Layout, env: dict[str, str], cwd: Path, args: list[str]) -> dict[str, Any]:
328
+ r = invoke_skill(layout, "product", args, env, cwd)
329
+ if r.returncode not in (0, 1):
330
+ raise AssertionError(
331
+ f"product {' '.join(args)} rc={r.returncode}\nSTDOUT:\n{r.stdout}\nSTDERR:\n{r.stderr}"
332
+ )
333
+ return parse_json_stdout(r.returncode, r.stdout, r.stderr, f"product {' '.join(args)}")
334
+
335
+
336
+ def run_schedule(layout: Layout, env: dict[str, str], cwd: Path, args: list[str]) -> dict[str, Any]:
337
+ r = invoke_skill(layout, "schedule", args, env, cwd)
338
+ if r.returncode not in (0, 1):
339
+ raise AssertionError(
340
+ f"schedule {' '.join(args)} rc={r.returncode}\nSTDOUT:\n{r.stdout}\nSTDERR:\n{r.stderr}"
341
+ )
342
+ return parse_json_stdout(r.returncode, r.stdout, r.stderr, f"schedule {' '.join(args)}")
343
+
344
+
345
+ def run_pipeline(layout: Layout, env: dict[str, str], cwd: Path, args: list[str]) -> dict[str, Any]:
346
+ r = invoke_skill(layout, "pipeline", args, env, cwd)
347
+ if r.returncode not in (0, 1):
348
+ raise AssertionError(
349
+ f"pipeline {' '.join(args)} rc={r.returncode}\nSTDOUT:\n{r.stdout}\nSTDERR:\n{r.stderr}"
350
+ )
351
+ return parse_json_stdout(r.returncode, r.stdout, r.stderr, f"pipeline {' '.join(args)}")
352
+
353
+
354
+ # ---------------------------------------------------------------------------
355
+ # 测试主体
356
+ # ---------------------------------------------------------------------------
357
+
358
+ def test_full(layout: Layout, db_dir: Path, smoke: bool) -> None:
359
+ cwd = Path(tempfile.gettempdir()) / "insurance_cli_cwd_probe"
360
+ cwd.mkdir(parents=True, exist_ok=True)
361
+
362
+ env = _base_env(db_dir)
363
+
364
+ ts = time.strftime("%Y%m%d%H%M%S", time.localtime())
365
+ cid_main = f"C_MAIN_{ts}"
366
+ cid_aux = f"C_AUX_{ts}"
367
+ cid_del = f"C_DEL_{ts}"
368
+
369
+ print(f"\n=== [1/5] insurance-customer-policy ===")
370
+
371
+ d = run_customer(layout, env, cwd, [
372
+ "customer-add", "--customer-id", cid_main, "--name", "云端主客户", "--phone", "13900000001",
373
+ "--age", "38", "--gender", "女", "--income", "500000",
374
+ "--household", '{"spouse":true,"children":1}',
375
+ "--occupation", "工程师", "--birthday", "1988-05-09",
376
+ "--tags", '["高净值"]', "--fields", "{}",
377
+ ])
378
+ must_ok(d, "customer-add main")
379
+
380
+ d = run_customer(layout, env, cwd, [
381
+ "customer-add", "--customer-id", cid_aux, "--name", "云端辅客户", "--phone", "13900000002",
382
+ "--age", "30", "--income", "120000", "--birthday", "1996-03-15",
383
+ ])
384
+ must_ok(d, "customer-add aux")
385
+
386
+ d = run_customer(layout, env, cwd, [
387
+ "customer-add", "--customer-id", cid_del, "--name", "待删除客户", "--phone", "13900000003",
388
+ ])
389
+ must_ok(d, "customer-add del")
390
+
391
+ d = run_customer(layout, env, cwd, ["customer-query", "--keyword", "云端"])
392
+ must_ok(d, "customer-query keyword")
393
+ assert d.get("count", 0) >= 2, d
394
+
395
+ d = run_customer(layout, env, cwd, ["customer-query", "--customer-id", cid_main])
396
+ must_ok(d, "customer-query id")
397
+ assert d["customers"][0]["name"] == "云端主客户", d
398
+
399
+ d = run_customer(layout, env, cwd, [
400
+ "customer-update", "--customer-id", cid_main,
401
+ "--set", '{"occupation":"架构师","income":520000}',
402
+ ])
403
+ must_ok(d, "customer-update")
404
+
405
+ d = run_customer(layout, env, cwd, [
406
+ "policy-add", "--customer-id", cid_main, "--company", "平安健康", "--product", "百万医疗2024",
407
+ "--type", "医疗险", "--coverage", "4000000", "--premium", "800",
408
+ "--effective-date", "2026-01-01", "--pay-period", "1年",
409
+ "--insured", "本人", "--beneficiary", "法定",
410
+ ])
411
+ must_ok(d, "policy-add")
412
+ pid = d.get("id") or d.get("policy_id")
413
+ assert pid, d
414
+
415
+ if not smoke:
416
+ d = run_customer(layout, env, cwd, [
417
+ "policy-add", "--customer-id", cid_main, "--company", "国寿", "--product", "康宁重疾",
418
+ "--type", "重疾险", "--coverage", "300000", "--premium", "9000",
419
+ "--effective-date", "2025-06-01", "--expire-date", "2045-06-01",
420
+ "--next-pay-date", "2026-06-01",
421
+ ])
422
+ must_ok(d, "policy-add explicit dates")
423
+
424
+ d = run_customer(layout, env, cwd, ["policy-query", "--customer-id", cid_main])
425
+ must_ok(d, "policy-query")
426
+ assert d.get("count", 0) >= 1, d
427
+
428
+ d = run_customer(layout, env, cwd, ["policy-query", "--policy-id", pid])
429
+ must_ok(d, "policy-query by policy-id")
430
+
431
+ d = run_customer(layout, env, cwd, ["policy-query", "--product-type", "医疗险"])
432
+ must_ok(d, "policy-query by type")
433
+
434
+ d = run_customer(layout, env, cwd, ["policy-count-by-customer", "--customer-id", cid_main])
435
+ must_ok(d, "policy-count-by-customer")
436
+
437
+ d = run_customer(layout, env, cwd, [
438
+ "policy-update", "--policy-id", pid, "--set", '{"notes":"测试备注更新"}',
439
+ ])
440
+ must_ok(d, "policy-update")
441
+
442
+ d = run_customer(layout, env, cwd, [
443
+ "followup-add", "--customer-id", cid_main, "--content", "介绍了重疾险理念",
444
+ "--next-step", "下周递送计划书", "--next-date", "2026-05-20", "--channel", "微信",
445
+ ])
446
+ must_ok(d, "followup-add")
447
+
448
+ d = run_customer(layout, env, cwd, ["followup-query", "--customer-id", cid_main])
449
+ must_ok(d, "followup-query by customer")
450
+
451
+ d = run_customer(layout, env, cwd, ["followup-query", "--days", "365"])
452
+ must_ok(d, "followup-query by days")
453
+
454
+ d = run_customer(layout, env, cwd, ["gap-analysis", "--customer-id", cid_main])
455
+ must_ok(d, "gap-analysis single")
456
+
457
+ d = run_customer(layout, env, cwd, ["gap-analysis", "--all"])
458
+ must_ok(d, "gap-analysis all")
459
+
460
+ d = run_customer(layout, env, cwd, ["gap-analysis", "--all", "--gap-type", "养老险"])
461
+ must_ok(d, "gap-analysis gap-type")
462
+
463
+ d = run_customer(layout, env, cwd, ["customer-segment"])
464
+ must_ok(d, "customer-segment")
465
+
466
+ d = run_customer(layout, env, cwd, ["customer-field-add", "--name", "risk_level", "--type", "text"])
467
+ must_ok(d, "customer-field-add")
468
+
469
+ d = run_customer(layout, env, cwd, ["customer-field-query"])
470
+ must_ok(d, "customer-field-query")
471
+
472
+ d = run_customer(layout, env, cwd, ["customer-exists", "--customer-id", cid_main, "--json"])
473
+ must_ok(d, "customer-exists")
474
+ assert d.get("exists") is True, d
475
+
476
+ d = run_customer(layout, env, cwd, ["dashboard", "--json"])
477
+ must_ok(d, "customer dashboard")
478
+
479
+ # ------------------------------------------------------------------
480
+
481
+ print(f"\n=== [2/5] insurance-product-analysis ===")
482
+
483
+ pname_a = f"云测重疾A_{ts}"
484
+ pname_b = f"云测重疾B_{ts}"
485
+
486
+ d = run_product(layout, env, cwd, [
487
+ "product-add", "--name", pname_a, "--company", "保司A", "--type", "重疾险",
488
+ "--coverage-range", "10万-50万", "--premium-range", "3000-8000",
489
+ "--age-range", "0-55岁", "--highlights", "多次赔付|癌症二次",
490
+ ])
491
+ must_ok(d, "product-add A")
492
+
493
+ d = run_product(layout, env, cwd, [
494
+ "product-add", "--name", pname_b, "--company", "保司B", "--type", "重疾险",
495
+ "--coverage-range", "10万-50万", "--premium-range", "2500-9000",
496
+ "--age-range", "0-60岁", "--highlights", "消费型|价格便宜",
497
+ ])
498
+ must_ok(d, "product-add B")
499
+
500
+ d = run_product(layout, env, cwd, ["product-query", "--name", pname_a])
501
+ must_ok(d, "product-query name")
502
+
503
+ d = run_product(layout, env, cwd, ["product-query", "--type", "重疾险"])
504
+ must_ok(d, "product-query type")
505
+
506
+ d = run_product(layout, env, cwd, ["product-update", "--name", pname_a, "--set", '{"waiting_period":"90天"}'])
507
+ must_ok(d, "product-update")
508
+
509
+ # product-analyze:冷启动应返回 needs_llm
510
+ d = run_product(layout, env, cwd, ["product-analyze", "--product", pname_a])
511
+ assert d.get("error") == "needs_llm", d
512
+
513
+ sample_result = json.dumps(
514
+ {
515
+ "key_terms": ["保100种重疾"],
516
+ "selling_points": ["保费低"],
517
+ "fit_audience": ["30-45岁家庭支柱"],
518
+ "pitch_script": "这款重疾主打性价比……",
519
+ },
520
+ ensure_ascii=False,
521
+ )
522
+ d = run_product(layout, env, cwd, ["analysis-write", "--product", pname_a, "--result", sample_result])
523
+ must_ok(d, "analysis-write")
524
+
525
+ d = run_product(layout, env, cwd, ["product-analyze", "--product", pname_a])
526
+ must_ok(d, "product-analyze cached")
527
+
528
+ d = run_product(layout, env, cwd, ["gap-analysis", "--customer-id", cid_main])
529
+ must_ok(d, "product gap-analysis forward")
530
+
531
+ d = run_product(layout, env, cwd, ["customer-match", "--product", pname_a])
532
+ must_ok(d, "customer-match by product")
533
+
534
+ d = run_product(layout, env, cwd, ["customer-match", "--customer-id", cid_aux])
535
+ must_ok(d, "customer-match by customer")
536
+
537
+ d = run_product(layout, env, cwd, ["product-compare", "--product-a", pname_a, "--product-b", pname_b])
538
+ must_ok(d, "product-compare")
539
+
540
+ d = run_product(layout, env, cwd, ["dashboard", "--json"])
541
+ must_ok(d, "product dashboard")
542
+
543
+ # ------------------------------------------------------------------
544
+
545
+ print(f"\n=== [3/5] insurance-schedule-renewal ===")
546
+
547
+ d = run_schedule(layout, env, cwd, ["renewal-query", "--days", "3650"])
548
+ must_ok(d, "renewal-query")
549
+
550
+ d = run_schedule(layout, env, cwd, ["renewal-remind", "--days", "3650"])
551
+ must_ok(d, "renewal-remind")
552
+
553
+ d = run_schedule(layout, env, cwd, ["renewal-remind", "--days", "3650", "--mark-sent"])
554
+ must_ok(d, "renewal-remind mark-sent")
555
+
556
+ d = run_schedule(layout, env, cwd, ["followup-check", "--days", "30"])
557
+ must_ok(d, "followup-check")
558
+
559
+ d = run_schedule(layout, env, cwd, ["followup-remind", "--days", "30"])
560
+ must_ok(d, "followup-remind")
561
+
562
+ d = run_schedule(layout, env, cwd, ["followup-remind", "--days", "30", "--mark-sent"])
563
+ must_ok(d, "followup-remind mark-sent")
564
+
565
+ d = run_schedule(layout, env, cwd, ["dormant-query", "--days", "60"])
566
+ must_ok(d, "dormant-query")
567
+
568
+ d = run_schedule(layout, env, cwd, ["dormant-wake", "--days", "60"])
569
+ must_ok(d, "dormant-wake")
570
+
571
+ d = run_schedule(layout, env, cwd, ["special-date-query", "--type", "birthday", "--month", "5"])
572
+ must_ok(d, "special-date-query birthday")
573
+
574
+ d = run_schedule(layout, env, cwd, ["special-date-query", "--type", "anniversary", "--month", "6"])
575
+ must_ok(d, "special-date-query anniversary")
576
+
577
+ d = run_schedule(layout, env, cwd, ["daily-summary"])
578
+ must_ok(d, "daily-summary")
579
+
580
+ d = run_schedule(layout, env, cwd, ["dashboard", "--json"])
581
+ must_ok(d, "schedule dashboard")
582
+
583
+ # ------------------------------------------------------------------
584
+
585
+ print(f"\n=== [4/5] insurance-sales-pipeline ===")
586
+
587
+ d = run_pipeline(layout, env, cwd, [
588
+ "deal-add", "--customer-id", cid_main, "--product", pname_a, "--product-type", "重疾险",
589
+ "--stage", "在谈", "--estimated-premium", "8000", "--expected-close-date", "2026-05-30",
590
+ "--note", "初次接触",
591
+ ])
592
+ must_ok(d, "deal-add")
593
+ deal_win = d["id"]
594
+
595
+ d = run_pipeline(layout, env, cwd, [
596
+ "deal-add", "--customer-id", cid_aux, "--product", pname_b, "--product-type", "重疾险",
597
+ "--stage", "在谈", "--estimated-premium", "6000",
598
+ ])
599
+ must_ok(d, "deal-add aux")
600
+ deal_lose = d["id"]
601
+
602
+ d = run_pipeline(layout, env, cwd, ["deal-query", "--deal-id", deal_win])
603
+ must_ok(d, "deal-query id")
604
+
605
+ d = run_pipeline(layout, env, cwd, ["deal-query", "--customer-id", cid_main])
606
+ must_ok(d, "deal-query customer")
607
+
608
+ d = run_pipeline(layout, env, cwd, ["deal-query", "--stage", "在谈"])
609
+ must_ok(d, "deal-query stage")
610
+
611
+ d = run_pipeline(layout, env, cwd, ["deal-query", "--status", "active"])
612
+ must_ok(d, "deal-query active")
613
+
614
+ d = run_pipeline(layout, env, cwd, [
615
+ "deal-update", "--deal-id", deal_win, "--set", '{"note":"更新备注"}',
616
+ ])
617
+ must_ok(d, "deal-update")
618
+
619
+ d = run_pipeline(layout, env, cwd, [
620
+ "deal-stage", "--deal-id", deal_win, "--to", "方案中", "--note", "已递方案",
621
+ ])
622
+ must_ok(d, "deal-stage ->方案中")
623
+
624
+ d = run_pipeline(layout, env, cwd, [
625
+ "deal-stage", "--deal-id", deal_win, "--to", "议价", "--note", "讨论折扣",
626
+ ])
627
+ must_ok(d, "deal-stage ->议价")
628
+
629
+ d = run_pipeline(layout, env, cwd, [
630
+ "deal-stage", "--deal-id", deal_win, "--to", "已成交",
631
+ "--actual-premium", "8800", "--note", "签单成功",
632
+ ])
633
+ must_ok(d, "deal-stage ->已成交")
634
+
635
+ month = time.strftime("%Y-%m", time.localtime())
636
+ d = run_pipeline(layout, env, cwd, ["deal-forecast", "--month", month])
637
+ must_ok(d, "deal-forecast")
638
+
639
+ d = run_pipeline(layout, env, cwd, ["target-track", "--month", month, "--target", "100000"])
640
+ must_ok(d, "target-track write")
641
+
642
+ d = run_pipeline(layout, env, cwd, ["target-track", "--month", month])
643
+ must_ok(d, "target-track read")
644
+
645
+ d = run_pipeline(layout, env, cwd, [
646
+ "deal-lost", "--deal-id", deal_lose, "--reason", "竞品", "--note", "选了别家",
647
+ ])
648
+ must_ok(d, "deal-lost")
649
+
650
+ d = run_pipeline(layout, env, cwd, ["deal-lost", "--stats", "--month", month])
651
+ must_ok(d, "deal-lost stats")
652
+
653
+ d = run_pipeline(layout, env, cwd, ["dashboard", "--json"])
654
+ must_ok(d, "pipeline dashboard")
655
+
656
+ # ------------------------------------------------------------------
657
+
658
+ print(f"\n=== [5/5] dashboard_all + 清理 ===")
659
+
660
+ if layout.dashboard_all is None or not layout.dashboard_all.is_file():
661
+ print("[WARN] 未找到 dashboard_all.py,跳过聚合测试(请更新镜像)。")
662
+ else:
663
+ _dacmd = [sys.executable, str(layout.dashboard_all)]
664
+ r = subprocess.run(
665
+ _dacmd,
666
+ cwd=str(cwd),
667
+ env=env,
668
+ capture_output=True,
669
+ text=True,
670
+ encoding="utf-8",
671
+ timeout=120,
672
+ )
673
+ _log_invocation("dashboard_all (JSON aggregate)", _dacmd, r)
674
+ assert r.returncode == 0, f"dashboard_all rc={r.returncode}\n{r.stderr}\n{r.stdout[:800]}"
675
+ agg = json.loads(r.stdout)
676
+ must_ok(agg, "dashboard_all")
677
+ assert "overview" in agg and "skills" in agg, agg
678
+ # 兼容两种聚合结构:
679
+ # 1) 旧版:skills 为各子 skill dashboard 结果(每项含 ok)
680
+ # 2) 新版共享库:skills={"source":"shared-db"}
681
+ skills_obj = agg.get("skills") or {}
682
+ if isinstance(skills_obj, dict):
683
+ has_subskill_payload = any(isinstance(v, dict) for v in skills_obj.values())
684
+ if has_subskill_payload:
685
+ for sk, sub in skills_obj.items():
686
+ if isinstance(sub, dict):
687
+ assert sub.get("ok"), f"sub skill {sk} failed: {sub}"
688
+
689
+ _mdcmd = [sys.executable, str(layout.dashboard_all), "--md"]
690
+ r = subprocess.run(
691
+ _mdcmd,
692
+ cwd=str(cwd),
693
+ env=env,
694
+ capture_output=True,
695
+ text=True,
696
+ encoding="utf-8",
697
+ timeout=120,
698
+ )
699
+ _log_invocation("dashboard_all --md", _mdcmd, r)
700
+ assert r.returncode == 0, r.stderr
701
+ assert "经营概览" in r.stdout, r.stdout[:500]
702
+
703
+ # 清理:删产品、删保单、删客户
704
+ d = run_product(layout, env, cwd, ["product-delete", "--name", pname_a, "--yes"])
705
+ must_ok(d, "product-delete A")
706
+
707
+ d = run_product(layout, env, cwd, ["product-delete", "--name", pname_b, "--yes"])
708
+ must_ok(d, "product-delete B")
709
+
710
+ # 查询主客户所有保单并软删
711
+ pq = run_customer(layout, env, cwd, ["policy-query", "--customer-id", cid_main])
712
+ must_ok(pq, "policy-query for cleanup")
713
+ for pol in pq.get("policies", []):
714
+ pid_c = pol.get("id") or pol.get("policy_id")
715
+ if pid_c:
716
+ run_customer(layout, env, cwd, ["policy-delete", "--policy-id", pid_c, "--yes"])
717
+
718
+ run_pipeline(layout, env, cwd, ["deal-delete", "--deal-id", deal_win, "--yes"])
719
+ # deal_lose 已是流失状态,也可删除
720
+ dq = run_pipeline(layout, env, cwd, ["deal-query", "--customer-id", cid_aux])
721
+ if dq.get("deals"):
722
+ run_pipeline(layout, env, cwd, ["deal-delete", "--deal-id", dq["deals"][0]["id"], "--yes"])
723
+
724
+ run_customer(layout, env, cwd, ["customer-delete", "--customer-id", cid_main, "--yes"])
725
+ run_customer(layout, env, cwd, ["customer-delete", "--customer-id", cid_aux, "--yes"])
726
+ run_customer(layout, env, cwd, ["customer-delete", "--customer-id", cid_del, "--yes"])
727
+
728
+ print("\n>>> 全部测试通过 <<<")
729
+ print(
730
+ "说明:若未加 -v,上述仅显示阶段标题;断言全部通过即视为成功。"
731
+ "验证退出码请执行: echo $? (应为 0)",
732
+ file=sys.stderr,
733
+ )
734
+
735
+
736
+ def main() -> int:
737
+ global VERBOSE, VERBOSE_FULL_JSON
738
+ parser = argparse.ArgumentParser(description="保险 4-skill 云端全覆盖测试")
739
+ parser.add_argument("--smoke", action="store_true", help="跳过部分冗长用例")
740
+ parser.add_argument("-v", "--verbose", action="store_true", help=" stderr 打印每条 CLI 调用与输出摘要")
741
+ parser.add_argument(
742
+ "--verbose-json",
743
+ action="store_true",
744
+ help="与 -v 合用:stdout 不截断(JSON 可能很长)",
745
+ )
746
+ args = parser.parse_args()
747
+ smoke = args.smoke or os.environ.get("INSURANCE_CLOUD_TEST_SMOKE") == "1"
748
+ VERBOSE = bool(args.verbose)
749
+ VERBOSE_FULL_JSON = bool(args.verbose_json)
750
+
751
+ try:
752
+ sys.stdout.reconfigure(encoding="utf-8")
753
+ sys.stderr.reconfigure(encoding="utf-8")
754
+ except (AttributeError, OSError):
755
+ pass
756
+
757
+ skills_root = discover_skills_root()
758
+ layout = resolve_layout(skills_root)
759
+
760
+ db_base = os.environ.get("INSURANCE_CLOUD_TEST_DIR")
761
+ if db_base:
762
+ db_dir = Path(db_base).expanduser().resolve()
763
+ db_dir.mkdir(parents=True, exist_ok=True)
764
+ else:
765
+ db_dir = Path(tempfile.mkdtemp(prefix=f"insurance_cloud_test_{os.getpid()}_"))
766
+
767
+ print("------ 保险 Skill 云端全覆盖自检 ------")
768
+ print(f"skills_root = {layout.skills_root}")
769
+ print(f"test_db_dir = {db_dir}")
770
+ print(f"customer_cli = {layout.customer_cli or '(fallback: python -m)'}")
771
+ print(f"dashboard_all = {layout.dashboard_all or '(missing)'}")
772
+ print(f"smoke_mode = {smoke}")
773
+ print(f"verbose = {VERBOSE}" + (" +full_json" if VERBOSE_FULL_JSON else ""))
774
+
775
+ test_full(layout, db_dir, smoke=smoke)
776
+ print("exit_code=0", file=sys.stderr)
777
+ return 0
778
+
779
+
780
+ if __name__ == "__main__":
781
+ try:
782
+ raise SystemExit(main())
783
+ except AssertionError as e:
784
+ print(f"\n[ASSERT FAILED] {e}", file=sys.stderr)
785
+ raise SystemExit(1)