@buaa_smat/hometrans 0.1.0 → 0.1.2

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 (136) hide show
  1. package/README.md +141 -124
  2. package/agents/build-fixer.md +1 -0
  3. package/agents/code-review-fix.md +1 -0
  4. package/agents/code-reviewer.md +1 -0
  5. package/agents/logic-coding.md +1 -0
  6. package/agents/logic-context-builder.md +1 -0
  7. package/agents/review-fixer.md +1 -0
  8. package/agents/self-test-fixer.md +1 -0
  9. package/agents/self-tester.md +260 -233
  10. package/agents/spec-generator.md +1 -0
  11. package/agents/test-tools/autotest/README.md +223 -0
  12. package/agents/test-tools/autotest/config.yaml.example +58 -0
  13. package/agents/test-tools/autotest/pyproject.toml +16 -0
  14. package/agents/test-tools/autotest/report_tool.py +759 -0
  15. package/agents/test-tools/autotest/self_test_runner.py +773 -0
  16. package/agents/test-tools/autotest/testcases_schema.md +143 -0
  17. package/agents/test-tools/autotest/testcases_tool.py +215 -0
  18. package/agents/test-tools/autotest/uv.lock +3156 -0
  19. package/agents/test-tools/harmony_autotest-0.1.0-py3-none-any.whl +0 -0
  20. package/agents/test-tools/hypium-6.1.0.210-py3-none-any.whl +0 -0
  21. package/agents/test-tools/hypium_mcp-0.6.5-py3-none-any.whl +0 -0
  22. package/agents/test-tools/xdevice-6.1.0.210-py3-none-any.whl +0 -0
  23. package/agents/test-tools/xdevice_devicetest-6.1.0.210-py3-none-any.whl +0 -0
  24. package/agents/test-tools/xdevice_ohos-6.1.0.210-py3-none-any.whl +0 -0
  25. package/dist/cli/config-store.js +27 -2
  26. package/dist/cli/config.js +17 -6
  27. package/dist/cli/index.js +3 -2
  28. package/dist/cli/init.js +135 -22
  29. package/dist/cli/mcp.js +2 -2
  30. package/dist/context/index.js +165 -69
  31. package/package.json +59 -60
  32. package/skills/code-dev-review-fix/SKILL.md +279 -0
  33. package/skills/code-dev-review-fix-workspace/evals/evals.json +56 -0
  34. package/skills/code-dev-review-fix-workspace/iteration-1/routing-results.md +23 -0
  35. package/skills/convert_pipeline/SKILL.md +423 -439
  36. package/skills/hmos-resources-convert/SKILL.md +623 -0
  37. package/skills/hmos-resources-convert/evals/evals.json +171 -0
  38. package/skills/hmos-resources-convert/references/conversion-rules.md +663 -0
  39. package/skills/hmos-resources-convert/references/dependency-analysis-rules.md +388 -0
  40. package/skills/hmos-resources-convert/references/resource-mapping-rules.md +457 -0
  41. package/skills/hmos-resources-convert/references/xml-drawable-to-svg-rules.md +513 -0
  42. package/skills/hmos-resources-convert/template/AppScope/app.json5 +10 -0
  43. package/skills/hmos-resources-convert/template/AppScope/resources/base/element/string.json +8 -0
  44. package/skills/hmos-resources-convert/template/AppScope/resources/base/media/background.png +0 -0
  45. package/skills/hmos-resources-convert/template/AppScope/resources/base/media/foreground.png +0 -0
  46. package/skills/hmos-resources-convert/template/AppScope/resources/base/media/layered_image.json +7 -0
  47. package/skills/hmos-resources-convert/template/build-profile.json5 +42 -0
  48. package/skills/hmos-resources-convert/template/code-linter.json5 +32 -0
  49. package/skills/hmos-resources-convert/template/entry/build-profile.json5 +33 -0
  50. package/skills/hmos-resources-convert/template/entry/hvigorfile.ts +6 -0
  51. package/skills/hmos-resources-convert/template/entry/obfuscation-rules.txt +23 -0
  52. package/skills/hmos-resources-convert/template/entry/oh-package.json5 +10 -0
  53. package/skills/hmos-resources-convert/template/entry/src/main/ets/entryability/EntryAbility.ets +48 -0
  54. package/skills/hmos-resources-convert/template/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +16 -0
  55. package/skills/hmos-resources-convert/template/entry/src/main/ets/pages/Index.ets +23 -0
  56. package/skills/hmos-resources-convert/template/entry/src/main/module.json5 +55 -0
  57. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/color.json +8 -0
  58. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/float.json +8 -0
  59. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/string.json +16 -0
  60. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/background.png +0 -0
  61. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/foreground.png +0 -0
  62. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/layered_image.json +7 -0
  63. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/startIcon.png +0 -0
  64. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/backup_config.json +3 -0
  65. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/main_pages.json +5 -0
  66. package/skills/hmos-resources-convert/template/entry/src/main/resources/dark/element/color.json +8 -0
  67. package/skills/hmos-resources-convert/template/entry/src/mock/mock-config.json5 +2 -0
  68. package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/Ability.test.ets +35 -0
  69. package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/List.test.ets +5 -0
  70. package/skills/hmos-resources-convert/template/entry/src/ohosTest/module.json5 +16 -0
  71. package/skills/hmos-resources-convert/template/entry/src/test/List.test.ets +5 -0
  72. package/skills/hmos-resources-convert/template/entry/src/test/LocalUnit.test.ets +33 -0
  73. package/skills/hmos-resources-convert/template/hvigor/hvigor-config.json5 +23 -0
  74. package/skills/hmos-resources-convert/template/hvigorfile.ts +6 -0
  75. package/skills/hmos-resources-convert/template/oh-package-lock.json5 +28 -0
  76. package/skills/hmos-resources-convert/template/oh-package.json5 +10 -0
  77. package/skills/hmos-resources-convert/tools/apktool.bat +85 -0
  78. package/skills/hmos-resources-convert/tools/apktool_3.0.1.jar +0 -0
  79. package/skills/hmos-ui-align/SKILL.md +182 -0
  80. package/skills/hmos-ui-align/config-example.json +11 -0
  81. package/skills/hmos-ui-align/config.json +11 -0
  82. package/skills/hmos-ui-align/diff_analysis.md +53 -0
  83. package/skills/hmos-ui-align/page_align.md +62 -0
  84. package/skills/hmos-ui-align/readme.md +231 -0
  85. package/skills/hmos-ui-align/references/Comparison_Template.md +2 -0
  86. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@Link/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/217/214/345/220/221/345/220/214/346/255/245.md +648 -0
  87. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@Observed/350/243/205/351/245/260/345/231/250/345/222/214@ObjectLink/350/243/205/351/245/260/345/231/250/357/274/232/345/265/214/345/245/227/347/261/273/345/257/271/350/261/241/345/261/236/346/200/247/345/217/230/345/214/226.md +2089 -0
  88. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@Prop/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/215/225/345/220/221/345/220/214/346/255/245.md +1033 -0
  89. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@Provide/350/243/205/351/245/260/345/231/250/345/222/214@Consume/350/243/205/351/245/260/345/231/250/357/274/232/344/270/216/345/220/216/344/273/243/347/273/204/344/273/266/345/217/214/345/220/221/345/220/214/346/255/245.md +1183 -0
  90. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@State/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/347/212/266/346/200/201.md +576 -0
  91. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@Track/350/243/205/351/245/260/345/231/250/357/274/232class/345/257/271/350/261/241/345/261/236/346/200/247/347/272/247/346/233/264/346/226/260.md +297 -0
  92. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@Watch/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/346/233/264/346/224/271/351/200/232/347/237/245.md +395 -0
  93. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/AppStorage/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200/347/232/204UI/347/212/266/346/200/201/345/255/230/345/202/250.md +903 -0
  94. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/Environment/357/274/232/350/256/276/345/244/207/347/216/257/345/242/203/346/237/245/350/257/242.md +106 -0
  95. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/LocalStorage/357/274/232/351/241/265/351/235/242/347/272/247UI/347/212/266/346/200/201/345/255/230/345/202/250.md +1178 -0
  96. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/MVVM/346/250/241/345/274/217V1.md +911 -0
  97. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md +911 -0
  98. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/PersistentStorage/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md +355 -0
  99. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243//347/256/241/347/220/206/345/272/224/347/224/250/346/213/245/346/234/211/347/232/204/347/212/266/346/200/201/346/246/202/350/277/260.md +11 -0
  100. package/skills/hmos-ui-align/references/UI_Analysis_Template.md +4 -0
  101. package/skills/hmos-ui-align/references/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +2535 -0
  102. package/skills/hmos-ui-align/references/android-to-harmonyOS-ui-interaction-mapping-reference.md +555 -0
  103. package/skills/hmos-ui-align/references/android-to-harmonyOS-ui-layout-mapping-reference.md +117 -0
  104. package/skills/hmos-ui-align/scripts/app_feature_verify.py +443 -0
  105. package/skills/hmos-ui-align/scripts/navigation-capure.md +37 -0
  106. package/skills/hmos-ui-align/scripts/page_capture.py +592 -0
  107. package/skills/hmos-ui-align-batch/SKILL.md +99 -0
  108. package/skills/hmos-ui-align-batch/references/conversion-procedure.md +180 -0
  109. package/skills/hmos-ui-align-batch/references/mappings/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +2535 -0
  110. package/skills/hmos-ui-align-batch/references/mappings/android-to-harmonyOS-ui-interaction-mapping-reference.md +555 -0
  111. package/skills/hmos-ui-align-batch/references/mappings/android-to-harmonyOS-ui-layout-mapping-reference.md +117 -0
  112. package/skills/hmos-ui-align-batch/references/mvvm/@Link/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/217/214/345/220/221/345/220/214/346/255/245.md +648 -0
  113. package/skills/hmos-ui-align-batch/references/mvvm/@Observed/350/243/205/351/245/260/345/231/250/345/222/214@ObjectLink/350/243/205/351/245/260/345/231/250/357/274/232/345/265/214/345/245/227/347/261/273/345/257/271/350/261/241/345/261/236/346/200/247/345/217/230/345/214/226.md +2089 -0
  114. package/skills/hmos-ui-align-batch/references/mvvm/@Prop/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/215/225/345/220/221/345/220/214/346/255/245.md +1033 -0
  115. package/skills/hmos-ui-align-batch/references/mvvm/@Provide/350/243/205/351/245/260/345/231/250/345/222/214@Consume/350/243/205/351/245/260/345/231/250/357/274/232/344/270/216/345/220/216/344/273/243/347/273/204/344/273/266/345/217/214/345/220/221/345/220/214/346/255/245.md +1183 -0
  116. package/skills/hmos-ui-align-batch/references/mvvm/@State/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/347/212/266/346/200/201.md +576 -0
  117. package/skills/hmos-ui-align-batch/references/mvvm/@Track/350/243/205/351/245/260/345/231/250/357/274/232class/345/257/271/350/261/241/345/261/236/346/200/247/347/272/247/346/233/264/346/226/260.md +297 -0
  118. package/skills/hmos-ui-align-batch/references/mvvm/@Watch/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/346/233/264/346/224/271/351/200/232/347/237/245.md +395 -0
  119. package/skills/hmos-ui-align-batch/references/mvvm/AppStorage/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200/347/232/204UI/347/212/266/346/200/201/345/255/230/345/202/250.md +903 -0
  120. package/skills/hmos-ui-align-batch/references/mvvm/Environment/357/274/232/350/256/276/345/244/207/347/216/257/345/242/203/346/237/245/350/257/242.md +106 -0
  121. package/skills/hmos-ui-align-batch/references/mvvm/LocalStorage/357/274/232/351/241/265/351/235/242/347/272/247UI/347/212/266/346/200/201/345/255/230/345/202/250.md +1178 -0
  122. package/skills/hmos-ui-align-batch/references/mvvm/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md +911 -0
  123. package/skills/hmos-ui-align-batch/references/mvvm/PersistentStorage/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md +355 -0
  124. package/skills/hmos-ui-align-batch/references/mvvm//347/256/241/347/220/206/345/272/224/347/224/250/346/213/245/346/234/211/347/232/204/347/212/266/346/200/201/346/246/202/350/277/260.md +11 -0
  125. package/skills/hmos-ui-align-batch/scripts/android_parse_fast.py +1606 -0
  126. package/skills/self-test/SKILL.md +369 -0
  127. package/skills/self-test/readme.md +309 -0
  128. package/skills/spec-generator-skill/SKILL.md +332 -0
  129. package/skills/spec-generator-skill/references/android-platform-tokens.md +105 -0
  130. package/skills/spec-generator-skill/references/spec-sample-1.md +78 -0
  131. package/skills/spec-generator-skill/references/spec-sample-2.md +58 -0
  132. package/skills/spec-generator-skill/references/spec-sample-3.md +116 -0
  133. package/skills/spec-generator-skill/references/step4-report-template.md +33 -0
  134. package/agents/self-test-setup.md +0 -165
  135. package/dist/context/resources/sdkConfig.json +0 -24
  136. package/src/context/resources/sdkConfig.json +0 -24
