@harmonyos-arkts/d2h 0.1.3 → 0.1.5

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 (65) hide show
  1. package/README.md +78 -44
  2. package/agents/android-to-hmos-00-orchestrator.md +34 -28
  3. package/agents/d2h.md +266 -95
  4. package/dist/bin/install-opencode.js +137 -43
  5. package/dist/src/install-opencode.js +44 -0
  6. package/dist/src/install-vscode.js +226 -0
  7. package/dist/src/toolchain.js +83 -0
  8. package/docs/environment-check.md +86 -0
  9. package/opencode/agents.json +1 -1
  10. package/opencode/vscode-d2h-package.json +7 -0
  11. package/opencode/vscode-d2h-plugin.mjs +29 -0
  12. package/package.json +6 -4
  13. package/schemas/task-envelope.schema.json +58 -4
  14. package/schemas/task-plan.schema.json +281 -63
  15. package/scripts/check-environment.mjs +321 -0
  16. package/skills/d2h-hypium-forge/SKILL.md +252 -0
  17. package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -0
  18. package/skills/d2h-hypium-forge/references/official-api.md +135 -0
  19. package/skills/d2h-hypium-forge/references/pitfalls.md +98 -0
  20. package/skills/d2h-hypium-forge/references/testability-template.md +71 -0
  21. package/skills/d2h-hypium-forge/scripts/_common_template.py +614 -0
  22. package/skills/d2h-hypium-forge/scripts/analyze_results.py +156 -0
  23. package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -0
  24. package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -0
  25. package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -0
  26. package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -0
  27. package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -0
  28. package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -0
  29. package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -0
  30. package/skills/d2h-hypium-forge/scripts/precondition.py +140 -0
  31. package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -0
  32. package/skills/d2h-hypium-forge/scripts/run_direct.py +392 -0
  33. package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -0
  34. package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -0
  35. package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -0
  36. package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -0
  37. package/skills/d2h-utg-ai-generate/SKILL.md +128 -0
  38. package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -0
  39. package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -0
  40. package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -0
  41. package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -0
  42. package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -0
  43. package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -0
  44. package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -0
  45. package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -0
  46. package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -0
  47. package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -0
  48. package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -0
  49. package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -0
  50. package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -0
  51. package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -0
  52. package/skills/d2h-utg-static-generate/SKILL.md +73 -0
  53. package/skills/d2h-utg-static-generate/references/discoveries.md +38 -0
  54. package/skills/d2h-utg-static-generate/scripts/generate.py +499 -0
  55. package/skills/d2h-utg-static-generate/tests/test_static.py +335 -0
  56. package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -0
  57. package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -0
  58. package/skills/harmonyos-migration-playbook/references/ui-automation.md +4 -3
  59. package/skills/harmonyos-migration-playbook/references/windows-environment.md +1 -1
  60. package/skills/managing-d2h-toolchain/SKILL.md +23 -0
  61. package/skills/managing-d2h-toolchain/environment-requirements.json +16 -0
  62. package/skills/managing-d2h-toolchain/toolchain.json +16 -0
  63. package/skills/preparing-migration-workspace/SKILL.md +9 -7
  64. package/agents/d2h_v2.md +0 -351
  65. package/skills/preparing-migration-workspace/toolchain.json +0 -19
