omni-engine 1.1.2.16__py2.py3-none-any.whl

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 (275) hide show
  1. omni/__init__.py +57 -0
  2. omni/agents/__init__.py +4 -0
  3. omni/agents/exploration.py +79 -0
  4. omni/agents/generator.py +58 -0
  5. omni/agents/healer.py +142 -0
  6. omni/agents/mcp_bridge.py +37 -0
  7. omni/agents/models.py +109 -0
  8. omni/agents/orchestrator.py +83 -0
  9. omni/agents/planner.py +212 -0
  10. omni/agents/script_writer.py +319 -0
  11. omni/agents/templates.py +205 -0
  12. omni/agents/testcase_writer.py +173 -0
  13. omni/cli/__init__.py +12 -0
  14. omni/cli/ai_cmd.py +165 -0
  15. omni/cli/api_cmd.py +220 -0
  16. omni/cli/config_cmd.py +28 -0
  17. omni/cli/main.py +268 -0
  18. omni/cli/manage_cmd.py +244 -0
  19. omni/cli/probe_cmd.py +160 -0
  20. omni/cli/pytest_cmd.py +152 -0
  21. omni/cli/spec_cmd.py +94 -0
  22. omni/cli/suite_cmd.py +457 -0
  23. omni/cli/sync_cmd.py +78 -0
  24. omni/cli/webai_cmd.py +162 -0
  25. omni/common/__init__.py +0 -0
  26. omni/common/case.py +783 -0
  27. omni/common/decorators.py +84 -0
  28. omni/common/emoji_msg.py +45 -0
  29. omni/common/exceptions.py +7 -0
  30. omni/common/log.py +7 -0
  31. omni/common/meta_class.py +1163 -0
  32. omni/common/reporter.py +7 -0
  33. omni/common/utils.py +171 -0
  34. omni/config/__init__.py +27 -0
  35. omni/config/conf_parser.py +409 -0
  36. omni/config/constants.py +64 -0
  37. omni/config/globals.py +14 -0
  38. omni/config/settings.ini +97 -0
  39. omni/config/unified_config.py +170 -0
  40. omni/config/version.py +115 -0
  41. omni/context/__init__.py +18 -0
  42. omni/context/call_tree_node.py +97 -0
  43. omni/context/context_vars.py +46 -0
  44. omni/context/db.py +248 -0
  45. omni/context/db_init.py +49 -0
  46. omni/context/global_context.py +481 -0
  47. omni/context/paths.py +151 -0
  48. omni/context/resource_manager.py +94 -0
  49. omni/context/runtime.py +85 -0
  50. omni/context/services.py +253 -0
  51. omni/context/webdriver_finder.py +55 -0
  52. omni/core/__init__.py +4 -0
  53. omni/core/ai/__init__.py +19 -0
  54. omni/core/ai/analyzer.py +105 -0
  55. omni/core/ai/data_generator.py +95 -0
  56. omni/core/ai/healing.py +103 -0
  57. omni/core/ai/openai_provider.py +241 -0
  58. omni/core/ai/provider.py +153 -0
  59. omni/core/ai/validators.py +42 -0
  60. omni/core/base.py +199 -0
  61. omni/core/evidence.py +169 -0
  62. omni/core/exceptions.py +211 -0
  63. omni/core/log.py +219 -0
  64. omni/core/registry.py +210 -0
  65. omni/core/reporter.py +50 -0
  66. omni/database/__init__.py +17 -0
  67. omni/database/mysql.py +49 -0
  68. omni/database/sqlite.py +265 -0
  69. omni/drivers/__init__.py +0 -0
  70. omni/drivers/api/__init__.py +0 -0
  71. omni/drivers/api/api_runner.py +768 -0
  72. omni/drivers/api/builtin/__init__.py +0 -0
  73. omni/drivers/api/builtin/comparators.py +242 -0
  74. omni/drivers/api/builtin/functions.py +190 -0
  75. omni/drivers/api/builtin_loader.py +266 -0
  76. omni/drivers/api/callback.py +222 -0
  77. omni/drivers/api/client.py +247 -0
  78. omni/drivers/api/data_loader/__init__.py +12 -0
  79. omni/drivers/api/data_loader/xmind_case_loader.py +305 -0
  80. omni/drivers/api/data_loader/yaml_case_loader.py +171 -0
  81. omni/drivers/api/engine/__init__.py +0 -0
  82. omni/drivers/api/engine/base.py +40 -0
  83. omni/drivers/api/engine/grpc_engine.py +192 -0
  84. omni/drivers/api/engine/http_engine.py +37 -0
  85. omni/drivers/api/engine/ws_engine.py +108 -0
  86. omni/drivers/api/executor.py +237 -0
  87. omni/drivers/api/ext/__init__.py +0 -0
  88. omni/drivers/api/ext/uploader.py +132 -0
  89. omni/drivers/api/grpc_client.py +192 -0
  90. omni/drivers/api/httpstat.py +95 -0
  91. omni/drivers/api/models.py +252 -0
  92. omni/drivers/api/orchestrator.py +156 -0
  93. omni/drivers/api/parser.py +641 -0
  94. omni/drivers/api/pytest_shim.py +86 -0
  95. omni/drivers/api/reporter.py +106 -0
  96. omni/drivers/api/response.py +365 -0
  97. omni/drivers/api/runner.py +11 -0
  98. omni/drivers/api/serializers/__init__.py +0 -0
  99. omni/drivers/api/serializers/result.py +201 -0
  100. omni/drivers/api/step.py +589 -0
  101. omni/drivers/api/step_executors/__init__.py +26 -0
  102. omni/drivers/api/step_executors/base.py +83 -0
  103. omni/drivers/api/step_executors/grpc_executor.py +106 -0
  104. omni/drivers/api/step_executors/script_executor.py +147 -0
  105. omni/drivers/api/step_executors/shell_executor.py +117 -0
  106. omni/drivers/api/step_executors/sleep_executor.py +68 -0
  107. omni/drivers/api/step_executors/sql_executor.py +159 -0
  108. omni/drivers/api/step_executors/ws_executor.py +100 -0
  109. omni/drivers/api/utils.py +52 -0
  110. omni/drivers/api/validator_extractor.py +66 -0
  111. omni/drivers/api/ws_client.py +108 -0
  112. omni/drivers/app/__init__.py +0 -0
  113. omni/drivers/app/app_driver.py +236 -0
  114. omni/drivers/app/app_runner.py +135 -0
  115. omni/drivers/app/exceptions.py +65 -0
  116. omni/drivers/cmd/__init__.py +0 -0
  117. omni/drivers/cmd/cmd_runner.py +150 -0
  118. omni/drivers/engine/__init__.py +61 -0
  119. omni/drivers/web/__init__.py +17 -0
  120. omni/drivers/web/base/__init__.py +0 -0
  121. omni/drivers/web/base/base_driver.py +13 -0
  122. omni/drivers/web/base/base_engine.py +459 -0
  123. omni/drivers/web/base/base_page.py +243 -0
  124. omni/drivers/web/base/driver_factory.py +217 -0
  125. omni/drivers/web/base/exceptions.py +79 -0
  126. omni/drivers/web/base/prober.py +785 -0
  127. omni/drivers/web/base/variable_resolver.py +54 -0
  128. omni/drivers/web/playwright/__init__.py +0 -0
  129. omni/drivers/web/playwright/playwright_driver.py +271 -0
  130. omni/drivers/web/playwright/playwright_engine.py +552 -0
  131. omni/drivers/web/selenium/__init__.py +0 -0
  132. omni/drivers/web/selenium/alert.py +64 -0
  133. omni/drivers/web/selenium/browser_manager.py +167 -0
  134. omni/drivers/web/selenium/keyboard.py +179 -0
  135. omni/drivers/web/selenium/locators.py +51 -0
  136. omni/drivers/web/selenium/selector.py +36 -0
  137. omni/drivers/web/selenium/selenium_driver.py +1030 -0
  138. omni/drivers/web/selenium/selenium_engine.py +439 -0
  139. omni/drivers/web/ui_assert.py +198 -0
  140. omni/drivers/web/web_runner.py +174 -0
  141. omni/integrations/__init__.py +12 -0
  142. omni/integrations/allure_custom/__init__.py +12 -0
  143. omni/integrations/allure_custom/apply_allure_custom.py +198 -0
  144. omni/integrations/allure_custom/settings.js +20 -0
  145. omni/integrations/jenkins_opt.py +92 -0
  146. omni/integrations/mysql_opt.py +219 -0
  147. omni/integrations/platform/__init__.py +9 -0
  148. omni/integrations/platform/client.py +119 -0
  149. omni/integrations/platform/manifest.py +85 -0
  150. omni/integrations/retry.py +172 -0
  151. omni/integrations/sqlalchemy_opt.py +119 -0
  152. omni/integrations/sqlite_opt.py +177 -0
  153. omni/integrations/ssh_mgr.py +359 -0
  154. omni/models/__init__.py +19 -0
  155. omni/models/conf.py +66 -0
  156. omni/models/emoji.py +136 -0
  157. omni/models/engine.py +100 -0
  158. omni/models/result.py +76 -0
  159. omni/models/testcase.py +61 -0
  160. omni/notification/__init__.py +12 -0
  161. omni/notification/mail.py +356 -0
  162. omni/notification/qw_chat.py +86 -0
  163. omni/omni_marks.py +24 -0
  164. omni/parser/__init__.py +18 -0
  165. omni/parser/testcase_scanner.py +385 -0
  166. omni/plugin/__init__.py +21 -0
  167. omni/plugin/__main__.py +69 -0
  168. omni/plugin/case_callback.py +151 -0
  169. omni/plugin/engine_plugin.py +336 -0
  170. omni/plugin/json_report.py +187 -0
  171. omni/plugin/template.py +224 -0
  172. omni/plugin_loader.py +70 -0
  173. omni/reporter/__init__.py +57 -0
  174. omni/reporter/allure/__init__.py +3 -0
  175. omni/reporter/allure/reporter.py +50 -0
  176. omni/reporter/base.py +73 -0
  177. omni/reporter/builtin/__init__.py +7 -0
  178. omni/reporter/builtin/model/__init__.py +43 -0
  179. omni/reporter/builtin/model/base.py +121 -0
  180. omni/reporter/builtin/model/message.py +46 -0
  181. omni/reporter/builtin/model/result.py +93 -0
  182. omni/reporter/builtin/model/statistics.py +55 -0
  183. omni/reporter/builtin/model/step.py +115 -0
  184. omni/reporter/builtin/model/suite.py +144 -0
  185. omni/reporter/builtin/model/testcase.py +77 -0
  186. omni/reporter/builtin/model/visitor.py +89 -0
  187. omni/reporter/builtin/output/__init__.py +243 -0
  188. omni/reporter/builtin/report/__init__.py +147 -0
  189. omni/reporter/builtin/report/html_writer.py +211 -0
  190. omni/reporter/builtin/report/json_writer.py +38 -0
  191. omni/reporter/builtin/report/xunit_writer.py +104 -0
  192. omni/reporter/builtin/reporter.py +191 -0
  193. omni/reporter/builtin/template.html +418 -0
  194. omni/reporter/log_to_html/__init__.py +51 -0
  195. omni/reporter/log_to_html/log.html +169 -0
  196. omni/reporter/log_to_html/log_template.html +193 -0
  197. omni/reporter/log_to_html/report.py +383 -0
  198. omni/reporter/manager.py +180 -0
  199. omni/reporter/noop/__init__.py +3 -0
  200. omni/reporter/noop/reporter.py +23 -0
  201. omni/reporter/registry.py +112 -0
  202. omni/reporter/utils.py +163 -0
  203. omni/spec/__init__.py +11 -0
  204. omni/spec/adapters/__init__.py +17 -0
  205. omni/spec/adapters/api_adapter.py +90 -0
  206. omni/spec/adapters/base.py +74 -0
  207. omni/spec/adapters/cli_adapter.py +20 -0
  208. omni/spec/adapters/grpc_adapter.py +44 -0
  209. omni/spec/adapters/registry.py +26 -0
  210. omni/spec/adapters/script_adapter.py +25 -0
  211. omni/spec/adapters/sleep_adapter.py +17 -0
  212. omni/spec/adapters/sql_adapter.py +98 -0
  213. omni/spec/adapters/web_adapter.py +334 -0
  214. omni/spec/adapters/ws_adapter.py +44 -0
  215. omni/spec/bridge.py +71 -0
  216. omni/spec/compiler.py +70 -0
  217. omni/spec/executor/__init__.py +9 -0
  218. omni/spec/executor/context.py +21 -0
  219. omni/spec/executor/step_executor.py +261 -0
  220. omni/spec/executor/tracker.py +46 -0
  221. omni/spec/executor/validator.py +204 -0
  222. omni/spec/generator/__init__.py +7 -0
  223. omni/spec/generator/base.py +15 -0
  224. omni/spec/generator/to_code.py +46 -0
  225. omni/spec/generator/to_spec.py +48 -0
  226. omni/spec/loader.py +79 -0
  227. omni/spec/models.py +66 -0
  228. omni/spec/variables.py +71 -0
  229. omni/utils/__init__.py +12 -0
  230. omni/utils/dsl_helper.py +224 -0
  231. omni/utils/file_helper.py +217 -0
  232. omni/utils/pkg_helper.py +83 -0
  233. omni/utils/singleton.py +28 -0
  234. omni/utils/util.py +408 -0
  235. omni_engine-1.1.2.16.dist-info/METADATA +1099 -0
  236. omni_engine-1.1.2.16.dist-info/RECORD +275 -0
  237. omni_engine-1.1.2.16.dist-info/WHEEL +6 -0
  238. omni_engine-1.1.2.16.dist-info/entry_points.txt +9 -0
  239. omni_engine-1.1.2.16.dist-info/top_level.txt +2 -0
  240. tests/__init__.py +12 -0
  241. tests/common/__init__.py +1 -0
  242. tests/common/test_common_utils.py +135 -0
  243. tests/common/test_decorator_class.py +33 -0
  244. tests/common/test_meta_class.py +321 -0
  245. tests/common/testcase/__init__.py +0 -0
  246. tests/common/testcase/test_repro_setup_class_cls.py +46 -0
  247. tests/context/__init__.py +1 -0
  248. tests/context/test_context_vars.py +55 -0
  249. tests/context/test_paths.py +87 -0
  250. tests/context/test_resource_manager.py +94 -0
  251. tests/context/test_services.py +134 -0
  252. tests/core/__init__.py +1 -0
  253. tests/core/ai/__init__.py +1 -0
  254. tests/core/ai/test_ai_analyzer.py +84 -0
  255. tests/core/ai/test_openai_provider.py +107 -0
  256. tests/core/test_engine_base.py +137 -0
  257. tests/core/test_engine_registry.py +123 -0
  258. tests/core/test_evidence.py +86 -0
  259. tests/core/test_reporter.py +84 -0
  260. tests/integrations/__init__.py +1 -0
  261. tests/integrations/test_platform_client.py +314 -0
  262. tests/parser/__init__.py +1 -0
  263. tests/parser/test_testcase_scanner.py +763 -0
  264. tests/plugin/__init__.py +1 -0
  265. tests/plugin/test_case_callback.py +145 -0
  266. tests/plugin/test_json_report_plugin.py +132 -0
  267. tests/plugin/test_plugin_template.py +78 -0
  268. tests/spec/__init__.py +1 -0
  269. tests/spec/test_behavior_matrix.py +100 -0
  270. tests/spec/test_bridge.py +84 -0
  271. tests/spec/test_spec_validator.py +116 -0
  272. tests/spec/test_step_executor_parallel.py +102 -0
  273. tests/spec/test_web_adapter_failure_log.py +90 -0
  274. tests/utils/__init__.py +1 -0
  275. tests/utils/test_util.py +167 -0