@@ -0,0 +1,773 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 自测执行器 - 统一的进程管理和测试执行
4
+
5
+ 子命令:
6
+ run - 准备环境后后台启动 AutoTest.batch
7
+ status - 查询后台执行状态(RUNNING/COMPLETED/CRASHED)
8
+ kill - 终止后台进程
9
+ clean - 重置本目录状态(.venv / 残留中间产物 / 残留进程),不动全局 uv 缓存
10
+
11
+ 使用示例:
12
+ python self_test_runner.py run --testcases testcases.json --hap app.hap --bundle-name com.xxx
13
+ python self_test_runner.py run --testcases testcases.jsonl --hap app.hap --bundle-name com.xxx
14
+ python self_test_runner.py status --task-dir task
15
+ python self_test_runner.py kill --task-dir task
16
+ python self_test_runner.py clean
17
+
18
+ `--testcases` 同时接受 JSON 数组(旧 `testcases.json`)与 JSONL,JSON 数组会自动转换成 JSONL。
19
+
20
+ 执行流程:
21
+ 主进程:清理 + 校验 config.yaml + uv sync + hdc install hap(等待完成)
22
+
23
+ 后台:python -m AutoTest.batch(循环执行 testcases.jsonl,cwd 为 task_<timestamp> 子目录)
24
+
25
+ 依赖:harmony-autotest 与 hypium_mcp 均在 pyproject.toml 中声明为远程 whl URL
26
+ 依赖;`uv sync` 会拉取到本目录 .venv 内,不再需要手动 pip install。
27
+
28
+ AutoTest.batch 启动时 cwd 设为 task_<timestamp>,因此 hypium_mcp.log / tmp_hypium/
29
+ 等中间产物会落在 task 子目录内,避免污染插件源码目录。
30
+ """
31
+
32
+ import argparse
33
+ import glob
34
+ import hashlib
35
+ import io
36
+ import json
37
+ import logging
38
+ import os
39
+ import shutil
40
+ import subprocess
41
+ import sys
42
+ import time
43
+ import traceback
44
+ from datetime import datetime
45
+ from pathlib import Path
46
+
47
+ SCRIPT_DIR = Path(__file__).resolve().parent
48
+ HAP_INSTALL_WAIT = 10
49
+ INSTALL_TIMEOUT = 120
50
+ UV_TIMEOUT = 600
51
+ API_KEY_PLACEHOLDER = "YOUR_API_KEY_HERE"
52
+ DEFAULT_CATEGORY = "self_test"
53
+
54
+ logger = logging.getLogger("self_test_runner")
55
+
56
+
57
+ # =============================================================================
58
+ # 公共函数:进程管理
59
+ # =============================================================================
60
+
61
+ def _get_python_processes():
62
+ if sys.platform != "win32":
63
+ return []
64
+
65
+ ps_cmd = (
66
+ 'Get-CimInstance Win32_Process -Filter "Name=\'python.exe\'" '
67
+ '| Select-Object ProcessId,CommandLine '
68
+ '| ForEach-Object { "$($_.ProcessId)|$($_.CommandLine)" }'
69
+ )
70
+ try:
71
+ result = subprocess.run(
72
+ ["powershell.exe", "-NoProfile", "-Command", ps_cmd],
73
+ capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=15,
74
+ )
75
+ except Exception:
76
+ return []
77
+
78
+ if not result.stdout:
79
+ return []
80
+
81
+ processes = []
82
+ for line in result.stdout.splitlines():
83
+ line = line.strip()
84
+ if not line or "|" not in line:
85
+ continue
86
+ pid_str, cmdline = line.split("|", 1)
87
+ try:
88
+ pid = int(pid_str.strip())
89
+ processes.append((pid, cmdline.strip()))
90
+ except ValueError:
91
+ continue
92
+ return processes
93
+
94
+
95
+ def _get_process_cmdline(pid: int) -> str | None:
96
+ if sys.platform == "win32":
97
+ ps_cmd = f"(Get-CimInstance Win32_Process -Filter \"ProcessId={pid}\").CommandLine"
98
+ try:
99
+ result = subprocess.run(
100
+ ["powershell.exe", "-NoProfile", "-Command", ps_cmd],
101
+ capture_output=True, text=True, timeout=10,
102
+ )
103
+ return result.stdout.strip() or None
104
+ except Exception:
105
+ return None
106
+ else:
107
+ try:
108
+ return Path(f"/proc/{pid}/cmdline").read_text().replace("\x00", " ").strip()
109
+ except Exception:
110
+ return None
111
+
112
+
113
+ def _pid_alive(pid: int) -> bool:
114
+ if sys.platform == "win32":
115
+ import ctypes
116
+ kernel32 = ctypes.windll.kernel32
117
+ SYNCHRONIZE = 0x00100000
118
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
119
+ handle = kernel32.OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
120
+ if handle == 0:
121
+ return False
122
+ ret = kernel32.WaitForSingleObject(handle, 0)
123
+ kernel32.CloseHandle(handle)
124
+ return ret == 0x00000102 # WAIT_TIMEOUT
125
+ else:
126
+ try:
127
+ os.kill(pid, 0)
128
+ return True
129
+ except ProcessLookupError:
130
+ return False
131
+ except PermissionError:
132
+ return True
133
+
134
+
135
+ def _kill_process_tree(pid: int):
136
+ if sys.platform == "win32":
137
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)], capture_output=True, timeout=10)
138
+ else:
139
+ try:
140
+ os.killpg(os.getpgid(pid), 9)
141
+ except Exception:
142
+ os.kill(pid, 9)
143
+
144
+
145
+ # =============================================================================
146
+ # 公共函数:清理
147
+ # =============================================================================
148
+
149
+ def _pyproject_sha256() -> str:
150
+ """Hash the active pyproject.toml so we can detect dep changes."""
151
+ return hashlib.sha256((SCRIPT_DIR / "pyproject.toml").read_bytes()).hexdigest()
152
+
153
+
154
+ def _venv_is_fresh() -> bool:
155
+ """venv exists AND was built against the current pyproject.toml content."""
156
+ venv = SCRIPT_DIR / ".venv"
157
+ marker = venv / ".pyproject.sha256"
158
+ if not (venv / "pyvenv.cfg").exists() or not marker.exists():
159
+ return False
160
+ try:
161
+ return marker.read_text(encoding="utf-8").strip() == _pyproject_sha256()
162
+ except OSError:
163
+ return False
164
+
165
+
166
+ def _stamp_venv() -> None:
167
+ """Record pyproject hash inside the venv so next run can short-circuit."""
168
+ marker = SCRIPT_DIR / ".venv" / ".pyproject.sha256"
169
+ if marker.parent.exists():
170
+ try:
171
+ marker.write_text(_pyproject_sha256(), encoding="utf-8")
172
+ except OSError as e:
173
+ logger.warning(f"无法写入 venv 标记 {marker}: {e}")
174
+
175
+
176
+ def cleanup_before_run(force_reinit: bool = False):
177
+ _kill_stale_processes()
178
+ # 兼容旧版:把之前留在 tools/autotest/ 顶层的 tmp_hypium / hypium_mcp.log
179
+ # 等清掉。新流程下 batch 会在 task_<ts> 目录里生成它们,不会再到这里。
180
+ _clean_stale_artifacts(SCRIPT_DIR)
181
+ if force_reinit:
182
+ logger.info("--force-reinit 指定,强制清理 venv")
183
+ _clean_venv()
184
+ elif _venv_is_fresh():
185
+ logger.info("venv 已就绪(pyproject.toml hash 未变),跳过 venv 清理")
186
+ else:
187
+ logger.info("venv 不存在或 pyproject.toml 已变更,重建 venv")
188
+ _clean_venv()
189
+
190
+
191
+ def _kill_stale_processes():
192
+ if sys.platform != "win32":
193
+ return
194
+
195
+ my_pid = os.getpid()
196
+ # 包含 AutoTest.batch 本体 + hypium_mcp(uvicorn 子进程)+ harmony_autotest 模块入口,
197
+ # 三类都可能在中断后变成孤儿。
198
+ markers = [
199
+ "AutoTest.batch",
200
+ "-m AutoTest ",
201
+ "hypium_mcp",
202
+ "harmony_autotest",
203
+ "harmony-autotest",
204
+ ]
205
+
206
+ logger.info("=== 清理残留进程 ===")
207
+
208
+ processes = _get_python_processes()
209
+ stale = [(pid, cmd) for pid, cmd in processes if pid != my_pid and any(m in cmd for m in markers)]
210
+
211
+ if not stale:
212
+ logger.info("无残留进程")
213
+ return
214
+
215
+ for pid, cmd in stale:
216
+ logger.warning(f"杀掉残留进程 PID={pid}: {cmd[:80]}...")
217
+ try:
218
+ _kill_process_tree(pid)
219
+ except Exception as e:
220
+ logger.warning(f"杀进程失败 PID={pid}: {e}")
221
+ time.sleep(2)
222
+
223
+
224
+ _STALE_ARTIFACT_NAMES = (
225
+ "tmp_hypium",
226
+ "hypium_mcp.log",
227
+ "batch.pid",
228
+ "batch_stdout.log",
229
+ )
230
+
231
+
232
+ def _clean_stale_artifacts(base_dir: Path):
233
+ """删除 batch 跑完留下的本地中间产物(在 base_dir 顶层查找)。"""
234
+ for name in _STALE_ARTIFACT_NAMES:
235
+ p = base_dir / name
236
+ if not p.exists():
237
+ continue
238
+ try:
239
+ if p.is_dir():
240
+ shutil.rmtree(p, ignore_errors=True)
241
+ else:
242
+ p.unlink()
243
+ logger.info(f"删除残留: {p}")
244
+ except OSError as e:
245
+ logger.warning(f"无法删除 {p}: {e}")
246
+
247
+
248
+ def _clean_venv():
249
+ venv_path = SCRIPT_DIR / ".venv"
250
+
251
+ logger.info("=== 清理虚拟环境 ===")
252
+ if venv_path.exists():
253
+ logger.info(f"删除: {venv_path}")
254
+ try:
255
+ shutil.rmtree(venv_path, ignore_errors=True)
256
+ except OSError as e:
257
+ logger.warning(f"无法删除 {venv_path}: {e}")
258
+
259
+ for pycache in SCRIPT_DIR.rglob("__pycache__"):
260
+ if pycache.is_dir():
261
+ try:
262
+ shutil.rmtree(pycache, ignore_errors=True)
263
+ except Exception:
264
+ pass
265
+
266
+
267
+ # =============================================================================
268
+ # run 子命令
269
+ # =============================================================================
270
+
271
+ def setup_logging(output_dir: str):
272
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
273
+ log_path = Path(output_dir) / f"self_test_{timestamp}.log"
274
+ log_path.parent.mkdir(parents=True, exist_ok=True)
275
+ fmt = logging.Formatter("[%(asctime)s] %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
276
+ fh = logging.FileHandler(str(log_path), mode="w", encoding="utf-8")
277
+ fh.setFormatter(fmt)
278
+ sh = logging.StreamHandler(
279
+ io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
280
+ if hasattr(sys.stdout, "buffer") else sys.stdout
281
+ )
282
+ sh.setFormatter(fmt)
283
+ logger.addHandler(fh)
284
+ logger.addHandler(sh)
285
+ logger.setLevel(logging.DEBUG)
286
+ logger.info(f"日志文件: {log_path}")
287
+
288
+
289
+ def run_cmd(cmd, *, check=True, timeout=None, **kwargs):
290
+ cmd_str = cmd if isinstance(cmd, str) else " ".join(str(c) for c in cmd)
291
+ logger.info(f">>> {cmd_str}")
292
+ try:
293
+ result = subprocess.run(cmd, timeout=timeout, **kwargs)
294
+ except subprocess.TimeoutExpired:
295
+ logger.error(f"命令超时 ({timeout}s): {cmd_str}")
296
+ raise
297
+ except Exception:
298
+ logger.error(f"命令异常:\n{traceback.format_exc()}")
299
+ raise
300
+ if result.returncode != 0:
301
+ logger.warning(f"退出码={result.returncode}: {cmd_str}")
302
+ if hasattr(result, "stdout") and result.stdout:
303
+ logger.warning(f" stdout: {result.stdout.strip() if isinstance(result.stdout, str) else result.stdout}")
304
+ if hasattr(result, "stderr") and result.stderr:
305
+ logger.warning(f" stderr: {result.stderr.strip() if isinstance(result.stderr, str) else result.stderr}")
306
+ if check:
307
+ sys.exit(result.returncode)
308
+ else:
309
+ logger.debug(f"成功 (rc=0): {cmd_str}")
310
+ return result
311
+
312
+
313
+ def get_hdc():
314
+ hdc = shutil.which("hdc") or shutil.which("hdc.exe")
315
+ if not hdc:
316
+ logger.error("hdc 未找到")
317
+ sys.exit(1)
318
+ return hdc
319
+
320
+
321
+ def get_uv():
322
+ """查找 uv 可执行文件。优先使用 UV_BIN 环境变量,否则在 PATH 中搜索。"""
323
+ uv = os.environ.get("UV_BIN") or shutil.which("uv") or shutil.which("uv.exe")
324
+ if not uv or not Path(uv).exists():
325
+ logger.error(
326
+ "uv 未找到。请确认:\n"
327
+ " 1) uv 已安装且其所在目录在当前 shell 的 PATH 中(PowerShell 中可用 `where.exe uv` 验证)\n"
328
+ " 2) 或显式设置环境变量 UV_BIN 指向 uv 可执行文件的绝对路径"
329
+ )
330
+ sys.exit(1)
331
+ logger.info(f"uv: {uv}")
332
+ return uv
333
+
334
+
335
+ def _uv_env():
336
+ env = os.environ.copy()
337
+ env["UV_NO_PROGRESS"] = "1"
338
+ env["RUST_LOG"] = "off"
339
+ return env
340
+
341
+
342
+ def _prepare_testcases_jsonl(input_path: Path, task_subdir: Path) -> Path:
343
+ """将 `--testcases` 入参规范化为 JSONL 路径。
344
+
345
+ - 若 input 是 JSON 数组(旧 `testcases.json` 格式),转成 JSONL
346
+ 落到 `task_subdir/testcases.jsonl`,缺失字段按需补:
347
+ * `uuid` 用 case_name md5 前 8 位(缺 case_name 时用 idx)
348
+ * `spec` 补空字符串
349
+ 其它字段原样保留。
350
+ - 若 input 已是 JSONL(每行一个 JSON 对象),原路径透传。
351
+
352
+ 检测策略:剥掉 BOM/前导空白后看首字符是否为 `[`。
353
+ """
354
+ if not input_path.exists():
355
+ logger.error(f"测试用例文件不存在: {input_path}")
356
+ sys.exit(1)
357
+
358
+ text = input_path.read_text(encoding="utf-8-sig")
359
+ stripped = text.lstrip()
360
+ if not stripped:
361
+ logger.error(f"测试用例文件为空: {input_path}")
362
+ sys.exit(1)
363
+
364
+ if stripped[0] == "[":
365
+ try:
366
+ arr = json.loads(text)
367
+ except json.JSONDecodeError as e:
368
+ logger.error(f"测试用例 JSON 数组解析失败 {input_path}: {e}")
369
+ sys.exit(1)
370
+ if not isinstance(arr, list):
371
+ logger.error(f"测试用例文件根不是数组: {input_path}")
372
+ sys.exit(1)
373
+
374
+ out_path = task_subdir / "testcases.jsonl"
375
+ with out_path.open("w", encoding="utf-8") as f:
376
+ for idx, case in enumerate(arr, 1):
377
+ if not isinstance(case, dict):
378
+ logger.error(f"用例 #{idx} 不是 JSON 对象: {case!r}")
379
+ sys.exit(1)
380
+ if not case.get("uuid"):
381
+ seed = case.get("case_name") or f"case_{idx}"
382
+ case["uuid"] = hashlib.md5(seed.encode("utf-8")).hexdigest()[:8]
383
+ if "spec" not in case:
384
+ case["spec"] = ""
385
+ f.write(json.dumps(case, ensure_ascii=False) + "\n")
386
+ logger.info(f"已将 JSON 数组转换为 JSONL: {out_path}({len(arr)} 条用例)")
387
+ return out_path
388
+
389
+ # 视作 JSONL,逐行校验
390
+ lines = [l for l in text.splitlines() if l.strip()]
391
+ for i, line in enumerate(lines, 1):
392
+ try:
393
+ json.loads(line)
394
+ except json.JSONDecodeError as e:
395
+ logger.error(f"JSONL 第 {i} 行解析失败 {input_path}: {e}")
396
+ sys.exit(1)
397
+ logger.info(f"测试用例文件已为 JSONL,原样透传: {input_path}({len(lines)} 条用例)")
398
+ return input_path
399
+
400
+
401
+ def validate_config(config_path: Path) -> None:
402
+ """确保用户已提供 config.yaml 且 api_key 已替换。"""
403
+ if not config_path.exists():
404
+ logger.error(
405
+ f"未找到配置文件: {config_path}\n"
406
+ f"请复制 `config.yaml.example` 为 `config.yaml` 并填入真实 api_key 后重试。"
407
+ )
408
+ sys.exit(1)
409
+
410
+ try:
411
+ text = config_path.read_text(encoding="utf-8")
412
+ except OSError as e:
413
+ logger.error(f"无法读取配置文件 {config_path}: {e}")
414
+ sys.exit(1)
415
+
416
+ if API_KEY_PLACEHOLDER in text:
417
+ logger.error(
418
+ f"配置文件 {config_path} 中仍包含占位符 `{API_KEY_PLACEHOLDER}`,请将所有 api_key 替换为真实值后重试。"
419
+ )
420
+ sys.exit(1)
421
+
422
+
423
+ def cmd_run(args):
424
+ """run 子命令:准备环境 + 后台执行 AutoTest.batch"""
425
+ task_dir = Path(args.task_dir).resolve()
426
+ task_dir.mkdir(parents=True, exist_ok=True)
427
+ output_dir = Path(args.output_dir).resolve() if args.output_dir else task_dir
428
+ output_dir.mkdir(parents=True, exist_ok=True)
429
+
430
+ setup_logging(str(output_dir))
431
+ logger.info(f"Python: {sys.executable}")
432
+ logger.info(
433
+ f"参数: testcases={args.testcases}, hap={args.hap}, bundle={args.bundle_name}, "
434
+ f"task_dir={task_dir}, category={args.category}, config={args.config}"
435
+ )
436
+ logger.info(
437
+ "前置用例(若有)已并入 testcases,以 case_name 前缀 `[PRE]` 标识;"
438
+ "self_test_runner 不再单独处理 pre-case。"
439
+ )
440
+
441
+ target_venv = (SCRIPT_DIR / ".venv").resolve()
442
+ try:
443
+ Path(sys.executable).resolve().relative_to(target_venv)
444
+ msg = (
445
+ f"Refusing to run: this script is executing inside {target_venv}, "
446
+ f"which it is about to delete and recreate (Windows file locks will leave "
447
+ f"the venv in a broken state). Invoke with the system Python instead, e.g. "
448
+ f"`python self_test_runner.py run ...` (NOT `uv run python ...`)."
449
+ )
450
+ logger.error(msg)
451
+ print(json.dumps({"status": "ERROR", "reason": msg}))
452
+ sys.exit(2)
453
+ except ValueError:
454
+ pass
455
+
456
+ # 创建本次任务专属子目录 task_<timestamp>,供 status 子命令通过 glob 找到
457
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
458
+ task_subdir = task_dir / f"task_{timestamp}"
459
+ task_subdir.mkdir(parents=True, exist_ok=True)
460
+
461
+ pid_file = task_dir / "batch.pid"
462
+ stdout_log = task_dir / "batch_stdout.log"
463
+
464
+ # 杀掉上次残留进程
465
+ if pid_file.exists():
466
+ try:
467
+ old_pid = int(pid_file.read_text().strip())
468
+ if _pid_alive(old_pid):
469
+ cmdline = _get_process_cmdline(old_pid)
470
+ if cmdline and "AutoTest.batch" in cmdline:
471
+ logger.warning(f"杀掉上次残留进程 PID={old_pid}")
472
+ _kill_process_tree(old_pid)
473
+ time.sleep(2)
474
+ except (ValueError, OSError):
475
+ pass
476
+
477
+ try:
478
+ # 在删 venv / 跑 uv 之前先校验 uv 与 config.yaml,避免清理后才发现缺东西
479
+ uv = get_uv()
480
+
481
+ config_path = Path(args.config).resolve() if args.config else (SCRIPT_DIR / "config.yaml")
482
+ validate_config(config_path)
483
+ logger.info(f"配置文件: {config_path}")
484
+
485
+ cleanup_before_run(force_reinit=args.force_reinit)
486
+
487
+ logger.info("=== Step 1: uv sync(确保 harmony-autotest + 传递依赖已安装) ===")
488
+ run_cmd([uv, "sync"], cwd=str(SCRIPT_DIR), capture_output=True, text=True,
489
+ encoding="utf-8", timeout=UV_TIMEOUT, env=_uv_env())
490
+ _stamp_venv()
491
+
492
+ hdc = get_hdc()
493
+
494
+ logger.info(f"=== Step 2a: uninstall {args.bundle_name} ===")
495
+ run_cmd([hdc, "uninstall", args.bundle_name], capture_output=True, text=True,
496
+ encoding="utf-8", timeout=INSTALL_TIMEOUT, check=False)
497
+
498
+ logger.info(f"=== Step 2b: install {args.hap} ===")
499
+ hap = Path(args.hap)
500
+ if not hap.exists():
501
+ logger.error(f"hap 不存在: {args.hap}")
502
+ sys.exit(1)
503
+ result = run_cmd([hdc, "install", "-r", str(hap)], capture_output=True, text=True,
504
+ encoding="utf-8", timeout=INSTALL_TIMEOUT, check=False)
505
+ if result.returncode == 0 or "successfully" in (result.stdout or ""):
506
+ logger.info("安装成功")
507
+ time.sleep(HAP_INSTALL_WAIT)
508
+ else:
509
+ logger.error(f"安装失败: {result.stdout}")
510
+ sys.exit(1)
511
+
512
+ # 测试用例规范化:旧 JSON 数组 → JSONL;本身就是 JSONL 透传
513
+ # 前置用例(若有)已经是 testcases 的第一条,case_name 以 [PRE] 开头
514
+ testcases_jsonl = _prepare_testcases_jsonl(
515
+ Path(args.testcases).resolve(), task_subdir,
516
+ )
517
+
518
+ # 构建 AutoTest.batch 命令
519
+ # `--project SCRIPT_DIR` 让 uv 在任意 cwd 下都能找到正确的 venv;
520
+ # 而 cwd 设为 task_subdir,hypium_mcp.log / tmp_hypium/ 等中间产物随之落在 task 子目录。
521
+ cmd = [
522
+ uv, "run", "--project", str(SCRIPT_DIR), "python", "-m", "AutoTest.batch",
523
+ "--task-dir", str(task_subdir),
524
+ "--testcases", str(testcases_jsonl),
525
+ "--category", args.category,
526
+ "--work-dir", str(SCRIPT_DIR),
527
+ "--config", str(config_path),
528
+ ]
529
+
530
+ # 后台启动 AutoTest.batch
531
+ logger.info("=== Step 3: 后台启动 AutoTest.batch ===")
532
+ batch_env = os.environ.copy()
533
+ batch_env["PYTHONIOENCODING"] = "utf-8"
534
+ batch_env["PYTHONUTF8"] = "1"
535
+ batch_env["PYTHONUNBUFFERED"] = "1"
536
+
537
+ log_fh = open(stdout_log, "w", encoding="utf-8")
538
+ popen_kwargs = dict(
539
+ stdin=subprocess.DEVNULL,
540
+ stdout=log_fh,
541
+ stderr=subprocess.STDOUT,
542
+ env=batch_env,
543
+ )
544
+ if sys.platform == "win32":
545
+ # CREATE_NEW_CONSOLE | CREATE_NEW_PROCESS_GROUP — survive parent shell exit
546
+ popen_kwargs["creationflags"] = 0x00000010 | 0x00000200
547
+ else:
548
+ popen_kwargs["start_new_session"] = True
549
+
550
+ proc = subprocess.Popen(cmd, cwd=str(task_subdir), **popen_kwargs)
551
+ pid_file.write_text(str(proc.pid), encoding="utf-8")
552
+
553
+ logger.info(f"后台进程 PID={proc.pid}")
554
+ print(json.dumps({
555
+ "status": "RUNNING",
556
+ "pid": proc.pid,
557
+ "pid_file": str(pid_file),
558
+ "stdout_log": str(stdout_log),
559
+ "task_dir": str(task_dir),
560
+ "task_subdir": str(task_subdir),
561
+ }))
562
+
563
+ except SystemExit:
564
+ raise
565
+ except Exception:
566
+ logger.error(f"异常:\n{traceback.format_exc()}")
567
+ sys.exit(1)
568
+
569
+
570
+ # =============================================================================
571
+ # status 子命令
572
+ # =============================================================================
573
+
574
+ def _find_task_subdir(task_dir: Path) -> Path | None:
575
+ candidates = sorted(task_dir.glob("task_*"), key=lambda p: p.name, reverse=True)
576
+ return candidates[0] if candidates else None
577
+
578
+
579
+ def _find_latest_log(output_dir: Path) -> Path | None:
580
+ candidates = sorted(output_dir.glob("self_test_*.log"), key=lambda p: p.name, reverse=True)
581
+ candidates += sorted(output_dir.glob("batch_stdout.log"), key=lambda p: p.name, reverse=True)
582
+ return candidates[0] if candidates else None
583
+
584
+
585
+ def _log_status(task_dir: Path, result: dict):
586
+ log_file = task_dir / "status_history.log"
587
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
588
+ log_file.parent.mkdir(parents=True, exist_ok=True)
589
+ existing = log_file.read_text(encoding="utf-8") if log_file.exists() else ""
590
+ log_file.write_text(existing + f"[{timestamp}] {json.dumps(result, ensure_ascii=False)}\n", encoding="utf-8")
591
+
592
+
593
+ def cmd_status(args):
594
+ """status 子命令:查询 AutoTest.batch 执行状态"""
595
+ task_dir = Path(args.task_dir)
596
+ output_dir = Path(args.output_dir) if args.output_dir else task_dir
597
+
598
+ pid_file = task_dir / "batch.pid"
599
+ if not pid_file.exists():
600
+ print(json.dumps({"status": "NOT_STARTED"}))
601
+ sys.exit(4)
602
+
603
+ pid = int(pid_file.read_text().strip())
604
+ alive = _pid_alive(pid)
605
+
606
+ task_subdir = _find_task_subdir(task_dir)
607
+ jsonl_file = task_subdir / "task_results.jsonl" if task_subdir else None
608
+
609
+ cases_done = 0
610
+ last_case = ""
611
+ if jsonl_file and jsonl_file.exists():
612
+ lines = jsonl_file.read_text(encoding="utf-8").strip().splitlines()
613
+ cases_done = len(lines)
614
+ if lines:
615
+ last_case = json.loads(lines[-1]).get("case_name", "")
616
+
617
+ summary_file = task_subdir / "summary.json" if task_subdir else None
618
+ has_summary = summary_file and summary_file.exists()
619
+
620
+ log_tail = ""
621
+ log_file = _find_latest_log(output_dir)
622
+ if log_file and log_file.exists():
623
+ log_tail = "\n".join(log_file.read_text(encoding="utf-8").strip().splitlines()[-5:])
624
+
625
+ if has_summary:
626
+ summary = json.loads(summary_file.read_text(encoding="utf-8"))
627
+ result = {
628
+ "status": "COMPLETED",
629
+ "pid": pid,
630
+ "task_subdir": str(task_subdir),
631
+ "cases_done": summary.get("total_cases", cases_done),
632
+ "pass_count": summary.get("pass_count", summary.get("success_count", 0)),
633
+ "fail_count": summary.get("fail_count", 0),
634
+ "unknown_count": summary.get("unknown_count", 0),
635
+ "pass_rate": summary.get("pass_rate", 0),
636
+ "log_tail": log_tail,
637
+ }
638
+ print(json.dumps(result))
639
+ _log_status(task_dir, result)
640
+ sys.exit(0)
641
+ elif alive:
642
+ result = {
643
+ "status": "RUNNING",
644
+ "pid": pid,
645
+ "cases_done": cases_done,
646
+ "last_case": last_case,
647
+ "task_subdir": str(task_subdir) if task_subdir else None,
648
+ "log_tail": log_tail,
649
+ }
650
+ print(json.dumps(result))
651
+ _log_status(task_dir, result)
652
+ sys.exit(2)
653
+ else:
654
+ result = {
655
+ "status": "CRASHED",
656
+ "pid": pid,
657
+ "cases_done": cases_done,
658
+ "last_case": last_case,
659
+ "task_subdir": str(task_subdir) if task_subdir else None,
660
+ "log_tail": log_tail,
661
+ }
662
+ print(json.dumps(result))
663
+ _log_status(task_dir, result)
664
+ sys.exit(3)
665
+
666
+
667
+ # =============================================================================
668
+ # kill 子命令
669
+ # =============================================================================
670
+
671
+ def cmd_kill(args):
672
+ """kill 子命令:终止 AutoTest.batch 进程"""
673
+ task_dir = Path(args.task_dir)
674
+ pid_file = task_dir / "batch.pid"
675
+
676
+ if not pid_file.exists():
677
+ print(json.dumps({"status": "NO_PID_FILE", "killed": False}))
678
+ return
679
+
680
+ pid = int(pid_file.read_text().strip())
681
+
682
+ if not _pid_alive(pid):
683
+ print(json.dumps({"status": "ALREADY_DEAD", "pid": pid, "killed": False}))
684
+ return
685
+
686
+ cmdline = _get_process_cmdline(pid)
687
+ if cmdline and "AutoTest.batch" not in cmdline:
688
+ print(json.dumps({
689
+ "status": "REFUSED", "pid": pid, "killed": False,
690
+ "reason": "非 AutoTest.batch 进程", "cmdline": cmdline,
691
+ }))
692
+ return
693
+
694
+ _kill_process_tree(pid)
695
+ print(json.dumps({"status": "KILLED", "pid": pid, "killed": True}))
696
+
697
+
698
+ # =============================================================================
699
+ # clean 子命令
700
+ # =============================================================================
701
+
702
+ def cmd_clean(args):
703
+ """clean 子命令:彻底重置本地状态以便重跑。
704
+
705
+ 动作:
706
+ - 杀掉所有残留 AutoTest.batch / hypium_mcp / harmony_autotest 进程
707
+ - 删除 .venv 及 __pycache__
708
+ - 删除本目录顶层的 tmp_hypium / hypium_mcp.log / batch.pid / batch_stdout.log
709
+
710
+ NOT touched: 全局 uv 缓存。重建缓存需重新从 gitcode 拉 6 个 whl,
711
+ 单纯"重跑一遍"不需要碰它。需要的话用 `uv cache clean` 自己清。
712
+ """
713
+ # 控制台输出即可,不写 self_test_*.log(避免污染 output-dir)
714
+ logging.basicConfig(
715
+ level=logging.INFO,
716
+ format="[%(asctime)s] %(levelname)s %(message)s",
717
+ datefmt="%Y-%m-%d %H:%M:%S",
718
+ )
719
+ logger.setLevel(logging.INFO)
720
+
721
+ _kill_stale_processes()
722
+ _clean_venv()
723
+ _clean_stale_artifacts(SCRIPT_DIR)
724
+ print(json.dumps({"status": "CLEANED"}, ensure_ascii=False))
725
+
726
+
727
+ # =============================================================================
728
+ # 主入口
729
+ # =============================================================================
730
+
731
+ def main():
732
+ parser = argparse.ArgumentParser(description="自测执行器")
733
+ sub = parser.add_subparsers(dest="command", required=True)
734
+
735
+ # run 子命令
736
+ p_run = sub.add_parser("run", help="准备环境并执行用例")
737
+ p_run.add_argument("--testcases", required=True,
738
+ help="测试用例文件,JSON 数组或 JSONL,传入 JSON 数组时会自动转换为 JSONL")
739
+ p_run.add_argument("--hap", required=True, help=".hap 文件")
740
+ p_run.add_argument("--bundle-name", required=True, help="包名")
741
+ p_run.add_argument("--task-dir", default="task", help="任务目录(内部会创建 task_<timestamp> 子目录)")
742
+ p_run.add_argument("--output-dir", default=None, help="日志目录")
743
+ p_run.add_argument("--category", default=DEFAULT_CATEGORY, help="测试分类名称,传给 AutoTest.batch")
744
+ p_run.add_argument("--config", default=None,
745
+ help="AutoTest 配置文件绝对路径;默认为脚本同目录下的 config.yaml")
746
+ p_run.add_argument("--force-reinit", action="store_true",
747
+ help="强制清理 .venv 并重新 uv sync;默认仅在 pyproject.toml 内容变更时才重建")
748
+ p_run.set_defaults(func=cmd_run)
749
+
750
+ # status 子命令
751
+ p_status = sub.add_parser("status", help="查询执行状态")
752
+ p_status.add_argument("--task-dir", required=True)
753
+ p_status.add_argument("--output-dir", default=None)
754
+ p_status.set_defaults(func=cmd_status)
755
+
756
+ # kill 子命令
757
+ p_kill = sub.add_parser("kill", help="终止进程")
758
+ p_kill.add_argument("--task-dir", required=True)
759
+ p_kill.set_defaults(func=cmd_kill)
760
+
761
+ # clean 子命令
762
+ p_clean = sub.add_parser(
763
+ "clean",
764
+ help="重置本地状态(杀残留进程、删 .venv、删本目录残留中间产物),不动全局 uv 缓存",
765
+ )
766
+ p_clean.set_defaults(func=cmd_clean)
767
+
768
+ args = parser.parse_args()
769
+ args.func(args)
770
+
771
+
772
+ if __name__ == "__main__":
773
+ main()