@@ -0,0 +1,614 @@
1
+ # coding: utf-8
2
+ """d2h-hypium-forge 共享库(自动生成)
3
+
4
+ 前置条件分组编排约定(TD-004):
5
+ 需要登录态的用例文件顶部声明 PRECOND = 'logged_in'
6
+ 首启态(bm clean)用例声明 PRECOND = 'first_launch'
7
+ 缺省 PRECOND = 'anonymous'
8
+ run_direct 按 anonymous→logged_in→first_launch 分组排序(破坏性递增,clean 类排尾),
9
+ 组内软隔离(launch() 默认 reset_data=False,preferences 保留→登录态保持)。
10
+ ensure_login 等 setup 必须【幂等】:先探测状态(抽屉用户名已就位)再决定是否走完整
11
+ UI 登录——组首一次完整 setup,组内后续直接复用(省重复登录/注册)。
12
+ 组首条执行时环境变量 FORGE_GROUP_FIRST=1(可选做完整冷启验证)。
13
+ """
14
+ import time
15
+ from hypium import *
16
+ from hypium.model import MatchPattern, UiParam
17
+
18
+ PACKAGE = "me.hackerchick.raisetoanswer"
19
+ ABILITY = "EntryAbility"
20
+ ENTRY_PAGE = "pages/Index"
21
+
22
+
23
+ # ── Level 0 统一引导引擎(TD-008):权限/隐私/广告/引导弹窗 + tab 归位,一揽子消化 ──
24
+ # 关卡模型:每关 = 探测特征文案集合 → 处置(点击命中项)→ 循环消化到无特征。
25
+ # app 级配置 GATES(按需增删);bootstrap 幂等(主页锚可见即成功),超时输出页面快照供分诊。
26
+
27
+ GATES = [
28
+ {"name": "privacy", "texts": ["同意并继续", "同意", "知道了", "我知道了"]},
29
+ {"name": "permission", "texts": ["仅在使用中允许", "使用应用时允许", "允许"]},
30
+ {"name": "ad", "texts": ["跳过", "关闭广告", "不再显示", "以后再说", "暂不"]},
31
+ ]
32
+
33
+ def _page_texts(d):
34
+ try:
35
+ return [(c.getText() or '') for c in (d.find_all_components(BY.type("Text")) or [])
36
+ if c.getText()]
37
+ except Exception:
38
+ return []
39
+
40
+ def bootstrap(d, gates=None, home_tab="", anchor_key="", total=60, log=print):
41
+ """冷启到可用基座。流程:轮询消化全部弹窗关卡(一轮多点,直到无特征)→
42
+ tab 归位(点目标 tab + 验证锚)→ 主页锚可见返回 True。
43
+ anchor_key 是"基座就绪"的唯一判据(app 级配置,如目标 tab 的内容控件 id)。"""
44
+ gates = gates if gates is not None else GATES
45
+ all_texts = [t for g in gates for t in g["texts"]]
46
+ t0 = time.time()
47
+ dismissed = []
48
+ while time.time() - t0 < total:
49
+ # 1) 主页锚已可见 → 提前成功(幂等)
50
+ if anchor_key:
51
+ try:
52
+ if d.find_component(BY.key(anchor_key)) is not None:
53
+ if dismissed:
54
+ log(f" [L0] 弹窗消化: {dismissed}")
55
+ return True
56
+ except Exception:
57
+ pass
58
+ # 2) 消化所有弹窗关卡:一轮内逐个点
59
+ hit_any = False
60
+ for t in all_texts:
61
+ if t == (home_tab or ""):
62
+ continue
63
+ try:
64
+ # 全类型探测(按钮常为 Button 非 Text——TD-008 实证:Text-only 扫描漏掉隐私页同意按钮)
65
+ if d.find_component(BY.text(t)) is not None:
66
+ d.touch(BY.text(t))
67
+ dismissed.append(t)
68
+ hit_any = True
69
+ time.sleep(1.2)
70
+ break # 点一个后重新扫(弹窗可能叠加/消失后露下一个)
71
+ except Exception:
72
+ pass
73
+ if hit_any:
74
+ continue
75
+ pts = _page_texts(d)
76
+ # 3) 无弹窗特征:tab 归位(点目标 tab,锚验证)
77
+ if home_tab and home_tab in pts:
78
+ if anchor_key:
79
+ try:
80
+ if d.find_component(BY.key(anchor_key)) is not None:
81
+ return True
82
+ except Exception:
83
+ pass
84
+ try:
85
+ d.touch(BY.text(home_tab))
86
+ time.sleep(2.0)
87
+ except Exception:
88
+ pass
89
+ time.sleep(0.9)
90
+ # 超时:输出页面快照供分诊
91
+ log(f" [L0] ✗ bootstrap 超时 {total}s;页面文本快照: {_page_texts(d)[:15]}")
92
+ return False
93
+
94
+
95
+ # ── Level 0 前置三件套(TD-007:冷启自适应,用例不得自带)────────────────
96
+ # ① 首启弹窗 dismiss:多文案轮询(隐私/权限/引导),幂等无弹窗秒过
97
+ FIRST_RUN_DISMISS_TEXTS = ["同意并继续", "同意", "允许", "仅在使用中允许", "我知道了", "知道了", "暂不", "以后再说"]
98
+
99
+ def dismiss_first_run(d, rounds=3, gap=1.2):
100
+ for _ in range(rounds):
101
+ hit = False
102
+ for t in FIRST_RUN_DISMISS_TEXTS:
103
+ try:
104
+ c = d.find_component(BY.text(t))
105
+ if c is not None:
106
+ d.touch(BY.text(t)); time.sleep(gap); hit = True; break
107
+ except Exception:
108
+ pass
109
+ if not hit:
110
+ return
111
+
112
+ # ② 首页 tab 归位:应用默认落在非目标 tab(如广场)时点回目标 tab(HOME_TAB 应用级配置)
113
+ def ensure_home_tab(d, tab_text, timeout=10):
114
+ t0 = time.time()
115
+ while time.time() - t0 < timeout:
116
+ try:
117
+ if d.find_component(BY.text(tab_text)) is not None:
118
+ d.touch(BY.text(tab_text)); time.sleep(1.5); return True
119
+ except Exception:
120
+ pass
121
+ time.sleep(0.8)
122
+ return False
123
+
124
+ # ③ 冷启三件套:launch 内自动执行——弹窗 dismiss + tab 归位(HOME_TAB 非空时)
125
+ HOME_TAB = "" # 应用级:主功能 tab 文案(如 "玩安卓");空串跳过归位
126
+
127
+
128
+ def launch(d, want="", reset_data=False):
129
+ """v3-①②(EXEC-034):want=启动参数('k=v;k2=v2' → hdc aa start --ps 透传,启动参数门控应用免人工补丁);
130
+ reset_data=True → bm clean -n 清应用数据(用例级状态重置,防跨用例泄漏)。"""
131
+ import os as _os, subprocess as _sp
132
+ d.stop_app(PACKAGE)
133
+ time.sleep(0.8)
134
+ if reset_data:
135
+ hdc = _os.environ.get("FORGE_HDC", "hdc")
136
+ dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
137
+ try:
138
+ # DEVIATION-3: bm clean 不带 -d 时仅清 cache(-c),持久化不清,用例间状态泄漏
139
+ # (TC02 关 answer 残留致 TC03/08/09 连锁失败);本机 bm 无 -b 选项,-d 清数据
140
+ _sp.run([hdc, "-t", dev, "shell", "bm", "clean", "-n", PACKAGE, "-d"],
141
+ capture_output=True, timeout=15)
142
+ time.sleep(0.5)
143
+ except Exception as e:
144
+ print(" [reset] bm clean 失败:", e)
145
+ if want:
146
+ hdc = _os.environ.get("FORGE_HDC", "hdc")
147
+ dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
148
+ args = ["aa", "start", "-b", PACKAGE, "-a", ABILITY]
149
+ for kv in want.split(";"):
150
+ if "=" in kv:
151
+ k, v = kv.split("=", 1)
152
+ # DEVIATION-1: aa start 的 --ps 只接受空格分隔 key value,等号形式报
153
+ # "invalid number of parameters for option --ps" 导致带参启动全部失败
154
+ args += ["--ps", k, v]
155
+ # EXEC-038 修复:带参启动 3 轮重试 + 实测探活(FORGE_WANT_PROBE=key,可寻才算活)
156
+ probe = _os.environ.get("FORGE_WANT_PROBE", "")
157
+ for attempt in range(3):
158
+ try:
159
+ _sp.run([hdc, "-t", dev, "shell", "aa", "force-stop", PACKAGE], capture_output=True, timeout=10)
160
+ time.sleep(0.8)
161
+ _sp.run([hdc, "-t", dev, "shell"] + args, capture_output=True, timeout=15)
162
+ time.sleep(3.0)
163
+ if probe:
164
+ c = None
165
+ try:
166
+ c = d.find_component(BY.key(probe))
167
+ except Exception:
168
+ pass
169
+ if c is None:
170
+ print(f" [want] 第{attempt+1}轮探活失败(key={probe}),重试")
171
+ time.sleep(1.5)
172
+ continue
173
+ _dismiss_first_run(d)
174
+ return
175
+ except Exception as e:
176
+ print(" [want] 带参启动异常,重试:", e)
177
+ time.sleep(1.5)
178
+ print(" [want] 3 轮未探活成功,继续交由断言暴露")
179
+ d.start_app(PACKAGE, ABILITY)
180
+ ok = bootstrap(d, home_tab=HOME_TAB, anchor_key=HOME_ANCHOR)
181
+ if not ok:
182
+ raise AssertionError("L0 bootstrap failed(弹窗/tab 未消化,见日志快照)")
183
+ time.sleep(2.5)
184
+ _dismiss_first_run(d)
185
+
186
+
187
+ def _dismiss_first_run(d):
188
+ """首启弹窗一键处置(权限/隐私协议)"""
189
+ for txt in ["同意并继续", "允许", "我知道了", "暂不", "以后再说", "同意"]:
190
+ try:
191
+ c = d.find_component(BY.text(txt))
192
+ if c:
193
+ c.click()
194
+ time.sleep(0.8)
195
+ except Exception:
196
+ pass
197
+
198
+
199
+ def stop(d):
200
+ d.stop_app(PACKAGE)
201
+
202
+
203
+ def nav_to(d, page):
204
+ """导航到目标页:入口页直接用;非入口页用返回键回根后再走用例自带链(由用例 steps 驱动)"""
205
+ if page == ENTRY_PAGE:
206
+ return
207
+ # 返回根页(最多 5 次)
208
+ for _ in range(5):
209
+ try:
210
+ if d.find_component(BY.key("__never__")) is None:
211
+ pass
212
+ except Exception:
213
+ pass
214
+ d.press_back()
215
+ time.sleep(0.4)
216
+
217
+
218
+ TAB_LABELS = ["首页", "视频", "我的"]
219
+
220
+
221
+ def click_tab(d, label):
222
+ """底部 Tab 切换:定位文本→bounds 中心 touch(比例坐标兜底)"""
223
+ import time as _t
224
+ for _ in range(8):
225
+ try:
226
+ comps = d.find_all_components(BY.text(label))
227
+ if comps:
228
+ c = comps[-1] # 底部 tab 是同名文本中最靠下的
229
+ b = c.getBounds()
230
+ cx = (b.left + b.right) // 2
231
+ cy = (b.top + b.bottom) // 2
232
+ d.touch((int(cx), int(cy)))
233
+ _t.sleep(1.2)
234
+ return
235
+ except Exception:
236
+ pass
237
+ _t.sleep(0.5)
238
+ idx = TAB_LABELS.index(label) if label in TAB_LABELS else 2
239
+ w, h = d.get_display_size()
240
+ d.touch((int(w * (idx + 0.5) / len(TAB_LABELS)), int(h * 0.945)))
241
+ time.sleep(1.2)
242
+
243
+
244
+ def click_text(d, txt, wait=6):
245
+ """按可见文本点击(tab 名/中文按钮等无 ID 控件)。
246
+ 先试 BY.key——无下划线的单词 id(如 fab)无法与文本区分(_is_id_like 局限),此处兜底。"""
247
+ import time as _t
248
+ try:
249
+ c = d.find_component(BY.key(txt))
250
+ if c:
251
+ c.click()
252
+ _t.sleep(1.0)
253
+ return
254
+ except Exception:
255
+ pass
256
+ deadline = _t.time() + wait
257
+ while _t.time() < deadline:
258
+ try:
259
+ c = d.find_component(BY.text(txt, MatchPattern.CONTAINS))
260
+ if c:
261
+ c.click()
262
+ _t.sleep(1.0)
263
+ return
264
+ except Exception:
265
+ pass
266
+ _t.sleep(0.5)
267
+ raise AssertionError("text not clickable: " + txt)
268
+
269
+
270
+ def click_type_first(d, ctype, wait=6):
271
+ """点击指定类型的第一个组件(lineN:Type 引用的兜底点击)"""
272
+ import time as _t
273
+ deadline = _t.time() + wait
274
+ while _t.time() < deadline:
275
+ try:
276
+ comps = d.find_all_components(BY.type(ctype))
277
+ if comps:
278
+ b = comps[0].getBounds()
279
+ d.touch(((b.left + b.right) // 2, (b.top + b.bottom) // 2))
280
+ _t.sleep(1.2)
281
+ return
282
+ except Exception:
283
+ pass
284
+ _t.sleep(0.5)
285
+ raise AssertionError("type not clickable: " + ctype)
286
+
287
+
288
+ def id_click(d, cid, wait=6):
289
+ comp = _find(d, cid, wait)
290
+ comp.click()
291
+ time.sleep(1.0)
292
+
293
+
294
+ def id_input(d, cid, text, wait=6):
295
+ comp = _find(d, cid, wait)
296
+ d.input_text(comp, text)
297
+ time.sleep(0.6)
298
+
299
+
300
+ def _find(d, cid, wait=6):
301
+ import time as _t
302
+ deadline = _t.time() + wait
303
+ while _t.time() < deadline:
304
+ try:
305
+ c = d.find_component(BY.key(cid))
306
+ if c:
307
+ return c
308
+ except Exception:
309
+ pass
310
+ try:
311
+ c = d.find_component(BY.id(cid))
312
+ if c:
313
+ return c
314
+ except Exception:
315
+ pass
316
+ _t.sleep(0.5)
317
+ raise AssertionError("component not found: " + cid)
318
+
319
+
320
+ def smart_click(d, cid, txt=None, wait=6):
321
+ """key 优先点击;失败且有 txt 时文本点击(mine 类 HAR 模块 ID 丢失边界)"""
322
+ import time as _t
323
+ deadline = _t.time() + wait
324
+ while _t.time() < deadline:
325
+ try:
326
+ c = d.find_component(BY.key(cid))
327
+ if c:
328
+ c.click()
329
+ _t.sleep(1.0)
330
+ return
331
+ except Exception:
332
+ pass
333
+ _t.sleep(0.5)
334
+ if txt and txt.startswith("line"):
335
+ import re as _re
336
+ m = _re.match(r"line\d+:(\w+)", txt)
337
+ if m:
338
+ click_type_first(d, m.group(1), wait)
339
+ return
340
+ if txt:
341
+ click_text(d, txt, wait)
342
+ return
343
+ raise AssertionError("component not found: " + cid)
344
+
345
+
346
+ def scroll_text_exist(d, txt, wait=6):
347
+ """滚动查找文本,双向(EXEC-005):上滑 N 次找 → 未中则下滑回落 N 次找。
348
+ 单向滚动在连续断言场景必挂:上一断言滑到底后,本断言目标在上方,单向永远回不去。"""
349
+ import time as _t
350
+ w, h = d.get_display_size()
351
+ def _check():
352
+ try:
353
+ return d.find_component(BY.text(txt, MatchPattern.CONTAINS)) is not None
354
+ except Exception:
355
+ return False
356
+ for direction in (UiParam.UP, UiParam.DOWN):
357
+ for i in range(6):
358
+ if _check():
359
+ return True
360
+ d.swipe(direction, distance=40)
361
+ _t.sleep(1.0)
362
+ return text_exist(d, txt, wait)
363
+
364
+
365
+ def smart_check(d, cid, txt=None, wait=6):
366
+ """key 优先断言;失败且有 txt 时降级文本断言(部分 HAR 模块 .id 运行时丢弃的边界)"""
367
+ import time as _t
368
+ deadline = _t.time() + wait
369
+ while _t.time() < deadline:
370
+ try:
371
+ c = d.find_component(BY.key(cid))
372
+ if c:
373
+ return True
374
+ except Exception:
375
+ pass
376
+ _t.sleep(0.5)
377
+ if txt and txt.startswith("line"):
378
+ import re as _re
379
+ m = _re.match(r"line\d+:(\w+)", txt)
380
+ if m:
381
+ d.check_component_exist(BY.type(m.group(1)), wait_time=wait)
382
+ return True
383
+ if txt:
384
+ return text_exist(d, txt, wait)
385
+ raise AssertionError("component not found: " + cid)
386
+
387
+
388
+ def text_equals(d, txt, wait=6):
389
+ """精确文本断言(EQUALS)——CONTAINS 会被前缀污染(如 ❄️Gallery 含 Gallery),
390
+ 恢复/状态标记类断言必须用本原语。存在且精确相等返回 True,否则抛。"""
391
+ import time as _t
392
+ deadline = _t.time() + wait
393
+ while _t.time() < deadline:
394
+ try:
395
+ c = d.find_component(BY.text(txt))
396
+ if c:
397
+ return True
398
+ except Exception:
399
+ pass
400
+ _t.sleep(0.5)
401
+ raise AssertionError("text (exact) not found: " + txt)
402
+
403
+
404
+ def text_absent(d, txt, wait=3):
405
+ """文本不存在断言(反向;等待短暂稳定后确认消失)"""
406
+ import time as _t
407
+ _t.sleep(1.0)
408
+ try:
409
+ c = d.find_component(BY.text(txt, MatchPattern.CONTAINS))
410
+ if c:
411
+ raise AssertionError("text should be absent but found: " + txt)
412
+ except AssertionError:
413
+ raise
414
+ except Exception:
415
+ pass
416
+ return True
417
+
418
+
419
+ def step_shot(d, label="step"):
420
+ """分步截图:一个动作/断言一张页面图(run_direct 设 FORGE_SHOT_DIR/FORGE_CASE 时启用)。
421
+ 文件名 <case>_<序号>_<label>.jpeg,供 test-report.html 按步回放页面。"""
422
+ import os as _os
423
+ base = _os.environ.get("FORGE_SHOT_DIR")
424
+ case = _os.environ.get("FORGE_CASE", "case")
425
+ if not base:
426
+ return
427
+ n = step_shot.n.get(case, 0) + 1
428
+ try:
429
+ d.screenshot(_os.path.join(base, "%s_%02d_%s.jpeg" % (case, n, label)))
430
+ step_shot.n[case] = n
431
+ except Exception:
432
+ pass
433
+
434
+
435
+ step_shot.n = {}
436
+
437
+
438
+ def wait_for(d, by, timeout=10, interval=0.5):
439
+ """轮询等待控件出现(首屏锚点等待用);返回组件或 None,不抛异常"""
440
+ import time as _t
441
+ end = _t.time() + timeout
442
+ while _t.time() < end:
443
+ try:
444
+ c = d.find_component(by)
445
+ if c:
446
+ return c
447
+ except Exception:
448
+ pass
449
+ _t.sleep(interval)
450
+ return None
451
+
452
+
453
+ def text_exist(d, txt, wait=6):
454
+ """文本存在断言(BY.text CONTAINS)。
455
+ 契约:存在返回 True;不存在【抛 AssertionError】——永不返回 False。
456
+ run_direct/analyze_results 依赖异常文本做 FAIL 判定与失败聚类,勿改为静默返回。"""
457
+ import time as _t
458
+ deadline = _t.time() + wait
459
+ while _t.time() < deadline:
460
+ try:
461
+ c = d.find_component(BY.text(txt, MatchPattern.CONTAINS))
462
+ if c:
463
+ return True
464
+ except Exception:
465
+ pass
466
+ _t.sleep(0.5)
467
+ raise AssertionError("text not found: " + txt)
468
+
469
+
470
+ def text_value_equals(d, cid, expected, wait=6):
471
+ """值断言(EXEC-040 实证+反证):getText() 精确等于期望——锚定 key 控件动态内容。"""
472
+ import time as _t
473
+ deadline = _t.time() + wait
474
+ while _t.time() < deadline:
475
+ try:
476
+ c = d.find_component(BY.key(cid))
477
+ if c and c.getText() == expected:
478
+ return True
479
+ except Exception:
480
+ pass
481
+ _t.sleep(0.5)
482
+ got = None
483
+ try:
484
+ c = d.find_component(BY.key(cid))
485
+ got = c.getText() if c else '(not found)'
486
+ except Exception:
487
+ pass
488
+ raise AssertionError(f"text value mismatch: expect {expected!r} got {got!r} (key={cid})")
489
+
490
+
491
+ def text_value_contains(d, cid, part, wait=6):
492
+ """值包含断言:getText() 包含子串(动态文案前缀不定时用)。"""
493
+ import time as _t
494
+ deadline = _t.time() + wait
495
+ while _t.time() < deadline:
496
+ try:
497
+ c = d.find_component(BY.key(cid))
498
+ if c and part in (c.getText() or ''):
499
+ return True
500
+ except Exception:
501
+ pass
502
+ _t.sleep(0.5)
503
+ raise AssertionError(f"text value not containing {part!r} (key={cid})")
504
+
505
+
506
+ def count_equals(d, target, expected, wait=6):
507
+ """计数断言(EXEC-040 实证):key 或 CONTAINS 文本匹配的组件数==期望。"""
508
+ import time as _t, re as _re
509
+ deadline = _t.time() + wait
510
+ last = -1
511
+ while _t.time() < deadline:
512
+ try:
513
+ if _re.match(r'^[a-z][a-z0-9_]+$', target):
514
+ comps = d.find_all_components(BY.key(target))
515
+ else:
516
+ comps = d.find_all_components(BY.text(target, MatchPattern.CONTAINS))
517
+ last = len(comps) if comps else 0
518
+ if last == expected:
519
+ return True
520
+ except Exception:
521
+ pass
522
+ _t.sleep(0.5)
523
+ raise AssertionError(f"count mismatch: expect {expected} got {last} (target={target})")
524
+
525
+
526
+ def is_below(d, cid, ref_cid, wait=6):
527
+ """几何断言(EXEC-040 实证):cid 的 top >= ref_cid 的 bottom(垂直次序)。"""
528
+ import time as _t
529
+ deadline = _t.time() + wait
530
+ while _t.time() < deadline:
531
+ try:
532
+ a = d.find_component(BY.key(cid)); b = d.find_component(BY.key(ref_cid))
533
+ if a and b and a.getBounds().top >= b.getBounds().bottom:
534
+ return True
535
+ except Exception:
536
+ pass
537
+ _t.sleep(0.5)
538
+ raise AssertionError(f"geometry: {cid} not below {ref_cid}")
539
+
540
+
541
+ def toast_exist(d, txt, wait=4, poll=0.5):
542
+ """Toast 断言(v3.1):轮询 hilog 捕获 Toast 文本。需 FORGE_HDC/FORGE_DEVICE。"""
543
+ import os as _os, subprocess as _sp, time as _tt
544
+ hdc = _os.environ.get("FORGE_HDC", "hdc"); dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
545
+ deadline = _tt.time() + wait
546
+ while _tt.time() < deadline:
547
+ try:
548
+ r = _sp.run([hdc, "-t", dev, "shell", "hilog", "-x"],
549
+ capture_output=True, timeout=8, text=True, errors="ignore")
550
+ if txt in (r.stdout or ""):
551
+ return True
552
+ except Exception:
553
+ pass
554
+ _tt.sleep(poll)
555
+ raise AssertionError("toast not found in hilog: " + txt)
556
+
557
+
558
+ # ── 强断言原语(2026-09-29 吸收自 none 臂实证:无 skill 的 LLM 凭通用能力即达成 34% 强断言,
559
+ # B 线无理由低于此——Toast/系统事件/数据断言按下表选型,禁止只写 exist)──
560
+ def _hdc(args, timeout=20):
561
+ import subprocess as _sp, os as _os
562
+ dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
563
+ cmd = [HDC_BIN, "-t", dev] + args
564
+ r = _sp.run(cmd, capture_output=True, timeout=timeout)
565
+ return (r.stdout + r.stderr).decode("utf-8", "ignore")
566
+
567
+
568
+ def _dump_layout(timeout=20):
569
+ """取当前全量布局 JSON(含系统对话框/浮层),返回原始文本(Round-2:确保本地 tmp 目录存在)"""
570
+ import json as _json, os as _os
571
+ _os.makedirs("./tmp", exist_ok=True)
572
+ _hdc(["shell", "uitest", "dumpLayout", "-p", "/data/local/tmp/forge_dump.json"], timeout=timeout)
573
+ _hdc(["file", "recv", "/data/local/tmp/forge_dump.json", "./tmp/forge_dump.json"], timeout=timeout)
574
+ try:
575
+ return open("./tmp/forge_dump.json", encoding="utf-8").read()
576
+ except Exception:
577
+ return ""
578
+
579
+
580
+ def hilog_clear():
581
+ """清 hilog 缓冲,防止上一用例残留关键字造成假 PASS(tag=RaiseToAnswer)"""
582
+ _hdc(["shell", "hilog", "-r"])
583
+ time.sleep(0.3)
584
+
585
+
586
+ HILOG_TAG = 'JSAPP'
587
+
588
+
589
+ def hilog_tail(tag=None, n=400):
590
+ tag = tag or HILOG_TAG
591
+ """读取 tag 过滤的最近 n 行 hilog(诊断用)"""
592
+ import subprocess as _sp, os as _os
593
+ dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
594
+ r = _sp.run([HDC_BIN, "-t", dev, "shell", "hilog", "-z", str(n), "-T", tag],
595
+ capture_output=True, timeout=15)
596
+ return r.stdout.decode("utf-8", "ignore")
597
+
598
+
599
+ def toast_text_seen(d, txt, timeout=6):
600
+ """Toast 文本断言(dump 版)。无 ID 例外:Toast 为瞬时浮层无稳定 ID,且 hypium find_component
601
+ 实测看不到 Toast 子窗口,故以 uitest dumpLayout 原文轮询(首轮实测教训 Round-1)。"""
602
+ deadline = time.time() + timeout
603
+ while time.time() < deadline:
604
+ raw = _dump_layout()
605
+ if txt in raw:
606
+ return True
607
+ time.sleep(0.6)
608
+ raise AssertionError("toast not seen: " + txt)
609
+
610
+
611
+ def bm_clean():
612
+ """清应用持久化数据(公开包装;import * 不导出 _ 前缀——Round-1 教训)"""
613
+ _hdc(["shell", "bm", "clean", "-n", PACKAGE, "-d"])
614
+ time.sleep(0.6)