omni/__init__.py ADDED
@@ -0,0 +1,57 @@
1
+ # omni-engine — 多协议自动化测试执行引擎
2
+
3
+ # ── 核心基类(直接从模块导入,避免循环加载) ──
4
+ from omni.core.base import BaseEngine, StepResult, AIAnalysisMeta
5
+ from omni.core.registry import EngineRegistry
6
+
7
+ # ── 兼容导出(旧代码使用,保持稳定) ──
8
+ from omni.context import GlobalContext
9
+ from omni.common.emoji_msg import EMsg
10
+ from omni.common.meta_class import CaseMetaClass, StepMetaClass
11
+ from omni.common.case import TestCaseBase
12
+
13
+ # ── 执行器(lazy 加载,通过 drivers/engine/__init__.py 触发注册) ──
14
+ from omni.drivers.engine import (
15
+ ApiRunner, WebRunner, CmdRunner, AppRunner,
16
+ )
17
+
18
+ # ── Fluent API ──
19
+ from omni.drivers.api.step import RunRequest, Config
20
+
21
+ # ── AI 能力(可选) ──
22
+ try:
23
+ from omni.core.ai.validators import AIValidator as _AIValidator # type: ignore[assignment]
24
+ except ImportError:
25
+ class _AIValidator: # placeholder if AI not installed
26
+ """AIValidator"""
27
+ pass
28
+ AIValidator: type = _AIValidator
29
+
30
+ # ── Spec 路径 ──
31
+ from omni.spec import SpecCompiler, StepExecutor
32
+ from omni.spec.executor.validator import SpecValidator
33
+
34
+ # ── 多模态证据 ──
35
+ from omni.core.evidence import EvidenceCollector
36
+
37
+ # ── 上下文工具 ──
38
+ from omni.context.paths import PathManager
39
+
40
+ __all__ = [
41
+ # 执行器
42
+ "ApiRunner", "WebRunner", "CmdRunner", "AppRunner",
43
+ # 核心
44
+ "BaseEngine", "StepResult", "AIAnalysisMeta", "EngineRegistry", "TestCaseBase",
45
+ # Fluent API
46
+ "RunRequest", "Config",
47
+ # AI
48
+ "AIValidator",
49
+ # Spec
50
+ "SpecCompiler", "StepExecutor", "SpecValidator",
51
+ # 多模态
52
+ "EvidenceCollector",
53
+ # 路径
54
+ "PathManager",
55
+ # 兼容
56
+ "GlobalContext", "EMsg", "CaseMetaClass", "StepMetaClass",
57
+ ]
@@ -0,0 +1,4 @@
1
+ """omni.agents — AI 驱动的 Web 测试生成与自愈子系统(路径 C 增强)"""
2
+ from omni.agents.models import TestPlan, TestCase, HealReport
3
+
4
+ __all__ = ["TestPlan", "TestCase", "HealReport"]
@@ -0,0 +1,79 @@
1
+ """探索公共能力(供 Planner / Prober 复用)
2
+
3
+ 从 Planner 抽取: seed 步骤加载与执行、LLM 建议探索步骤转换。
4
+ """
5
+ import json
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Union
9
+
10
+
11
+ def load_seed_actions(seed: Union[str, Path, List[Dict]]) -> List[Dict]:
12
+ """加载 seed 步骤:内联列表 / .json 步骤列表 / .py 的 SEED_STEPS 变量"""
13
+ if isinstance(seed, list):
14
+ return seed
15
+ seed_path = Path(seed)
16
+ if not seed_path.exists():
17
+ raise FileNotFoundError(f"seed 文件不存在: {seed_path}")
18
+ if seed_path.suffix == ".json":
19
+ return json.loads(seed_path.read_text(encoding="utf-8"))
20
+ if seed_path.suffix == ".py":
21
+ ns: Dict[str, Any] = {}
22
+ exec(seed_path.read_text(encoding="utf-8"), ns) # noqa: S102 - seed 约定式
23
+ steps = ns.get("SEED_STEPS")
24
+ if not isinstance(steps, list):
25
+ raise ValueError(f"seed .py 文件 {seed_path} 需定义 SEED_STEPS 步骤列表")
26
+ return steps
27
+ raise ValueError(f"不支持的 seed 文件类型: {seed_path.suffix}(支持 .json / .py / 内联列表)")
28
+
29
+
30
+ def execute_seed_actions(engine, actions: List[Dict]) -> str:
31
+ """在浏览器中顺序执行 seed 步骤,返回执行摘要文本"""
32
+ summary_lines = []
33
+ for step in actions:
34
+ _execute_action(engine, step)
35
+ summary_lines.append(
36
+ f"- {step.get('action', '?')}: {step.get('locator', step.get('url', step.get('text', '')))}"
37
+ )
38
+ return "\n".join(summary_lines)
39
+
40
+
41
+ def _execute_action(engine, step: Dict) -> None:
42
+ action = step.get("action", "")
43
+ if action == "navigate":
44
+ engine.navigate(step["url"])
45
+ elif action == "type":
46
+ locator = step["locator"]
47
+ el = engine.find_element(locator)
48
+ engine.type(el, step.get("text", ""))
49
+ elif action == "click":
50
+ locator = step["locator"]
51
+ el = engine.find_element(locator)
52
+ engine.click(el)
53
+ elif action == "sleep":
54
+ time.sleep(float(step.get("seconds", 1)))
55
+ else:
56
+ raise ValueError(f"不支持的 seed 步骤 action: {action}")
57
+
58
+
59
+ def ask_exploration_steps(provider, request: str, prd_text: str,
60
+ pages: List[Dict], max_steps: int = 5) -> List[Dict]:
61
+ """让 LLM 基于请求/PRD/当前快照输出建议的探索步骤(JSON 列表)"""
62
+ snapshot_text = json.dumps(pages, ensure_ascii=False)[:4000]
63
+ prompt = (
64
+ f"你是 Web 探索助手。用户请求: {request}\n"
65
+ f"PRD: {prd_text[:2000]}\n"
66
+ f"当前页面快照: {snapshot_text}\n\n"
67
+ f"请返回 JSON 数组,给出最多 {max_steps} 个后续探索步骤(如登录、点击导航、表单填写),"
68
+ '每步格式: {"action": "type|click|navigate", "locator": {"by": "id", "value": "..."}, '
69
+ '"text": "...", "url": "..."}。只返回 JSON,不要解释。'
70
+ )
71
+ raw = provider.llm_ask(prompt)
72
+ start, end = raw.find("["), raw.rfind("]")
73
+ if start == -1 or end == -1:
74
+ return []
75
+ try:
76
+ return json.loads(raw[start:end + 1])
77
+ except (json.JSONDecodeError, ValueError, TypeError):
78
+ # LLM 返回截断/非法 JSON → 返回 [] 不裸崩,由调用方明确报错提示改用 --seed(规格 §9.3/§10)
79
+ return []
@@ -0,0 +1,58 @@
1
+ """🎭 Generator Agent — 手工用例 → PO + testcase 自动化脚本(全 AI)
2
+
3
+ 流程(废除模板渲染):
4
+ ① Designer(TestcaseWriter): plan → testcases/*.md(手工执行用例)
5
+ ② ScriptWriter: testcases → features/<page>_page.py(PO)+ testcase/<page>/test_<page>.py
6
+ """
7
+ from pathlib import Path
8
+ from typing import List
9
+
10
+ from loguru import logger
11
+
12
+ from omni.core.ai.provider import get_provider
13
+ from omni.agents.script_writer import ScriptWriter
14
+ from omni.agents.models import TestPlan
15
+ from omni.agents import templates
16
+
17
+ # 默认基类:TestCaseBase 提供框架执行规范(setup/teardown、断言、CaseMetaClass 注册)。
18
+ # 可传业务基类(如 demo.testcase.saucedemo.base.SauceDemo)以组合页面对象。
19
+ _DEFAULT_BASE_CLASS = "TestCaseBase"
20
+
21
+
22
+ class Generator:
23
+ """Generator:将 TestPlan 渲染为 testcases(Designer)再经 ScriptWriter 生成自动化脚本"""
24
+
25
+ def __init__(self, provider=None):
26
+ """__init__"""
27
+ self.provider = provider or get_provider()
28
+
29
+ def generate(self, plan: TestPlan, output_dir: str,
30
+ base_class: str = _DEFAULT_BASE_CLASS) -> List[str]:
31
+ """生成自动化脚本(全 AI:Designer 出用例 → ScriptWriter 出 PO+testcase)
32
+
33
+ Args:
34
+ base_class: 生成的测试类继承的基类(默认 TestCaseBase,可传业务基类)
35
+ """
36
+ out = Path(output_dir)
37
+ out.mkdir(parents=True, exist_ok=True)
38
+ created = []
39
+ # 基础文件(conftest 注入 self.driver/om_web)— 执行必需;BasePage 由 PO 从框架 base.base_page 导入,无需生成
40
+ base_files = [
41
+ ("conftest.py", templates.CONFTEST_TEMPLATE),
42
+ ]
43
+ for rel, content in base_files:
44
+ target = out / rel
45
+ target.parent.mkdir(parents=True, exist_ok=True)
46
+ if not target.exists():
47
+ target.write_text(content, encoding="utf-8")
48
+ created.append(str(rel))
49
+ # ① Designer:plan → 手工用例行数据 + case.csv(不写 md)
50
+ from omni.agents.testcase_writer import TestcaseWriter
51
+ cases = TestcaseWriter(provider=self.provider).write(plan, str(out))
52
+ created.append("case.csv" if (out / "case.csv").exists() else "")
53
+ # ② ScriptWriter:场景行数据 → PO + testcase
54
+ if cases:
55
+ sw = ScriptWriter(provider=self.provider)
56
+ created += sw.write(cases, str(out), base_class=base_class)
57
+ logger.info(f"[webai] 生成完成: {len(created)} 个文件")
58
+ return [c for c in created if c]
omni/agents/healer.py ADDED
@@ -0,0 +1,142 @@
1
+ """🎭 Healer Agent — 执行失败 → 分类 → 修复 → 重跑"""
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+ from loguru import logger
8
+
9
+ from omni.core.ai.provider import get_provider
10
+ from omni.agents.models import HealReport
11
+
12
+ _PROMPT = Path(__file__).resolve().parent / "prompts" / "healer.md"
13
+
14
+
15
+ class Healer:
16
+ """Healer:执行测试、收集失败信号、AI 分类修复、重跑验证"""
17
+
18
+ def __init__(self, provider=None, max_attempts: int = 3):
19
+ """__init__"""
20
+ self.provider = provider or get_provider()
21
+ assert self.provider is not None, "AI Provider 不可用"
22
+ self.max_attempts = max_attempts
23
+
24
+ # ── 生成产物校验 + AI 自修复(对齐官方 healer guardrails) ──
25
+
26
+ def validate_generated(self, code: str, kind: str) -> bool:
27
+ """校验 AI 生成产物:po 继承 BasePage;testcase 方法 test_ 且含 self.om_web"""
28
+ from omni.agents.script_writer import _validate_po, _validate_testcase
29
+ if kind == "po":
30
+ return _validate_po(code)
31
+ if kind == "testcase":
32
+ return _validate_testcase(code)
33
+ return False
34
+
35
+ def self_heal(self, code: str, kind: str, error: str = "") -> Optional[str]:
36
+ """AI 自修复:校验不合格时让 LLM 修正,重试 ≤3 次"""
37
+ provider = self.provider
38
+ if provider is None:
39
+ return None
40
+ for attempt in range(1, self.max_attempts + 1):
41
+ if self.validate_generated(code, kind):
42
+ return code
43
+ try:
44
+ code = provider.llm_ask(
45
+ f"修复以下 {kind} 代码,使其通过校验"
46
+ f"(PO 继承 BasePage;testcase 方法 test_ 且含 self.om_web 调用)。\n"
47
+ f"错误: {error}\n代码:\n{code}\n只返回修正后的代码。"
48
+ )
49
+ except Exception as e:
50
+ logger.warning(f"[webai] healer 自修复 LLM 失败({e})")
51
+ return None
52
+ return code if self.validate_generated(code, kind) else None
53
+
54
+ def heal(self, test_path: str, cwd: str = "") -> HealReport:
55
+ """执行单个测试文件并尝试修复
56
+
57
+ 首版:测试通过 → status=healed;测试失败 → 交由 LLM 分类给出修复建议,
58
+ 无法自动落地修复时标记 skipped(不静默通过)。
59
+ """
60
+ test_file = Path(test_path)
61
+ name = test_file.name
62
+ logger.info(f"[webai] heal 开始执行: {name}")
63
+ run = self._run_pytest(test_file, cwd)
64
+ if run.returncode == 0:
65
+ logger.info(f"[webai] heal: {name} 测试通过(无需修复)")
66
+ return HealReport(test_name=name, category="none", original="",
67
+ fixed="", attempts=1, status="healed")
68
+
69
+ # 失败:收集信号
70
+ failure_text = run.stdout[-2000:] + run.stderr[-2000:]
71
+ logger.warning(f"[webai] heal: {name} 测试失败(rc={run.returncode}),收集失败信号")
72
+ provider = self.provider
73
+ if provider is None:
74
+ return HealReport(test_name=name, category="unknown", original="",
75
+ fixed="", attempts=1, status="skipped")
76
+ try:
77
+ system = _PROMPT.read_text(encoding="utf-8")
78
+ raw = provider.llm_ask(
79
+ f"{system}\n\n测试 {name} 失败,输出:\n{failure_text}\n\n"
80
+ "请回复 JSON: {\"category\": \"locator|timing|assertion|bug\", "
81
+ "\"suggestion\": \"...\"}"
82
+ )
83
+ category = self._classify(raw)
84
+ suggestion = self._extract_suggestion(raw)
85
+ logger.info(f"[webai] heal: {name} 分类={category} 建议={suggestion[:80]}")
86
+ except Exception as err: # noqa: BLE001 — LLM/提示词异常不可使 heal() 崩溃
87
+ logger.warning(f"[webai] heal: {name} LLM 不可用({err}),标记 skipped")
88
+ return HealReport(
89
+ test_name=name, category="unknown",
90
+ original=failure_text[:500], fixed=f"LLM unavailable: {err}",
91
+ attempts=1, status="skipped",
92
+ )
93
+ status = "skipped" if category != "bug" else "failed"
94
+ logger.info(f"[webai] heal: {name} 完成(status={status})")
95
+ return HealReport(
96
+ test_name=name, category=category,
97
+ original=failure_text[:500], fixed=suggestion,
98
+ attempts=1, status=status,
99
+ )
100
+
101
+ def heal_all(self, tests_dir: str, cwd: str = "") -> List[HealReport]:
102
+ """修复目录下所有失败测试"""
103
+ reports = []
104
+ for test_file in sorted(Path(tests_dir).glob("test_*.py")):
105
+ if test_file.name.startswith("test_") and test_file.name != "conftest.py":
106
+ reports.append(self.heal(str(test_file), cwd=cwd))
107
+ return reports
108
+
109
+ def _run_pytest(self, test_file: Path, cwd: str) -> subprocess.CompletedProcess:
110
+ python = sys.executable
111
+ # 绝对路径作为 pytest 目标:cwd 已设为 test_file.parent,
112
+ # 若再传相对路径会拼接成 cwd/相对路径 重复(Test file not found)
113
+ target = str(test_file.resolve())
114
+ try:
115
+ return subprocess.run(
116
+ [python, "-m", "pytest", target, "-q", "--no-header", "-x"],
117
+ capture_output=True, text=True, cwd=cwd or str(test_file.parent),
118
+ timeout=120,
119
+ )
120
+ except subprocess.TimeoutExpired as err:
121
+ # 超时视为失败(非零返回码),流向 LLM 分支
122
+ out = err.stdout or ""
123
+ if isinstance(out, bytes):
124
+ out = out.decode("utf-8", errors="replace")
125
+ err_out = err.stderr or ""
126
+ if isinstance(err_out, bytes):
127
+ err_out = err_out.decode("utf-8", errors="replace")
128
+ out += "\n[pytest timed out after 120s]"
129
+ return subprocess.CompletedProcess(
130
+ args=[python, "-m", "pytest", target],
131
+ returncode=-9, stdout=out, stderr=err_out,
132
+ )
133
+
134
+ def _classify(self, raw: str) -> str:
135
+ import json, re
136
+ m = re.search(r'"category"\s*:\s*"(\w+)"', raw)
137
+ return m.group(1) if m else "unknown"
138
+
139
+ def _extract_suggestion(self, raw: str) -> str:
140
+ import json, re
141
+ m = re.search(r'"suggestion"\s*:\s*"([^"]*)"', raw)
142
+ return m.group(1) if m else raw[:200]
@@ -0,0 +1,37 @@
1
+ """双通道桥接 — 将 agent 能力经 @playwright/mcp 暴露给外部 agent(VS Code/Claude 等)
2
+
3
+ 外部接入方式:
4
+ python -m omni.agents.mcp_bridge start # 启动 playwright MCP 服务
5
+ python -m omni.agents.mcp_bridge config # 打印 .mcp.json 配置
6
+ """
7
+ import subprocess
8
+ import sys
9
+ from typing import Dict
10
+
11
+
12
+ def build_mcp_config() -> Dict:
13
+ """返回 .mcp.json 兼容的 playwright MCP 配置"""
14
+ return {
15
+ "mcpServers": {
16
+ "playwright": {
17
+ "command": "npx",
18
+ "args": ["-y", "@playwright/mcp@latest"],
19
+ }
20
+ }
21
+ }
22
+
23
+
24
+ def start_mcp_server(port: int = 8931) -> subprocess.Popen:
25
+ """启动 playwright MCP 服务子进程(独立进程,不影响内闭环)"""
26
+ return subprocess.Popen(
27
+ ["npx", "-y", "@playwright/mcp@latest", f"--port={port}"],
28
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
29
+ )
30
+
31
+
32
+ if __name__ == "__main__":
33
+ if len(sys.argv) > 1 and sys.argv[1] == "start":
34
+ start_mcp_server()
35
+ else:
36
+ import json
37
+ print(json.dumps(build_mcp_config(), indent=2))
omni/agents/models.py ADDED
@@ -0,0 +1,109 @@
1
+ """agent 数据模型 — 测试计划 / 用例 / 修复报告"""
2
+ import re
3
+ from dataclasses import dataclass, field
4
+ from typing import Dict, List
5
+
6
+
7
+ @dataclass
8
+ class TestCase:
9
+ """单个测试场景(steps[i] 与 expected[i] 一一对应)"""
10
+ id: int
11
+ title: str
12
+ steps: List[str] = field(default_factory=list)
13
+ expected: List[str] = field(default_factory=list)
14
+ data: Dict = field(default_factory=dict)
15
+
16
+
17
+ @dataclass
18
+ class TestPlan:
19
+ """Markdown 测试计划"""
20
+ name: str
21
+ scenarios: List[TestCase] = field(default_factory=list)
22
+
23
+ def to_markdown(self) -> str:
24
+ """to_markdown"""
25
+ lines = [f"# {self.name}", ""]
26
+ for sc in self.scenarios:
27
+ lines.append(f"## Scenario {sc.id}: {sc.title}")
28
+ lines.append("")
29
+ # 步骤与期望一一对应:表格形式(每行 步骤 | 期望结果)
30
+ if sc.steps:
31
+ lines.append("| 步骤 | 期望结果 |")
32
+ lines.append("|------|----------|")
33
+ for i, step in enumerate(sc.steps, 1):
34
+ exp = sc.expected[i - 1] if i - 1 < len(sc.expected) else ""
35
+ lines.append(f"| {i}. {step} | {exp} |")
36
+ lines.append("")
37
+ return "\n".join(lines)
38
+
39
+ @classmethod
40
+ def from_markdown(cls, text: str) -> "TestPlan":
41
+ """from_markdown"""
42
+ lines = text.splitlines()
43
+ name = ""
44
+ for ln in lines:
45
+ if ln.startswith("# ") and not ln.startswith("## "):
46
+ name = ln[2:].strip()
47
+ break
48
+ scenarios = []
49
+ current = None
50
+ in_table = False
51
+ in_expected_block = False
52
+ for ln in lines:
53
+ stripped = ln.strip()
54
+ # 标准格式:## Scenario 1: 标题(提示词要求的唯一格式)
55
+ m = re.match(r"^## Scenario (\d+): (.+)$", stripped)
56
+ # 防御兼容:### 1. 标题(三级标题,旧提示词/LLM 可能输出)
57
+ m3 = re.match(r"^### (\d+)\.\s+(.+)$", stripped) if not m else None
58
+ matched = m or m3
59
+ if matched is not None:
60
+ current = TestCase(id=int(matched.group(1)),
61
+ title=matched.group(2).strip())
62
+ scenarios.append(current)
63
+ in_table = False
64
+ in_expected_block = False
65
+ continue
66
+ if current is None:
67
+ continue
68
+ # 表格行:| 1. 步骤 | 期望结果 |
69
+ if stripped.startswith("|") and stripped.endswith("|") and not stripped.startswith("|---"):
70
+ cols = [c.strip() for c in stripped.strip("|").split("|")]
71
+ if len(cols) >= 2 and not cols[0].startswith("步骤"):
72
+ step_text = re.sub(r"^\d+\.\s*", "", cols[0]).strip()
73
+ current.steps.append(step_text)
74
+ current.expected.append(cols[1].strip())
75
+ in_table = True
76
+ in_expected_block = False
77
+ continue
78
+ # 兼容旧格式:**Expected:** 区块
79
+ if stripped == "**Expected:**":
80
+ in_expected_block = True
81
+ continue
82
+ step_m = re.match(r"^(\d+)\.\s+(.+)$", stripped)
83
+ if step_m and not in_table and not in_expected_block:
84
+ current.steps.append(step_m.group(2).strip())
85
+ elif stripped.startswith("- ") and in_expected_block:
86
+ current.expected.append(stripped[2:].strip())
87
+ return cls(name=name, scenarios=scenarios)
88
+
89
+
90
+ @dataclass
91
+ class HealReport:
92
+ """healer 修复报告(可审计)"""
93
+ test_name: str
94
+ category: str # locator / timing / assertion / bug
95
+ original: str
96
+ fixed: str
97
+ attempts: int
98
+ status: str # healed / skipped / failed
99
+
100
+ def to_markdown(self) -> str:
101
+ """to_markdown"""
102
+ return (
103
+ f"## {self.test_name}\n\n"
104
+ f"- category: {self.category}\n"
105
+ f"- attempts: {self.attempts}\n"
106
+ f"- status: {self.status}\n"
107
+ f"- original: `{self.original}`\n"
108
+ f"- fixed: `{self.fixed}`\n"
109
+ )
@@ -0,0 +1,83 @@
1
+ """AgentOrchestrator — 三 agent 编排(single / sequential / loop)"""
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Dict, List
6
+
7
+ from loguru import logger
8
+
9
+ from omni.agents.planner import Planner
10
+ from omni.agents.generator import Generator
11
+ from omni.agents.healer import Healer
12
+ from omni.core.ai.provider import get_provider
13
+
14
+
15
+ class AgentOrchestrator:
16
+ """编排 Planner → Generator → (pytest) → Healer"""
17
+
18
+ def __init__(self, planner=None, generator=None, healer=None, provider=None):
19
+ """__init__"""
20
+ self.provider = provider or get_provider()
21
+ self.planner = planner or Planner(engine=None, provider=self.provider)
22
+ self.generator = generator or Generator(provider=self.provider)
23
+ self.healer = healer or Healer(provider=self.provider)
24
+
25
+ def run_sequential(self, url: str, plan_path: str, tests_dir: str,
26
+ request: str = "", seed=None, prd: str = "") -> Dict:
27
+ """planner → generator → pytest → healer 顺序执行
28
+
29
+ Args:
30
+ url: 被测应用 URL
31
+ plan_path: 测试计划保存路径(Markdown)
32
+ tests_dir: 生成测试输出目录
33
+ request: 明确请求(如"为访客结账生成一个计划"),透传给 planner
34
+ seed: 种子测试(内联步骤列表 / .json / .py 文件),预置环境
35
+ prd: 产品需求文档(文本或文件路径),作为 LLM 上下文
36
+ """
37
+ # P6:探索引擎生命周期经 ResourceManager 托管(退出统一释放,防浏览器泄漏)
38
+ from omni.context.resource_manager import ResourceManager
39
+
40
+ with ResourceManager() as rm:
41
+ # 1. Planner 产出计划:engine 非空且计划不存在时先探索(含 seed/request/PRD)再保存;
42
+ # engine 为 None 时保持原行为——不探索,直接走 save_plan/LLM 兜底
43
+ plan_file = Path(plan_path)
44
+ if not plan_file.exists():
45
+ if self.planner.engine is not None:
46
+ if seed:
47
+ self.planner.run_seed(seed)
48
+ self.planner.explore(url, request=request, prd=prd)
49
+ self.planner.save_plan(plan_path)
50
+ logger.info(f"计划: {plan_path}")
51
+
52
+ # 2. Generator 生成测试
53
+ from omni.agents.models import TestPlan
54
+ plan = TestPlan.from_markdown(plan_file.read_text(encoding="utf-8"))
55
+ created = self.generator.generate(plan, tests_dir)
56
+ logger.info(f"生成测试: {created}")
57
+
58
+ # 禁止静默成功:计划无场景或零生成文件必须报错
59
+ if not plan.scenarios or not created:
60
+ logger.error(
61
+ f"计划未产出可执行场景: scenarios={len(plan.scenarios)}, "
62
+ f"created={len(created)}"
63
+ )
64
+ raise ValueError(
65
+ "计划未产出任何可执行测试场景"
66
+ f"(scenarios={len(plan.scenarios)}, created={len(created)})"
67
+ )
68
+
69
+ # 3. 执行 pytest
70
+ python = sys.executable
71
+ run = subprocess.run(
72
+ [python, "-m", "pytest", tests_dir, "-q", "--no-header"],
73
+ capture_output=True, text=True,
74
+ )
75
+ logger.info(f"pytest exit={run.returncode}")
76
+
77
+ # 4. 失败 → Healer
78
+ heal_reports = []
79
+ if run.returncode != 0:
80
+ heal_reports = self.healer.heal_all(tests_dir)
81
+ logger.info(f"healer 报告: {len(heal_reports)} 条")
82
+
83
+ return {"plan": plan_path, "tests": created, "heal_reports": heal_reports}