@guanlin4924/devflow-pi 0.1.94 → 0.1.96
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.
- package/README.md +34 -4
- package/node_modules/@playwright/test/LICENSE +202 -0
- package/node_modules/@playwright/test/NOTICE +5 -0
- package/node_modules/@playwright/test/README.md +318 -0
- package/node_modules/@playwright/test/cli.js +19 -0
- package/node_modules/@playwright/test/index.d.ts +18 -0
- package/node_modules/@playwright/test/index.js +17 -0
- package/node_modules/@playwright/test/index.mjs +18 -0
- package/node_modules/@playwright/test/package.json +35 -0
- package/node_modules/@playwright/test/reporter.d.ts +17 -0
- package/node_modules/@playwright/test/reporter.js +17 -0
- package/node_modules/@playwright/test/reporter.mjs +17 -0
- package/node_modules/playwright/LICENSE +202 -0
- package/node_modules/playwright/NOTICE +5 -0
- package/node_modules/playwright/README.md +318 -0
- package/node_modules/playwright/ThirdPartyNotices.txt +14 -0
- package/node_modules/playwright/cli.js +19 -0
- package/node_modules/playwright/index.d.ts +17 -0
- package/node_modules/playwright/index.js +17 -0
- package/node_modules/playwright/index.mjs +18 -0
- package/node_modules/playwright/jsx-runtime.js +42 -0
- package/node_modules/playwright/jsx-runtime.mjs +21 -0
- package/node_modules/playwright/lib/agents/agentParser.js +89 -0
- package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +34 -0
- package/node_modules/playwright/lib/agents/generateAgents.js +395 -0
- package/node_modules/playwright/lib/agents/playwright-test-coverage.prompt.md +31 -0
- package/node_modules/playwright/lib/agents/playwright-test-generate.prompt.md +8 -0
- package/node_modules/playwright/lib/agents/playwright-test-generator.agent.md +88 -0
- package/node_modules/playwright/lib/agents/playwright-test-heal.prompt.md +6 -0
- package/node_modules/playwright/lib/agents/playwright-test-healer.agent.md +56 -0
- package/node_modules/playwright/lib/agents/playwright-test-plan.prompt.md +9 -0
- package/node_modules/playwright/lib/agents/playwright-test-planner.agent.md +74 -0
- package/node_modules/playwright/lib/cli/reportActions.js +78 -0
- package/node_modules/playwright/lib/cli/testActions.js +214 -0
- package/node_modules/playwright/lib/common/index.js +2923 -0
- package/node_modules/playwright/lib/common/index.js.txt +34 -0
- package/node_modules/playwright/lib/errorContext.js +129 -0
- package/node_modules/playwright/lib/globals.js +58 -0
- package/node_modules/playwright/lib/index.js +815 -0
- package/node_modules/playwright/lib/isomorphic.js +260 -0
- package/node_modules/playwright/lib/isomorphic.js.txt +9 -0
- package/node_modules/playwright/lib/loader/loaderProcessEntry.js +34 -0
- package/node_modules/playwright/lib/loader/loaderProcessEntry.js.txt +9 -0
- package/node_modules/playwright/lib/matchers/expect.js +13490 -0
- package/node_modules/playwright/lib/matchers/expect.js.LICENSE +640 -0
- package/node_modules/playwright/lib/matchers/expect.js.txt +67 -0
- package/node_modules/playwright/lib/mcp/test/browserBackend.js +135 -0
- package/node_modules/playwright/lib/mcp/test/generatorTools.js +122 -0
- package/node_modules/playwright/lib/mcp/test/plannerTools.js +150 -0
- package/node_modules/playwright/lib/mcp/test/seed.js +82 -0
- package/node_modules/playwright/lib/mcp/test/streams.js +44 -0
- package/node_modules/playwright/lib/mcp/test/testBackend.js +99 -0
- package/node_modules/playwright/lib/mcp/test/testContext.js +306 -0
- package/node_modules/playwright/lib/mcp/test/testTool.js +30 -0
- package/node_modules/playwright/lib/mcp/test/testTools.js +98 -0
- package/node_modules/playwright/lib/package.js +47 -0
- package/node_modules/playwright/lib/program.js +241 -0
- package/node_modules/playwright/lib/runner/index.js +8527 -0
- package/node_modules/playwright/lib/runner/index.js.txt +61 -0
- package/node_modules/playwright/lib/transform/babelBundle.js +72274 -0
- package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +2384 -0
- package/node_modules/playwright/lib/transform/babelBundle.js.txt +325 -0
- package/node_modules/playwright/lib/transform/esmLoader.js +8054 -0
- package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +355 -0
- package/node_modules/playwright/lib/transform/esmLoader.js.txt +60 -0
- package/node_modules/playwright/lib/util.js +373 -0
- package/node_modules/playwright/lib/worker/workerProcessEntry.js +1861 -0
- package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +19 -0
- package/node_modules/playwright/package.json +55 -0
- package/node_modules/playwright/test.d.ts +18 -0
- package/node_modules/playwright/test.js +24 -0
- package/node_modules/playwright/test.mjs +35 -0
- package/node_modules/playwright/types/test.d.ts +11074 -0
- package/node_modules/playwright/types/testReporter.d.ts +950 -0
- package/node_modules/playwright-core/LICENSE +202 -0
- package/node_modules/playwright-core/NOTICE +5 -0
- package/node_modules/playwright-core/README.md +3 -0
- package/node_modules/playwright-core/ThirdPartyNotices.txt +13 -0
- package/node_modules/playwright-core/bin/install_media_pack.ps1 +5 -0
- package/node_modules/playwright-core/bin/install_webkit_wsl.ps1 +33 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh +42 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh +13 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 +24 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh +42 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh +12 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 +24 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh +48 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh +11 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 +23 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh +48 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh +11 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 +23 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh +48 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh +11 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 +24 -0
- package/node_modules/playwright-core/browsers.json +52 -0
- package/node_modules/playwright-core/cli.js +21 -0
- package/node_modules/playwright-core/index.d.ts +17 -0
- package/node_modules/playwright-core/index.js +17 -0
- package/node_modules/playwright-core/index.mjs +28 -0
- package/node_modules/playwright-core/lib/bootstrap.js +88 -0
- package/node_modules/playwright-core/lib/coreBundle.js +75813 -0
- package/node_modules/playwright-core/lib/entry/cliDaemon.js +5 -0
- package/node_modules/playwright-core/lib/entry/dashboardApp.js +3 -0
- package/node_modules/playwright-core/lib/entry/mcp.js +10 -0
- package/node_modules/playwright-core/lib/entry/oopBrowserDownload.js +3 -0
- package/node_modules/playwright-core/lib/package.js +50 -0
- package/node_modules/playwright-core/lib/server/chromium/appIcon.png +0 -0
- package/node_modules/playwright-core/lib/server/electron/loader.js +116 -0
- package/node_modules/playwright-core/lib/serverRegistry.js +1939 -0
- package/node_modules/playwright-core/lib/serverRegistry.js.LICENSE +65 -0
- package/node_modules/playwright-core/lib/tools/cli-client/channelSessions.js +141 -0
- package/node_modules/playwright-core/lib/tools/cli-client/cli.js +6 -0
- package/node_modules/playwright-core/lib/tools/cli-client/help.json +721 -0
- package/node_modules/playwright-core/lib/tools/cli-client/minimist.js +128 -0
- package/node_modules/playwright-core/lib/tools/cli-client/output.js +351 -0
- package/node_modules/playwright-core/lib/tools/cli-client/program.js +418 -0
- package/node_modules/playwright-core/lib/tools/cli-client/registry.js +180 -0
- package/node_modules/playwright-core/lib/tools/cli-client/session.js +259 -0
- package/node_modules/playwright-core/lib/tools/dashboard/appIcon.png +0 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/SKILL.md +425 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/element-attributes.md +23 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/request-mocking.md +87 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/running-code.md +241 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/session-management.md +225 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/storage-state.md +275 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/test-generation.md +433 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/tracing.md +139 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/video-recording.md +143 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +136 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +89 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +69 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/typing.md +173 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +77 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-trace/SKILL.md +174 -0
- package/node_modules/playwright-core/lib/tools/utils/extension.js +112 -0
- package/node_modules/playwright-core/lib/tools/utils/socketConnection.js +108 -0
- package/node_modules/playwright-core/lib/utilsBundle.js +85357 -0
- package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +1890 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule--QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule-CQ8RZGBm.js +32 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf +0 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-AwgwcYie.css +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-hRvO4yU2.js +12 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/safari-na3_-uQk.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/index.html +29 -0
- package/node_modules/playwright-core/lib/vite/dashboard/playwright-logo.svg +24 -0
- package/node_modules/playwright-core/lib/vite/htmlReport/index.html +16 -0
- package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -0
- package/node_modules/playwright-core/lib/vite/htmlReport/report.js +32 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CVQWJZAA.js +32 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf +0 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-BZpYJZ-P.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-hrlLqLtq.js +129 -0
- package/node_modules/playwright-core/lib/vite/recorder/index.html +29 -0
- package/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg +9 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-BbkfBe3n.js +32 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Ds6CBOo0.js +182 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-DywYcAf8.js +7 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/codicon.DCmgc-ay.ttf +0 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.Bqk9acqE.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.B4cLoZK3.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.html +44 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/manifest.webmanifest +16 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/playwright-logo.svg +9 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +10 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +4 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CU5KtEkS.js +5 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CyMfwkXJ.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +18 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
- package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
- package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
- package/node_modules/playwright-core/lib/xdg-open +1267 -0
- package/node_modules/playwright-core/package.json +34 -0
- package/node_modules/playwright-core/types/protocol.d.ts +24645 -0
- package/node_modules/playwright-core/types/structs.d.ts +52 -0
- package/node_modules/playwright-core/types/types.d.ts +26632 -0
- package/package.json +13 -2
- package/plugins/devflow-core/extensions/devflow-core.ts +432 -7
- package/plugins/devflow-core/runtime/hooks/hook_common.py +514 -0
- package/plugins/devflow-core/runtime/hooks/index_gate.py +47 -16
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +488 -71
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +51 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_pins.py +433 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_store.py +137 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_coverage.py +87 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_draft.py +53 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_v1.py +318 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/environment_profile.py +134 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +1 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_surface.py +138 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/playwright_adapter.py +515 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tc_hash.py +132 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_command_policy.py +102 -8
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +13 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +13 -34
- package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +3 -1
- package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +68 -140
- package/plugins/devflow-core/skills/workflow-project-context/scripts/validate_initialization_database.py +1 -1
- package/plugins/devflow-implementation/skills/workflow-execute-phase/references/execute-phase.md +3 -1
- package/plugins/devflow-intel/extensions/devflow-intel.ts +1 -1
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +23 -4
- package/plugins/devflow-quality/runtime/playwright/devflow-test-dsl-v1.schema.json +872 -0
- package/plugins/devflow-quality/runtime/playwright/playwright.config.mjs +46 -0
- package/plugins/devflow-quality/runtime/playwright/reporter.mjs +311 -0
- package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +1308 -0
- package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +11 -0
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +3 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +71 -58
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/api-mainflow-rules.md +20 -15
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/codebase-discovery.md +3 -1
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +115 -192
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/mainflow-selection.md +27 -23
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/project-dsl-contract.md +78 -153
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/ui-mainflow-rules.md +36 -83
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +497 -556
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/export_legacy_testagent.py +647 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/normalize_case_payloads.py +10 -3
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/validate_case_payloads.py +165 -275
- package/plugins/devflow-quality/skills/workflow-human-acceptance/SKILL.md +63 -0
- package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +4 -4
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +17 -9
- package/src/approval-store.ts +340 -0
- package/src/cli.ts +10 -0
- package/src/constants.ts +1 -1
- package/src/doctor.ts +268 -2
- package/src/initialization-store.ts +48 -1
- package/src/mcp/auth.ts +103 -0
- package/src/mcp/core.ts +102 -0
- package/src/mcp/server.ts +193 -0
- package/src/mcp/types.ts +5 -0
- package/src/paths.ts +12 -1
- package/src/project-context.ts +24 -1
- package/templates/omp/hook-config.json +6 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""LEGACY: export case descriptions as TestAgent import payloads (project_id + dsl_json).
|
|
3
|
+
|
|
4
|
+
This exporter is NOT part of the Devflow acceptance gate. Its output uses the
|
|
5
|
+
old TestAgent DSL (structured_first + Midscene fallback, ai_only verification,
|
|
6
|
+
xpath/near_text targets) and can never be bound to ``devflow_playwright``,
|
|
7
|
+
approved as an execution plan, or counted as an acceptance result. The gate
|
|
8
|
+
uses ``compile_dsl.py`` (devflow-test-dsl/v1, one file per frozen TC).
|
|
9
|
+
|
|
10
|
+
Run it only when a developer explicitly asks for a TestAgent import file:
|
|
11
|
+
|
|
12
|
+
python3 export_legacy_testagent.py legacy-case-descriptions.json \
|
|
13
|
+
--output .devflow/planning/generated-cases/<feature>/legacy-testagent-import.json
|
|
14
|
+
|
|
15
|
+
The input is the legacy simplified format (``text:`` / ``placeholder:`` /
|
|
16
|
+
``near:`` locators, ``verify`` steps, optional ``auth``), not the v1
|
|
17
|
+
``case-descriptions.json`` consumed by ``compile_dsl.py``.
|
|
18
|
+
"""Compile AI-friendly case descriptions into strict DSL JSON.
|
|
19
|
+
|
|
20
|
+
AI 产出简化的用例描述(locator 用 "type:value" 字符串格式),
|
|
21
|
+
本脚本负责编译成符合系统 DslTarget 严格格式的 DSL JSON。
|
|
22
|
+
这是硬约束层 —— 所有 target key、strategy、assertion 由脚本决定,AI 无法编造。
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import re
|
|
30
|
+
from copy import deepcopy
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# 硬约束常量 — 唯一权威定义
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
VALID_TARGET_KEYS = {"css", "xpath", "label", "placeholder", "role", "name", "near_text", "exact"}
|
|
40
|
+
EXACT_LOCATOR_KEYS = {"css", "xpath", "role"} # 精确匹配,必须有 source evidence
|
|
41
|
+
SEMANTIC_LOCATOR_KEYS = {"label", "placeholder", "near_text"} # 语义匹配,需检查唯一性
|
|
42
|
+
|
|
43
|
+
# 高风险 label — 表格行常见操作文本,用 label 定位时几乎一定不唯一
|
|
44
|
+
HIGH_RISK_LABELS = {
|
|
45
|
+
"编辑", "删除", "查看", "详情", "启用", "禁用", "停用",
|
|
46
|
+
"新增", "添加", "修改", "复制", "移动", "导出", "导入", "下载",
|
|
47
|
+
"上传", "设置", "配置", "操作", "更多", "取消", "确定", "保存",
|
|
48
|
+
"提交", "重置", "刷新", "搜索", "查询", "筛选", "排序",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
MIN_CSS_LENGTH = 10
|
|
52
|
+
|
|
53
|
+
DEFAULT_UI_STRATEGY = {"mode": "structured_first", "fallback": "midscene"}
|
|
54
|
+
AI_ONLY_STRATEGY = {"mode": "ai_only"}
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Locator 字符串解析器
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
LOCATOR_RE = re.compile(
|
|
61
|
+
r"^(?P<type>text|placeholder|css|xpath|role|near)"
|
|
62
|
+
r":(?P<value>.+)$"
|
|
63
|
+
)
|
|
64
|
+
ROLE_LOCATOR_RE = re.compile(r"^role:(?P<role>[^:]+):(?P<name>.+)$")
|
|
65
|
+
SCOPE_RE = re.compile(r"^row:(?P<row>\d+)$")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_locator(raw: str) -> dict[str, str]:
|
|
69
|
+
"""Parse 'type:value' string into flat target dict.
|
|
70
|
+
|
|
71
|
+
Returns {'label': '...'} for text:, {'placeholder': '...'} for placeholder:, etc.
|
|
72
|
+
Raises ValueError on unrecognized format.
|
|
73
|
+
"""
|
|
74
|
+
raw = raw.strip()
|
|
75
|
+
|
|
76
|
+
# role:button:确定
|
|
77
|
+
m = ROLE_LOCATOR_RE.match(raw)
|
|
78
|
+
if m:
|
|
79
|
+
return {"role": m.group("role"), "name": m.group("name")}
|
|
80
|
+
|
|
81
|
+
m = LOCATOR_RE.match(raw)
|
|
82
|
+
if not m:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"Invalid locator format: '{raw}'. "
|
|
85
|
+
f"Expected: text:<text> | placeholder:<text> | css:<selector> | "
|
|
86
|
+
f"xpath:<expr> | role:<role>:<name> | near:<text>"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
loc_type = m.group("type")
|
|
90
|
+
value = m.group("value").strip()
|
|
91
|
+
|
|
92
|
+
if not value:
|
|
93
|
+
raise ValueError(f"Locator '{raw}' has empty value.")
|
|
94
|
+
|
|
95
|
+
if loc_type == "text":
|
|
96
|
+
return {"label": value}
|
|
97
|
+
if loc_type == "placeholder":
|
|
98
|
+
return {"placeholder": value}
|
|
99
|
+
if loc_type == "css":
|
|
100
|
+
return {"css": value}
|
|
101
|
+
if loc_type == "xpath":
|
|
102
|
+
return {"xpath": value}
|
|
103
|
+
if loc_type == "near":
|
|
104
|
+
return {"near_text": value}
|
|
105
|
+
|
|
106
|
+
raise ValueError(f"Unknown locator type: {loc_type}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# CSS 编造检测 — 零容忍
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
# 明显是 AI 编造的 CSS class 特征
|
|
114
|
+
HALLUCINATED_CSS_PATTERNS: list[tuple[str, str]] = [
|
|
115
|
+
# 包含 % 的伪 Tailwind class(w-100%, h-50% 等)
|
|
116
|
+
(r"\b[a-z]+-\d+%", "contains invented utility class with % (e.g. w-100%)"),
|
|
117
|
+
# 裸的百分比值
|
|
118
|
+
(r"%", "contains '%' — not valid in CSS selector without escaping"),
|
|
119
|
+
# 纯表单容器通用类,不指定具体字段
|
|
120
|
+
(r"^\.el-form-item(__content)?$", "matches all form items, not a specific field"),
|
|
121
|
+
# 明显编造的随机 class 名模式
|
|
122
|
+
(r"\b[a-z]{1,3}-\d{3,}\b", "looks like a made-up class (random letters+numbers)"),
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
# fill/select 动作在非 placeholder 情况下,CSS 禁止匹配纯容器类
|
|
126
|
+
GENERIC_FORM_CLASSES = [
|
|
127
|
+
".el-form-item",
|
|
128
|
+
".el-form-item__content",
|
|
129
|
+
".el-form-item__label",
|
|
130
|
+
".ant-form-item",
|
|
131
|
+
".ant-form-item-control",
|
|
132
|
+
".form-item",
|
|
133
|
+
".form-group",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _validate_css_not_hallucinated(css_value: str, step_name: str) -> None:
|
|
138
|
+
"""Reject CSS selectors that show signs of AI hallucination. Hard gate."""
|
|
139
|
+
errors: list[str] = []
|
|
140
|
+
|
|
141
|
+
# Check against hallucination patterns
|
|
142
|
+
for pattern, description in HALLUCINATED_CSS_PATTERNS:
|
|
143
|
+
if re.search(pattern, css_value):
|
|
144
|
+
errors.append(
|
|
145
|
+
f"CSS selector '{css_value}' rejected: {description}. "
|
|
146
|
+
f"Step: {step_name}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Check if CSS is purely generic form classes (no specific target)
|
|
150
|
+
stripped = css_value.strip()
|
|
151
|
+
parts = [p.strip() for p in stripped.split() if p.strip()]
|
|
152
|
+
if parts and all(
|
|
153
|
+
any(p == g or p.startswith(g) for g in GENERIC_FORM_CLASSES)
|
|
154
|
+
for p in parts
|
|
155
|
+
):
|
|
156
|
+
errors.append(
|
|
157
|
+
f"CSS selector '{css_value}' only matches generic form containers, "
|
|
158
|
+
f"not a specific field. Use 'near:<label文本>' instead. "
|
|
159
|
+
f"Step: {step_name}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Too short = probably made up
|
|
163
|
+
if len(css_value) < MIN_CSS_LENGTH:
|
|
164
|
+
errors.append(
|
|
165
|
+
f"CSS selector '{css_value}' is too generic ({len(css_value)} chars, min {MIN_CSS_LENGTH}). "
|
|
166
|
+
f"Step: {step_name}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if errors:
|
|
170
|
+
raise ValueError("CSS HALLUCINATION DETECTED:\n" + "\n".join(f" - {e}" for e in errors))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Fill 动作 locator 硬决策链
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
FILL_ALLOWED_LOCATORS = {"placeholder", "near"} # fill 只能用这两个
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _validate_fill_locator(step_name: str, locator_raw: str) -> None:
|
|
181
|
+
"""Hard decision chain for fill actions: placeholder → near → ai_only. CSS/xpath forbidden."""
|
|
182
|
+
if not locator_raw:
|
|
183
|
+
raise ValueError(
|
|
184
|
+
f"Fill step '{step_name}' has no locator. "
|
|
185
|
+
f"Use 'placeholder:<text>', 'near:<label文本>', or change action to 'act' with ai_only."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
loc_type = locator_raw.split(":", 1)[0].strip()
|
|
189
|
+
if loc_type not in FILL_ALLOWED_LOCATORS:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"Fill step '{step_name}' uses locator type '{loc_type}', which is forbidden for fill actions. "
|
|
192
|
+
f"Fill hard chain: placeholder → near → ai_only. "
|
|
193
|
+
f"Got '{locator_raw}'. "
|
|
194
|
+
f"Use 'placeholder:<text>' if the field has a placeholder, "
|
|
195
|
+
f"'near:<label文本>' if there's a label next to the field, "
|
|
196
|
+
f"or change action to 'act' with ai_only strategy."
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
# Target 编译 — 硬约束规则
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
def compile_target(
|
|
205
|
+
locator_raw: str,
|
|
206
|
+
scope: str | None = None,
|
|
207
|
+
step_name: str = "",
|
|
208
|
+
) -> dict[str, Any]:
|
|
209
|
+
"""Compile a locator string + scope into a valid DslTarget dict.
|
|
210
|
+
|
|
211
|
+
Rules (in priority order):
|
|
212
|
+
1. text:<unique_button> + page scope → label + exact
|
|
213
|
+
2. text:<common_action> + row:N scope → label + css row selector
|
|
214
|
+
3. text:<text> + dialog scope → label (dialog内默认唯一)
|
|
215
|
+
4. placeholder:<text> → placeholder
|
|
216
|
+
5. css:<selector> → css (validated for specificity)
|
|
217
|
+
6. role:<role>:<name> → role + name
|
|
218
|
+
7. near:<text> → near_text
|
|
219
|
+
8. xpath:<expr> → xpath (last resort)
|
|
220
|
+
"""
|
|
221
|
+
target = parse_locator(locator_raw)
|
|
222
|
+
errors: list[str] = []
|
|
223
|
+
|
|
224
|
+
# text: → label
|
|
225
|
+
if "label" in target:
|
|
226
|
+
label_value = target["label"]
|
|
227
|
+
|
|
228
|
+
# Determine scope
|
|
229
|
+
if scope:
|
|
230
|
+
scope_match = SCOPE_RE.match(scope)
|
|
231
|
+
if scope_match:
|
|
232
|
+
# row scope: label + css row selector
|
|
233
|
+
row_n = int(scope_match.group("row"))
|
|
234
|
+
target["css"] = (
|
|
235
|
+
f".el-table__body tr:nth-child({row_n}) "
|
|
236
|
+
f"td:last-child .el-button--small"
|
|
237
|
+
)
|
|
238
|
+
elif scope == "dialog":
|
|
239
|
+
# dialog scope: label within dialog, no extra css needed
|
|
240
|
+
pass
|
|
241
|
+
# other scopes just use label as-is
|
|
242
|
+
|
|
243
|
+
# exact match for page-unique items
|
|
244
|
+
if label_value not in HIGH_RISK_LABELS:
|
|
245
|
+
target["exact"] = True
|
|
246
|
+
|
|
247
|
+
# High-risk label without row scope → warn but don't block
|
|
248
|
+
if label_value in HIGH_RISK_LABELS and not (scope and SCOPE_RE.match(scope)):
|
|
249
|
+
# Still produce valid target, but record warning
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
# css: → hard hallucination check
|
|
253
|
+
if "css" in target:
|
|
254
|
+
css_value = target["css"]
|
|
255
|
+
_validate_css_not_hallucinated(css_value, step_name)
|
|
256
|
+
|
|
257
|
+
# role: → ensure name is present (already handled by parse_locator)
|
|
258
|
+
|
|
259
|
+
if errors:
|
|
260
|
+
raise ValueError("\n".join(errors))
|
|
261
|
+
|
|
262
|
+
return target
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# Strategy 决策 — 硬约束
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
VERIFICATION_ACTIONS = {"verify", "assert_text", "assert_visible", "assert_url_contains", "assert_any_visible"}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def compile_strategy(action: str) -> dict[str, Any]:
|
|
273
|
+
"""Decide strategy based on action type. Hard rule: verification → ai_only."""
|
|
274
|
+
if action in VERIFICATION_ACTIONS:
|
|
275
|
+
return deepcopy(AI_ONLY_STRATEGY)
|
|
276
|
+
if action == "act":
|
|
277
|
+
return deepcopy(AI_ONLY_STRATEGY)
|
|
278
|
+
return deepcopy(DEFAULT_UI_STRATEGY)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
# Step 编译
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
def compile_step(step: dict[str, Any], index: int) -> dict[str, Any]:
|
|
286
|
+
"""Compile one simplified step into strict DSL step."""
|
|
287
|
+
name = str(step.get("name") or f"Step {index}")
|
|
288
|
+
action = str(step.get("action") or "").lower().strip()
|
|
289
|
+
|
|
290
|
+
compiled: dict[str, Any] = {
|
|
291
|
+
"id": f"s{index:02d}",
|
|
292
|
+
"name": name,
|
|
293
|
+
"depends_on": [],
|
|
294
|
+
"timeout_ms": int(step.get("timeout_ms", 30_000)),
|
|
295
|
+
"on_failure": step.get("on_failure", "stop"),
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# --- Navigate → web/goto ---
|
|
299
|
+
if action == "navigate":
|
|
300
|
+
compiled["driver"] = "web"
|
|
301
|
+
compiled["action"] = "goto"
|
|
302
|
+
compiled["params"] = {"url": step.get("url", "")}
|
|
303
|
+
return compiled
|
|
304
|
+
|
|
305
|
+
# --- Wait ---
|
|
306
|
+
if action == "wait":
|
|
307
|
+
compiled["driver"] = "web"
|
|
308
|
+
compiled["action"] = "wait"
|
|
309
|
+
compiled["params"] = {"wait_for": step.get("wait_for", "load")}
|
|
310
|
+
compiled["timeout_ms"] = int(step.get("timeout_ms", 15_000))
|
|
311
|
+
return compiled
|
|
312
|
+
|
|
313
|
+
# --- Inject token ---
|
|
314
|
+
if action == "inject_token":
|
|
315
|
+
compiled["driver"] = "web"
|
|
316
|
+
compiled["action"] = "evaluate_js"
|
|
317
|
+
compiled["params"] = {"script": step.get("script", "")}
|
|
318
|
+
return compiled
|
|
319
|
+
|
|
320
|
+
# --- API step ---
|
|
321
|
+
if action in {"get", "post", "put", "patch", "delete"}:
|
|
322
|
+
compiled["driver"] = "api"
|
|
323
|
+
compiled["action"] = action
|
|
324
|
+
compiled["params"] = deepcopy(step.get("params", {}))
|
|
325
|
+
compiled["assertions"] = _compile_api_assertions(step.get("assertions", []))
|
|
326
|
+
compiled["extract_vars"] = deepcopy(step.get("extract_vars", {}))
|
|
327
|
+
return compiled
|
|
328
|
+
|
|
329
|
+
# --- UI action steps ---
|
|
330
|
+
if action in {"click", "fill", "select", "hover"}:
|
|
331
|
+
compiled["driver"] = "ui"
|
|
332
|
+
compiled["action"] = action
|
|
333
|
+
locator_raw = step.get("locator", "")
|
|
334
|
+
if locator_raw:
|
|
335
|
+
compiled["target"] = compile_target(
|
|
336
|
+
locator_raw,
|
|
337
|
+
scope=step.get("scope"),
|
|
338
|
+
step_name=name,
|
|
339
|
+
)
|
|
340
|
+
compiled["strategy"] = compile_strategy(action)
|
|
341
|
+
if action == "fill":
|
|
342
|
+
compiled["params"] = {"value": step.get("value", "")}
|
|
343
|
+
_validate_fill_locator(name, locator_raw)
|
|
344
|
+
return compiled
|
|
345
|
+
|
|
346
|
+
# --- UI verification step ---
|
|
347
|
+
if action == "verify":
|
|
348
|
+
compiled["driver"] = "ui"
|
|
349
|
+
compiled["action"] = "act"
|
|
350
|
+
locator_raw = step.get("locator", "")
|
|
351
|
+
if locator_raw:
|
|
352
|
+
compiled["target"] = compile_target(locator_raw, scope=step.get("scope"), step_name=name)
|
|
353
|
+
compiled["strategy"] = deepcopy(AI_ONLY_STRATEGY)
|
|
354
|
+
expect = step.get("expect", "")
|
|
355
|
+
compiled["prompt"] = _build_verify_prompt(name, expect, step)
|
|
356
|
+
compiled["timeout_ms"] = int(step.get("timeout_ms", 15_000))
|
|
357
|
+
return compiled
|
|
358
|
+
|
|
359
|
+
# --- AI act step (with prompt) ---
|
|
360
|
+
if action == "act":
|
|
361
|
+
compiled["driver"] = "ui"
|
|
362
|
+
compiled["action"] = "act"
|
|
363
|
+
locator_raw = step.get("locator", "")
|
|
364
|
+
if locator_raw:
|
|
365
|
+
compiled["target"] = compile_target(locator_raw, scope=step.get("scope"), step_name=name)
|
|
366
|
+
compiled["strategy"] = deepcopy(AI_ONLY_STRATEGY)
|
|
367
|
+
compiled["prompt"] = step.get("prompt", "")
|
|
368
|
+
compiled["timeout_ms"] = int(step.get("timeout_ms", 15_000))
|
|
369
|
+
return compiled
|
|
370
|
+
|
|
371
|
+
# Fallback
|
|
372
|
+
raise ValueError(f"Unknown action '{action}' in step '{name}'")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _compile_api_assertions(raw_assertions: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
376
|
+
"""Compile simplified API assertions into strict format."""
|
|
377
|
+
compiled: list[dict[str, Any]] = []
|
|
378
|
+
for a in raw_assertions:
|
|
379
|
+
atype = str(a.get("type", "status_code"))
|
|
380
|
+
compiled.append({
|
|
381
|
+
"type": atype,
|
|
382
|
+
"expected": a.get("expected"),
|
|
383
|
+
"path": a.get("path"),
|
|
384
|
+
})
|
|
385
|
+
return compiled
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _build_verify_prompt(step_name: str, expect: str, step: dict[str, Any]) -> str:
|
|
389
|
+
"""Build AI prompt for verification step."""
|
|
390
|
+
if step.get("prompt"):
|
|
391
|
+
return step["prompt"]
|
|
392
|
+
if expect.startswith("text:"):
|
|
393
|
+
text = expect[5:]
|
|
394
|
+
return f"检查页面是否出现'{text}'的提示消息,确认操作已成功完成"
|
|
395
|
+
if expect == "dialog_visible":
|
|
396
|
+
return "检查对话框是否已打开并显示"
|
|
397
|
+
if expect == "dialog_closed":
|
|
398
|
+
return "检查对话框是否已关闭"
|
|
399
|
+
return f"验证:{expect}"
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ---------------------------------------------------------------------------
|
|
403
|
+
# Auth setup 编译
|
|
404
|
+
# ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
def compile_auth_steps(auth: dict[str, Any] | None, base_url: str = "${base_url}") -> list[dict[str, Any]]:
|
|
407
|
+
"""Compile auth config into API login + web inject steps."""
|
|
408
|
+
if not auth:
|
|
409
|
+
return []
|
|
410
|
+
|
|
411
|
+
method = str(auth.get("method", "")).lower()
|
|
412
|
+
steps: list[dict[str, Any]] = []
|
|
413
|
+
|
|
414
|
+
if method == "api_login":
|
|
415
|
+
login_url = auth.get("login_url", f"{base_url}/auth/login_by_id")
|
|
416
|
+
login_body = auth.get("login_body", {})
|
|
417
|
+
token_path = auth.get("token_path", "headers.X-Jwt-Token")
|
|
418
|
+
app_id = auth.get("app_id")
|
|
419
|
+
|
|
420
|
+
steps.append({
|
|
421
|
+
"id": "api_auth_login",
|
|
422
|
+
"name": "获取JWT令牌",
|
|
423
|
+
"driver": "api",
|
|
424
|
+
"action": "post",
|
|
425
|
+
"params": {"url": login_url, "json": login_body},
|
|
426
|
+
"assertions": [
|
|
427
|
+
{"type": "status_code", "expected": 200},
|
|
428
|
+
{"type": "json_path_exists", "path": token_path},
|
|
429
|
+
],
|
|
430
|
+
"extract_vars": {"jwt_token": {"path": token_path, "source": "jsonpath"}},
|
|
431
|
+
"depends_on": [],
|
|
432
|
+
"timeout_ms": 15_000,
|
|
433
|
+
"on_failure": "stop",
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
if app_id:
|
|
437
|
+
steps.append({
|
|
438
|
+
"id": "api_app_token",
|
|
439
|
+
"name": "获取应用访问Token",
|
|
440
|
+
"driver": "api",
|
|
441
|
+
"action": "post",
|
|
442
|
+
"params": {
|
|
443
|
+
"url": auth.get("app_token_url", f"{base_url}/admin/app/access"),
|
|
444
|
+
"json": {"id": app_id},
|
|
445
|
+
"headers": {"Authorization": "Bearer ${jwt_token}"},
|
|
446
|
+
},
|
|
447
|
+
"assertions": [{"type": "status_code", "expected": 200}],
|
|
448
|
+
"extract_vars": {"token_url": {"path": "body.data.url", "source": "jsonpath"}},
|
|
449
|
+
"depends_on": ["api_auth_login"],
|
|
450
|
+
"timeout_ms": 15_000,
|
|
451
|
+
"on_failure": "stop",
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
steps.append({
|
|
455
|
+
"id": "web_auth_inject",
|
|
456
|
+
"name": "Token注入",
|
|
457
|
+
"driver": "web",
|
|
458
|
+
"action": "goto",
|
|
459
|
+
"params": {"url": "${token_url}"},
|
|
460
|
+
"depends_on": ["api_app_token"],
|
|
461
|
+
"timeout_ms": 30_000,
|
|
462
|
+
"on_failure": "stop",
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
return steps
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# ---------------------------------------------------------------------------
|
|
469
|
+
# Main compile entry
|
|
470
|
+
# ---------------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
def _check_terminology(steps: list[dict[str, Any]]) -> list[str]:
|
|
473
|
+
"""Check for terminology hallucination in step names vs locator values."""
|
|
474
|
+
# Commonly hallucinated term pairs: (AI-invented, source-code-actual)
|
|
475
|
+
TERM_PAIRS: list[tuple[str, str]] = [
|
|
476
|
+
("搜索", "查询"),
|
|
477
|
+
("搜索", "筛选"),
|
|
478
|
+
("新增", "添加"),
|
|
479
|
+
("编辑", "修改"),
|
|
480
|
+
("删除", "移除"),
|
|
481
|
+
("保存", "提交"),
|
|
482
|
+
("关闭", "取消"),
|
|
483
|
+
]
|
|
484
|
+
warnings: list[str] = []
|
|
485
|
+
for step in steps:
|
|
486
|
+
name = str(step.get("name", ""))
|
|
487
|
+
locator_raw = str(step.get("_locator_raw", ""))
|
|
488
|
+
for ai_term, src_term in TERM_PAIRS:
|
|
489
|
+
if ai_term in name and src_term in locator_raw:
|
|
490
|
+
warnings.append(
|
|
491
|
+
f"Step '{name}' uses '{ai_term}' but locator contains '{src_term}'. "
|
|
492
|
+
f"Verify the button text in source code — use the exact source text."
|
|
493
|
+
)
|
|
494
|
+
return warnings
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def compile_case(case_desc: dict[str, Any]) -> dict[str, Any]:
|
|
498
|
+
"""Compile one simplified case description into strict DSL."""
|
|
499
|
+
name = str(case_desc.get("name") or "Unnamed Case")
|
|
500
|
+
tags = list(case_desc.get("tags") or ["ui-mainflow"])
|
|
501
|
+
variables = dict(case_desc.get("variables") or {})
|
|
502
|
+
base_url = variables.get("base_url", "${base_url}")
|
|
503
|
+
|
|
504
|
+
# Compile auth
|
|
505
|
+
auth_steps = compile_auth_steps(case_desc.get("auth"), base_url)
|
|
506
|
+
|
|
507
|
+
# Compile business steps
|
|
508
|
+
raw_steps = list(case_desc.get("steps") or [])
|
|
509
|
+
business_steps: list[dict[str, Any]] = []
|
|
510
|
+
for i, raw_step in enumerate(raw_steps):
|
|
511
|
+
compiled_step = compile_step(raw_step, i + 1)
|
|
512
|
+
# Preserve original locator string for terminology checking
|
|
513
|
+
compiled_step["_locator_raw"] = raw_step.get("locator", "")
|
|
514
|
+
business_steps.append(compiled_step)
|
|
515
|
+
|
|
516
|
+
# Terminology check (warning, not error)
|
|
517
|
+
term_warnings = _check_terminology(business_steps)
|
|
518
|
+
if term_warnings:
|
|
519
|
+
for w in term_warnings:
|
|
520
|
+
print(f"TERMINOLOGY WARNING: {w}", file=__import__("sys").stderr)
|
|
521
|
+
|
|
522
|
+
# Merge: auth steps first, then business steps
|
|
523
|
+
all_steps = auth_steps + business_steps
|
|
524
|
+
|
|
525
|
+
# Wire dependencies: auth steps are already wired; business steps depend on previous
|
|
526
|
+
first_business_idx = len(auth_steps)
|
|
527
|
+
for i, step in enumerate(all_steps):
|
|
528
|
+
if i >= first_business_idx and i > 0:
|
|
529
|
+
prev_id = all_steps[i - 1]["id"]
|
|
530
|
+
if not step.get("depends_on"):
|
|
531
|
+
step["depends_on"] = [prev_id]
|
|
532
|
+
|
|
533
|
+
dsl = {
|
|
534
|
+
"name": name,
|
|
535
|
+
"description": case_desc.get("description", ""),
|
|
536
|
+
"tags": tags,
|
|
537
|
+
"variables": variables,
|
|
538
|
+
"steps": all_steps,
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
"project_id": int(case_desc.get("project_id", 1)),
|
|
543
|
+
"name": name,
|
|
544
|
+
"description": case_desc.get("description", ""),
|
|
545
|
+
"tags": tags,
|
|
546
|
+
"dsl_json": dsl,
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def compile_all(payload: dict[str, Any], frozen_mainflow_tc_ids: set[str] | None = None) -> dict[str, Any]:
|
|
551
|
+
"""Compile all cases in the payload."""
|
|
552
|
+
metadata = dict(payload.get("metadata") or {})
|
|
553
|
+
descriptions = list(payload.get("cases", []))
|
|
554
|
+
tc_ids: list[str] = []
|
|
555
|
+
if frozen_mainflow_tc_ids is not None:
|
|
556
|
+
for index, description in enumerate(descriptions):
|
|
557
|
+
tc_id = description.get("tc_id") if isinstance(description, dict) else None
|
|
558
|
+
if not isinstance(tc_id, str) or tc_id not in frozen_mainflow_tc_ids:
|
|
559
|
+
raise ValueError(f"cases[{index}].tc_id must reference a frozen api/page TC that is positive")
|
|
560
|
+
tc_ids.append(tc_id)
|
|
561
|
+
if len(tc_ids) != len(set(tc_ids)):
|
|
562
|
+
raise ValueError("each frozen positive api/page TC may generate at most one mainflow case")
|
|
563
|
+
cases = [compile_case(c) for c in descriptions]
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
"metadata": {
|
|
567
|
+
**metadata,
|
|
568
|
+
**({"frozen_tc_ids": tc_ids} if frozen_mainflow_tc_ids is not None else {}),
|
|
569
|
+
"compiled_at": datetime.now(timezone.utc).isoformat(),
|
|
570
|
+
"compiled_by": "export_legacy_testagent.py (legacy, not an acceptance gate)",
|
|
571
|
+
"legacy_export": True,
|
|
572
|
+
},
|
|
573
|
+
"cases": cases,
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def frozen_mainflow_tc_ids_from_cwd(cwd: Path) -> set[str] | None:
|
|
578
|
+
"""Return the frozen API/page suite for new workflows, else preserve legacy compilation."""
|
|
579
|
+
state_path = cwd / ".devflow/planning" / "STATE.md"
|
|
580
|
+
if not state_path.is_file():
|
|
581
|
+
return None
|
|
582
|
+
match = re.search(r"^current_feature:\s*['\"]?([^'\"\n]+)", state_path.read_text(encoding="utf-8"), re.MULTILINE)
|
|
583
|
+
if not match:
|
|
584
|
+
return None
|
|
585
|
+
suite_path = cwd / ".devflow/planning" / "requirements" / match.group(1).strip() / "tests.json"
|
|
586
|
+
if not suite_path.is_file():
|
|
587
|
+
return None
|
|
588
|
+
suite = json.loads(suite_path.read_text(encoding="utf-8"))
|
|
589
|
+
if suite.get("frozen") is not True:
|
|
590
|
+
raise ValueError("current TC suite exists but is not frozen and fresh")
|
|
591
|
+
eligible = {
|
|
592
|
+
node.get("id")
|
|
593
|
+
for node in suite.get("nodes", [])
|
|
594
|
+
if isinstance(node, dict)
|
|
595
|
+
and node.get("kind") == "test_case"
|
|
596
|
+
and node.get("status") == "confirmed"
|
|
597
|
+
and isinstance(node.get("data"), dict)
|
|
598
|
+
and node["data"].get("test_type") in {"api", "page"}
|
|
599
|
+
# A generated API TC with an error-code oracle is a negative/exception
|
|
600
|
+
# test. It belongs to run-acceptance-tests, never to the positive
|
|
601
|
+
# mainflow importer even though its broad test_type is "api".
|
|
602
|
+
and not (
|
|
603
|
+
isinstance(node["data"].get("generated_case_key"), str)
|
|
604
|
+
and isinstance(node["data"].get("execution"), dict)
|
|
605
|
+
and isinstance(node["data"]["execution"].get("expect"), dict)
|
|
606
|
+
and "error_code" in node["data"]["execution"]["expect"]
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
return {value for value in eligible if isinstance(value, str)}
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
# ---------------------------------------------------------------------------
|
|
613
|
+
# CLI
|
|
614
|
+
# ---------------------------------------------------------------------------
|
|
615
|
+
|
|
616
|
+
def main() -> int:
|
|
617
|
+
parser = argparse.ArgumentParser(
|
|
618
|
+
allow_abbrev=False,
|
|
619
|
+
description="LEGACY (not a gate): export case descriptions as TestAgent import payloads."
|
|
620
|
+
)
|
|
621
|
+
parser.add_argument("input", help="Legacy simplified case descriptions JSON")
|
|
622
|
+
parser.add_argument("--output", required=True, help="Output path for the legacy TestAgent import payload")
|
|
623
|
+
args = parser.parse_args()
|
|
624
|
+
|
|
625
|
+
input_path = Path(args.input)
|
|
626
|
+
output_path = Path(args.output)
|
|
627
|
+
|
|
628
|
+
raw = json.loads(input_path.read_text(encoding="utf-8"))
|
|
629
|
+
|
|
630
|
+
# Accept both array and {cases: [...]} format
|
|
631
|
+
if isinstance(raw, list):
|
|
632
|
+
raw = {"metadata": {}, "cases": raw}
|
|
633
|
+
|
|
634
|
+
try:
|
|
635
|
+
result = compile_all(raw, frozen_mainflow_tc_ids_from_cwd(Path.cwd()))
|
|
636
|
+
except ValueError as exc:
|
|
637
|
+
print(f"COMPILE ERROR: {exc}", file=__import__("sys").stderr)
|
|
638
|
+
return 1
|
|
639
|
+
|
|
640
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
641
|
+
output_path.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
642
|
+
print(json.dumps({"output": str(output_path), "case_count": len(result["cases"])}, ensure_ascii=False))
|
|
643
|
+
return 0
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
if __name__ == "__main__":
|
|
647
|
+
raise SystemExit(main())
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""LEGACY: normalize draft TestAgent case payloads into a TestAgent import payload.
|
|
3
|
+
|
|
4
|
+
Not part of the Devflow acceptance gate and not used by the workflow. The
|
|
5
|
+
output (``dsl_json`` with structured_first/Midscene strategies) is the legacy
|
|
6
|
+
TestAgent import format: it can never be bound to ``devflow_playwright``,
|
|
7
|
+
approved as an execution plan, or counted as an acceptance result. The gate
|
|
8
|
+
DSL is produced by ``compile_dsl.py`` (devflow-test-dsl/v1).
|
|
9
|
+
"""
|
|
3
10
|
|
|
4
11
|
from __future__ import annotations
|
|
5
12
|
|
|
@@ -140,9 +147,9 @@ def normalize_case(case: dict[str, Any], metadata: dict[str, Any]) -> tuple[dict
|
|
|
140
147
|
|
|
141
148
|
|
|
142
149
|
def main() -> int:
|
|
143
|
-
parser = argparse.ArgumentParser()
|
|
150
|
+
parser = argparse.ArgumentParser(allow_abbrev=False)
|
|
144
151
|
parser.add_argument("input", help="legacy raw case payload JSON")
|
|
145
|
-
parser.add_argument("--output", required=True, help="
|
|
152
|
+
parser.add_argument("--output", required=True, help="legacy TestAgent import payload path")
|
|
146
153
|
args = parser.parse_args()
|
|
147
154
|
|
|
148
155
|
draft_path = Path(args.input)
|