@epoint-testtech/ep-stage-skill 0.0.4-alpha.1 → 0.0.6

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 (213) hide show
  1. package/SKILL.md +27 -27
  2. package/codex-skill/ep-stage/glue-create-project/SKILL.md +223 -206
  3. package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +275 -274
  4. package/codex-skill/ep-stage/glue-generate-testcase/examples/observable-testcase.json +40 -40
  5. package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-schema.md +95 -112
  6. package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +72 -72
  7. package/codex-skill/ep-stage/glue-run-test/SKILL.md +295 -289
  8. package/codex-skill/ep-stage/glue-run-test/references/ai-runtime-loop.md +30 -0
  9. package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +132 -145
  10. package/codex-skill/ep-stage/glue-run-test/references/gap-review-protocol.md +44 -43
  11. package/codex-skill/ep-stage/glue-run-test/references/harness-principles.md +46 -46
  12. package/codex-skill/ep-stage/glue-run-test/references/playwright-cli-odav.md +166 -0
  13. package/codex-skill/ep-stage/glue-run-test/scripts/extract-page-elements-eval.mjs +233 -0
  14. package/codex-skill/ep-stage/glue-run-test/scripts/generate-crud-spec.mjs +149 -149
  15. package/codex-skill/ep-stage/page-element-scan/SKILL.md +91 -0
  16. package/codex-skill/ep-stage/page-element-scan/evals/evals.json +37 -0
  17. package/codex-skill/ep-stage/page-element-scan/evals/fixtures/sample-list-page.html +85 -0
  18. package/codex-skill/ep-stage/page-element-scan/references/pagination-controls.md +153 -0
  19. package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements-eval.mjs +233 -0
  20. package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements.mjs +298 -0
  21. package/codex-skill/ep-stage/page-element-scan/scripts/scan-page.mjs +749 -0
  22. package/codex-skill/ep-stage/recording-to-glue/SKILL.md +28 -28
  23. package/codex-skill/ep-stage/scripts/validate-skill.mjs +98 -95
  24. package/dist/src/cli/generate-testcase.d.ts +17 -51
  25. package/dist/src/cli/generate-testcase.d.ts.map +1 -1
  26. package/dist/src/cli/generate-testcase.js +74 -181
  27. package/dist/src/cli/index.d.ts +3 -2
  28. package/dist/src/cli/index.d.ts.map +1 -1
  29. package/dist/src/cli/index.js +51 -17
  30. package/dist/src/cli/run.d.ts +136 -29
  31. package/dist/src/cli/run.d.ts.map +1 -1
  32. package/dist/src/cli/run.js +507 -65
  33. package/dist/src/cli/spec.d.ts +17 -13
  34. package/dist/src/cli/spec.d.ts.map +1 -1
  35. package/dist/src/cli/spec.js +59 -33
  36. package/dist/src/contracts/crud-business-module.d.ts +6 -0
  37. package/dist/src/contracts/crud-business-module.d.ts.map +1 -1
  38. package/dist/src/contracts/glue-contract-v1.d.ts +98 -0
  39. package/dist/src/contracts/glue-contract-v1.d.ts.map +1 -0
  40. package/dist/src/contracts/glue-contract-v1.js +9 -0
  41. package/dist/src/contracts/observable-chain.d.ts +4 -0
  42. package/dist/src/contracts/observable-chain.d.ts.map +1 -1
  43. package/dist/src/extractors/code-list-json5.d.ts +192 -0
  44. package/dist/src/extractors/code-list-json5.d.ts.map +1 -0
  45. package/dist/src/extractors/code-list-json5.js +91 -0
  46. package/dist/src/extractors/html-page.d.ts +9 -0
  47. package/dist/src/extractors/html-page.d.ts.map +1 -1
  48. package/dist/src/extractors/html-page.js +26 -2
  49. package/dist/src/extractors/prd-extractor.d.ts +70 -0
  50. package/dist/src/extractors/prd-extractor.d.ts.map +1 -0
  51. package/dist/src/extractors/prd-extractor.js +248 -0
  52. package/dist/src/generators/agent-inferred-workflow-script.js +66 -66
  53. package/dist/src/generators/stage-skeleton-script.d.ts +87 -20
  54. package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
  55. package/dist/src/generators/stage-skeleton-script.js +647 -238
  56. package/dist/src/index.d.ts +2 -2
  57. package/dist/src/index.d.ts.map +1 -1
  58. package/dist/src/index.js +1 -1
  59. package/dist/src/normalizers/crud-contract.d.ts +3 -0
  60. package/dist/src/normalizers/crud-contract.d.ts.map +1 -1
  61. package/dist/src/normalizers/crud-contract.js +3 -0
  62. package/dist/src/runtime/ai-runtime-trace.d.ts +88 -0
  63. package/dist/src/runtime/ai-runtime-trace.d.ts.map +1 -0
  64. package/dist/src/runtime/ai-runtime-trace.js +119 -0
  65. package/dist/src/testcase/button-interaction-inference.d.ts +37 -0
  66. package/dist/src/testcase/button-interaction-inference.d.ts.map +1 -0
  67. package/dist/src/testcase/button-interaction-inference.js +385 -0
  68. package/dist/src/testcase/contract-testcase-assembler.d.ts +44 -0
  69. package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -0
  70. package/dist/src/testcase/contract-testcase-assembler.js +806 -0
  71. package/dist/src/testcase/prd-button-mapper.d.ts +72 -0
  72. package/dist/src/testcase/prd-button-mapper.d.ts.map +1 -0
  73. package/dist/src/testcase/prd-button-mapper.js +148 -0
  74. package/dist/src/testcase/runtime-context.d.ts +9 -0
  75. package/dist/src/testcase/runtime-context.d.ts.map +1 -0
  76. package/dist/src/testcase/runtime-context.js +1 -0
  77. package/dist/src/testcase/test-access.d.ts +38 -0
  78. package/dist/src/testcase/test-access.d.ts.map +1 -0
  79. package/dist/src/testcase/test-access.js +106 -0
  80. package/dist/src/testcase/testcase-execution-fields.d.ts +10 -0
  81. package/dist/src/testcase/testcase-execution-fields.d.ts.map +1 -0
  82. package/dist/src/testcase/testcase-execution-fields.js +1 -0
  83. package/dist/src/testcase/testcase-generator.d.ts.map +1 -1
  84. package/dist/src/testcase/testcase-generator.js +6 -0
  85. package/dist/src/testcase/testcase-markdown.d.ts +11 -0
  86. package/dist/src/testcase/testcase-markdown.d.ts.map +1 -0
  87. package/dist/src/testcase/testcase-markdown.js +181 -0
  88. package/dist/src/testcase/testcase-skeleton.d.ts +4 -0
  89. package/dist/src/testcase/testcase-skeleton.d.ts.map +1 -1
  90. package/dist/src/testcase/testcase-spec-assembly.d.ts +4 -0
  91. package/dist/src/testcase/testcase-spec-assembly.d.ts.map +1 -1
  92. package/dist/src/testcase/testcase-v1-draft.d.ts +58 -0
  93. package/dist/src/testcase/testcase-v1-draft.d.ts.map +1 -0
  94. package/dist/src/testcase/testcase-v1-draft.js +13 -0
  95. package/dist/src/validation/contract.d.ts +14 -0
  96. package/dist/src/validation/contract.d.ts.map +1 -0
  97. package/dist/src/validation/contract.js +132 -0
  98. package/dist/src/validation/credentials.d.ts +7 -2
  99. package/dist/src/validation/credentials.d.ts.map +1 -1
  100. package/dist/src/validation/credentials.js +15 -4
  101. package/dist/src/validation/index.d.ts +1 -0
  102. package/dist/src/validation/index.d.ts.map +1 -1
  103. package/dist/src/validation/index.js +1 -0
  104. package/dist/src/validation/testcase.d.ts +9 -7
  105. package/dist/src/validation/testcase.d.ts.map +1 -1
  106. package/dist/src/validation/testcase.js +181 -34
  107. package/docs/README.md +124 -134
  108. package/docs/mvp-usage-guide.md +298 -298
  109. package/examples/schemeresource-observable-docs/schemeresource.context.md +20 -20
  110. package/examples/schemeresource.module-hints.json +38 -38
  111. package/examples/schemeresource.observable.code_list.md +37 -37
  112. package/examples/zwplace-observable-docs/zwplace.context.md +16 -16
  113. package/examples/zwplace-placecategory-validation.json +29 -29
  114. package/examples/zwplace.module-hints.json +69 -69
  115. package/examples/zwplace.observable.code_list.md +37 -37
  116. package/package.json +2 -2
  117. package/codex-skill/ep-stage/.omc/state/sessions/d636ad35-5fc9-4d5e-85b0-2bcad7d86ee5/pre-tool-advisory-throttle.json +0 -10
  118. package/dist/test/cli/extract-contract.test.d.ts +0 -2
  119. package/dist/test/cli/extract-contract.test.d.ts.map +0 -1
  120. package/dist/test/cli/extract-contract.test.js +0 -32
  121. package/dist/test/cli/generate-testcase-inference.test.d.ts +0 -2
  122. package/dist/test/cli/generate-testcase-inference.test.d.ts.map +0 -1
  123. package/dist/test/cli/generate-testcase-inference.test.js +0 -63
  124. package/dist/test/cli/generate-testcase-paths.test.d.ts +0 -2
  125. package/dist/test/cli/generate-testcase-paths.test.d.ts.map +0 -1
  126. package/dist/test/cli/generate-testcase-paths.test.js +0 -165
  127. package/dist/test/cli/generate-testcase-requirements.test.d.ts +0 -2
  128. package/dist/test/cli/generate-testcase-requirements.test.d.ts.map +0 -1
  129. package/dist/test/cli/generate-testcase-requirements.test.js +0 -229
  130. package/dist/test/cli/generate-testcase.test.d.ts +0 -2
  131. package/dist/test/cli/generate-testcase.test.d.ts.map +0 -1
  132. package/dist/test/cli/generate-testcase.test.js +0 -130
  133. package/dist/test/cli/index.test.d.ts +0 -2
  134. package/dist/test/cli/index.test.d.ts.map +0 -1
  135. package/dist/test/cli/index.test.js +0 -113
  136. package/dist/test/cli/probe.test.d.ts +0 -2
  137. package/dist/test/cli/probe.test.d.ts.map +0 -1
  138. package/dist/test/cli/probe.test.js +0 -31
  139. package/dist/test/cli/run.test.d.ts +0 -2
  140. package/dist/test/cli/run.test.d.ts.map +0 -1
  141. package/dist/test/cli/run.test.js +0 -149
  142. package/dist/test/cli/spec-boundary.test.d.ts +0 -2
  143. package/dist/test/cli/spec-boundary.test.d.ts.map +0 -1
  144. package/dist/test/cli/spec-boundary.test.js +0 -198
  145. package/dist/test/cli/spec.test.d.ts +0 -2
  146. package/dist/test/cli/spec.test.d.ts.map +0 -1
  147. package/dist/test/cli/spec.test.js +0 -196
  148. package/dist/test/code-list.test.d.ts +0 -2
  149. package/dist/test/code-list.test.d.ts.map +0 -1
  150. package/dist/test/code-list.test.js +0 -39
  151. package/dist/test/crud-contract.test.d.ts +0 -2
  152. package/dist/test/crud-contract.test.d.ts.map +0 -1
  153. package/dist/test/crud-contract.test.js +0 -799
  154. package/dist/test/gap-inference.test.d.ts +0 -2
  155. package/dist/test/gap-inference.test.d.ts.map +0 -1
  156. package/dist/test/gap-inference.test.js +0 -597
  157. package/dist/test/generalization.test.d.ts +0 -2
  158. package/dist/test/generalization.test.d.ts.map +0 -1
  159. package/dist/test/generalization.test.js +0 -73
  160. package/dist/test/inference/module-hints-inference.test.d.ts +0 -2
  161. package/dist/test/inference/module-hints-inference.test.d.ts.map +0 -1
  162. package/dist/test/inference/module-hints-inference.test.js +0 -25
  163. package/dist/test/inference/source-analysis.test.d.ts +0 -2
  164. package/dist/test/inference/source-analysis.test.d.ts.map +0 -1
  165. package/dist/test/inference/source-analysis.test.js +0 -23
  166. package/dist/test/material-inventory.test.d.ts +0 -2
  167. package/dist/test/material-inventory.test.d.ts.map +0 -1
  168. package/dist/test/material-inventory.test.js +0 -154
  169. package/dist/test/observable-chain.test.d.ts +0 -2
  170. package/dist/test/observable-chain.test.d.ts.map +0 -1
  171. package/dist/test/observable-chain.test.js +0 -123
  172. package/dist/test/observable-pipeline.test.d.ts +0 -2
  173. package/dist/test/observable-pipeline.test.d.ts.map +0 -1
  174. package/dist/test/observable-pipeline.test.js +0 -470
  175. package/dist/test/page-structure.test.d.ts +0 -2
  176. package/dist/test/page-structure.test.d.ts.map +0 -1
  177. package/dist/test/page-structure.test.js +0 -45
  178. package/dist/test/scenario-inference.test.d.ts +0 -2
  179. package/dist/test/scenario-inference.test.d.ts.map +0 -1
  180. package/dist/test/scenario-inference.test.js +0 -73
  181. package/dist/test/stage-context.test.d.ts +0 -2
  182. package/dist/test/stage-context.test.d.ts.map +0 -1
  183. package/dist/test/stage-context.test.js +0 -395
  184. package/dist/test/testcase-generator.test.d.ts +0 -2
  185. package/dist/test/testcase-generator.test.d.ts.map +0 -1
  186. package/dist/test/testcase-generator.test.js +0 -276
  187. package/dist/test/testcase-skeleton.test.d.ts +0 -2
  188. package/dist/test/testcase-skeleton.test.d.ts.map +0 -1
  189. package/dist/test/testcase-skeleton.test.js +0 -185
  190. package/dist/test/testcase-spec-assembly.test.d.ts +0 -2
  191. package/dist/test/testcase-spec-assembly.test.d.ts.map +0 -1
  192. package/dist/test/testcase-spec-assembly.test.js +0 -105
  193. package/dist/test/util/credentials.test.d.ts +0 -2
  194. package/dist/test/util/credentials.test.d.ts.map +0 -1
  195. package/dist/test/util/credentials.test.js +0 -64
  196. package/dist/test/util/i18n-testcase.test.d.ts +0 -2
  197. package/dist/test/util/i18n-testcase.test.d.ts.map +0 -1
  198. package/dist/test/util/i18n-testcase.test.js +0 -119
  199. package/dist/test/util/softlink.test.d.ts +0 -2
  200. package/dist/test/util/softlink.test.d.ts.map +0 -1
  201. package/dist/test/util/softlink.test.js +0 -82
  202. package/dist/test/validation/credentials.test.d.ts +0 -2
  203. package/dist/test/validation/credentials.test.d.ts.map +0 -1
  204. package/dist/test/validation/credentials.test.js +0 -72
  205. package/dist/test/validation/projects-index.test.d.ts +0 -2
  206. package/dist/test/validation/projects-index.test.d.ts.map +0 -1
  207. package/dist/test/validation/projects-index.test.js +0 -48
  208. package/dist/test/validation/testcase.test.d.ts +0 -2
  209. package/dist/test/validation/testcase.test.d.ts.map +0 -1
  210. package/dist/test/validation/testcase.test.js +0 -129
  211. package/dist/vitest.config.d.ts +0 -3
  212. package/dist/vitest.config.d.ts.map +0 -1
  213. package/dist/vitest.config.js +0 -7
@@ -0,0 +1,385 @@
1
+ /**
2
+ * 前端按钮交互行为推理器。
3
+ *
4
+ * 职责:
5
+ * 1. 从 code_list.json5 的 sourceRoot + pages.list.path 定位列表页 HTML;
6
+ * 2. 解析 HTML 中自定义按钮的 onclick 属性及同页内联脚本中的函数体;
7
+ * 3. 判断交互模式(确认弹窗 / toast / 异步调用 / 打开弹窗 / 通用);
8
+ * 4. 为每个 custom 按钮生成精确的中文步骤与断言;
9
+ * 5. 当 HTML 不可读、解析失败或模式未知时,回退到通用描述并记录原因。
10
+ */
11
+ import { existsSync, readFileSync } from 'node:fs';
12
+ import path from 'node:path';
13
+ import * as cheerio from 'cheerio';
14
+ /**
15
+ * 归一化文本:去空白、去首尾空格。
16
+ */
17
+ function normalizeText(value) {
18
+ return (value ?? '').replace(/\s+/g, ' ').trim();
19
+ }
20
+ /**
21
+ * 从内联脚本内容中提取指定函数体。
22
+ *
23
+ * @param scriptContent - 页面内所有 inline script 拼接文本。
24
+ * @param functionName - 函数名。
25
+ * @returns 函数体或 undefined。
26
+ */
27
+ function extractFunctionBody(scriptContent, functionName) {
28
+ const pattern = new RegExp(`function\\s+${functionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\([^)]*\\)\\s*\\{`, 'g');
29
+ const match = pattern.exec(scriptContent);
30
+ if (!match)
31
+ return undefined;
32
+ const start = match.index + match[0].length;
33
+ let depth = 1;
34
+ let index = start;
35
+ while (index < scriptContent.length && depth > 0) {
36
+ const char = scriptContent[index];
37
+ if (char === '{')
38
+ depth += 1;
39
+ else if (char === '}')
40
+ depth -= 1;
41
+ index += 1;
42
+ }
43
+ return scriptContent.slice(start, index - 1);
44
+ }
45
+ /**
46
+ * 从合并后的 JS 文本中识别交互模式。
47
+ *
48
+ * 优先级:
49
+ * 1. epoint.confirm -> confirm_then_execute
50
+ * 2. epoint.openTopDialog / epoint.openDialog -> open_dialog
51
+ * 3. epoint.execute / epoint.ajax / $.ajax / .ajax( / XMLHttpRequest / fetch( -> async_call
52
+ * 4. epoint.message / epoint.showTips / epoint.alert -> toast
53
+ * 5. 其他 -> generic
54
+ */
55
+ function detectPattern(combinedJs) {
56
+ const normalized = combinedJs.replace(/\s+/g, ' ');
57
+ if (/epoint\.confirm\s*\(/.test(normalized))
58
+ return 'confirm_then_execute';
59
+ if (/epoint\.(?:openTopDialog|openDialog)\s*\(/.test(normalized))
60
+ return 'open_dialog';
61
+ if (/epoint\.(?:execute|ajax)\s*\(/.test(normalized) ||
62
+ /\$\.ajax\s*\(/.test(normalized) ||
63
+ /[^a-zA-Z0-9_$]ajax\s*\(/.test(normalized) ||
64
+ /XMLHttpRequest/.test(normalized) ||
65
+ /[^a-zA-Z0-9_$]fetch\s*\(/.test(normalized)) {
66
+ return 'async_call';
67
+ }
68
+ if (/epoint\.(?:message|showTips|alert)\s*\(/.test(normalized))
69
+ return 'toast';
70
+ return 'generic';
71
+ }
72
+ /**
73
+ * 尝试提取 epoint.confirm 的第一个参数(提示文案)。
74
+ *
75
+ * @param combinedJs - 合并后的 JS 文本。
76
+ * @returns 提取到的文案或 undefined。
77
+ */
78
+ function extractConfirmMessage(combinedJs) {
79
+ const match = combinedJs.match(/epoint\.confirm\s*\(\s*["']([^"']+)["']/);
80
+ return match?.[1];
81
+ }
82
+ /**
83
+ * 根据交互模式渲染中文步骤和断言。
84
+ */
85
+ function renderStepsAndAssertion(label, pattern, confirmMessage) {
86
+ if (pattern === 'confirm_then_execute') {
87
+ const confirmStep = confirmMessage
88
+ ? `2. 页面弹出确认弹窗,文案包含「${confirmMessage}」。`
89
+ : '2. 页面弹出确认弹窗。';
90
+ return {
91
+ steps: [
92
+ `1. 在列表页定位并点击「${label}」按钮。`,
93
+ confirmStep,
94
+ `3. 在确认弹窗中点击「确定」。`,
95
+ `4. 等待后端请求完成。`,
96
+ `5. 观察并记录最终反馈(提示信息、弹窗或列表刷新)。`,
97
+ ].join(' '),
98
+ assertion: `点击「${label}」并确认后,系统应调用对应接口并给出可见成功反馈;若接口返回错误,应展示错误提示。`,
99
+ };
100
+ }
101
+ if (pattern === 'toast') {
102
+ return {
103
+ steps: [
104
+ `1. 在列表页定位并点击「${label}」按钮。`,
105
+ `2. 观察页面是否出现 toast / 提示信息。`,
106
+ `3. 记录提示文案与页面状态变化。`,
107
+ ].join(' '),
108
+ assertion: `点击「${label}」按钮后,页面应展示可见的提示信息。`,
109
+ };
110
+ }
111
+ if (pattern === 'async_call') {
112
+ return {
113
+ steps: [
114
+ `1. 在列表页定位并点击「${label}」按钮。`,
115
+ `2. 等待异步请求完成。`,
116
+ `3. 观察并记录页面反馈(提示信息、列表刷新等)。`,
117
+ ].join(' '),
118
+ assertion: `点击「${label}」按钮后,系统应发起异步请求并给出成功或错误反馈。`,
119
+ };
120
+ }
121
+ if (pattern === 'open_dialog') {
122
+ return {
123
+ steps: [
124
+ `1. 在列表页定位并点击「${label}」按钮。`,
125
+ `2. 等待弹窗或新页面打开。`,
126
+ `3. 确认弹窗/页面标题与内容符合预期。`,
127
+ ].join(' '),
128
+ assertion: `点击「${label}」按钮后,应打开对应的弹窗或页面。`,
129
+ };
130
+ }
131
+ return {
132
+ steps: [
133
+ `1. 在列表页定位并点击「${label}」按钮。`,
134
+ `2. 观察并记录页面反馈(如弹窗、提示信息、列表刷新等)。`,
135
+ `3. 确认页面状态与按钮功能预期一致。`,
136
+ ].join(' '),
137
+ assertion: `点击「${label}」按钮后,页面应出现与该功能对应的可见响应(如提示信息、弹窗确认或数据刷新)。`,
138
+ };
139
+ }
140
+ /**
141
+ * 从 code_list.pages[list].buttons 中提取 action='custom' 的按钮。
142
+ */
143
+ function collectCustomButtons(codeList) {
144
+ const listPage = codeList.pages.find((p) => p.role === 'list');
145
+ if (!listPage)
146
+ return [];
147
+ return (listPage.buttons ?? []).reduce((acc, raw) => {
148
+ const anyB = raw;
149
+ if (anyB.action === 'custom' && anyB.label) {
150
+ acc.push({
151
+ label: anyB.label,
152
+ customAction: anyB.customAction ?? anyB.label,
153
+ backendMethod: anyB.backendMethod,
154
+ });
155
+ }
156
+ return acc;
157
+ }, []);
158
+ }
159
+ /**
160
+ * 解析 HTML 中 .mini-button 按钮,按 label 建立映射。
161
+ */
162
+ function buildHtmlButtonMap(html) {
163
+ const $ = cheerio.load(html);
164
+ const map = new Map();
165
+ $('.mini-button').each((_, element) => {
166
+ const button = $(element);
167
+ const label = normalizeText(button.text());
168
+ if (!label)
169
+ return;
170
+ map.set(label, {
171
+ label,
172
+ onclick: button.attr('onclick') ?? undefined,
173
+ });
174
+ });
175
+ return map;
176
+ }
177
+ /**
178
+ * 收集页面中所有 inline script 内容。
179
+ */
180
+ function collectInlineScripts(html) {
181
+ const $ = cheerio.load(html);
182
+ return $('script')
183
+ .toArray()
184
+ .map((element) => $(element).html() ?? '')
185
+ .join('\n');
186
+ }
187
+ /**
188
+ * 收集页面中外部 script src 的内容。
189
+ *
190
+ * @param html - HTML 文本。
191
+ * @param htmlDir - HTML 文件所在目录,用于解析相对 src。
192
+ * @returns 所有可读外部脚本内容拼接。
193
+ */
194
+ function collectExternalScripts(html, htmlDir) {
195
+ const $ = cheerio.load(html);
196
+ const contents = [];
197
+ $('script[src]').each((_, element) => {
198
+ const src = $(element).attr('src');
199
+ if (!src)
200
+ return;
201
+ // 忽略远程脚本和 data URI。
202
+ if (/^(https?:|data:|\/\/)/i.test(src))
203
+ return;
204
+ const scriptPath = path.resolve(htmlDir, src);
205
+ if (!existsSync(scriptPath))
206
+ return;
207
+ try {
208
+ contents.push(readFileSync(scriptPath, 'utf8'));
209
+ }
210
+ catch {
211
+ // 忽略无法读取的外部脚本。
212
+ }
213
+ });
214
+ return contents.join('\n');
215
+ }
216
+ /**
217
+ * 从 onclick 值中提取可能调用的函数名。
218
+ *
219
+ * 兼容形式:
220
+ * - `openAdd()`
221
+ * - `syncWindowSystem`
222
+ * - `epoint.confirm('...', cb)`(返回 epoint,会被过滤)
223
+ *
224
+ * @param onclick - onclick 属性值。
225
+ * @returns 函数名或 undefined。
226
+ */
227
+ function extractFunctionNameFromOnclick(onclick) {
228
+ const trimmed = onclick.trim();
229
+ // 1. 优先匹配 functionName(...)
230
+ const callMatch = trimmed.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/);
231
+ if (callMatch)
232
+ return callMatch[1];
233
+ // 2. 兼容裸函数名赋值,如 onclick="syncWindowSystem"
234
+ const bareMatch = trimmed.match(/^([A-Za-z_$][A-Za-z0-9_$]*)$/);
235
+ if (bareMatch)
236
+ return bareMatch[1];
237
+ return undefined;
238
+ }
239
+ /**
240
+ * 选择并返回可用的源码根目录。
241
+ *
242
+ * 优先级:options.sourceRootOverride -> process.env.EP_STAGE_SOURCE_ROOT -> codeList.sourceRoot.root
243
+ */
244
+ function resolveSourceRoot(codeList, options) {
245
+ const candidates = [
246
+ options?.sourceRootOverride,
247
+ process.env.EP_STAGE_SOURCE_ROOT,
248
+ codeList.sourceRoot?.root,
249
+ ].filter((item) => Boolean(item));
250
+ return candidates[0];
251
+ }
252
+ /**
253
+ * 推理所有 custom 按钮的交互行为。
254
+ *
255
+ * @param codeList - 解析后的 code_list.json5。
256
+ * @param options - 可选覆盖。
257
+ * @returns label -> 推理结果 的映射。
258
+ */
259
+ export function inferButtonInteractions(codeList, options) {
260
+ const customButtons = collectCustomButtons(codeList);
261
+ const result = new Map();
262
+ if (customButtons.length === 0)
263
+ return result;
264
+ const listPage = codeList.pages.find((p) => p.role === 'list');
265
+ const listPath = listPage?.path;
266
+ const sourceRoot = resolveSourceRoot(codeList, options);
267
+ if (!listPath || !sourceRoot) {
268
+ const reason = !listPath
269
+ ? 'code_list.json5 缺少 pages[role=list].path'
270
+ : '未提供可用的源码根目录(sourceRoot.root / EP_STAGE_SOURCE_ROOT / --source-root)';
271
+ for (const btn of customButtons) {
272
+ const generic = renderStepsAndAssertion(btn.label, 'generic');
273
+ result.set(btn.label, {
274
+ label: btn.label,
275
+ pattern: 'generic',
276
+ steps: generic.steps,
277
+ assertion: generic.assertion,
278
+ evidence: [],
279
+ fallbackReason: reason,
280
+ });
281
+ }
282
+ return result;
283
+ }
284
+ const htmlPath = path.resolve(sourceRoot, listPath);
285
+ if (!existsSync(htmlPath)) {
286
+ const reason = `列表页 HTML 不存在: ${htmlPath}`;
287
+ for (const btn of customButtons) {
288
+ const generic = renderStepsAndAssertion(btn.label, 'generic');
289
+ result.set(btn.label, {
290
+ label: btn.label,
291
+ pattern: 'generic',
292
+ steps: generic.steps,
293
+ assertion: generic.assertion,
294
+ evidence: [],
295
+ fallbackReason: reason,
296
+ });
297
+ }
298
+ return result;
299
+ }
300
+ let html;
301
+ try {
302
+ html = readFileSync(htmlPath, 'utf8');
303
+ }
304
+ catch (err) {
305
+ const reason = `读取列表页 HTML 失败: ${err instanceof Error ? err.message : String(err)}`;
306
+ for (const btn of customButtons) {
307
+ const generic = renderStepsAndAssertion(btn.label, 'generic');
308
+ result.set(btn.label, {
309
+ label: btn.label,
310
+ pattern: 'generic',
311
+ steps: generic.steps,
312
+ assertion: generic.assertion,
313
+ evidence: [],
314
+ fallbackReason: reason,
315
+ });
316
+ }
317
+ return result;
318
+ }
319
+ let htmlButtonMap;
320
+ let scriptContent;
321
+ try {
322
+ htmlButtonMap = buildHtmlButtonMap(html);
323
+ const inlineScripts = collectInlineScripts(html);
324
+ const externalScripts = collectExternalScripts(html, path.dirname(htmlPath));
325
+ scriptContent = `${inlineScripts}\n${externalScripts}`;
326
+ }
327
+ catch (err) {
328
+ const reason = `解析 HTML 失败: ${err instanceof Error ? err.message : String(err)}`;
329
+ for (const btn of customButtons) {
330
+ const generic = renderStepsAndAssertion(btn.label, 'generic');
331
+ result.set(btn.label, {
332
+ label: btn.label,
333
+ pattern: 'generic',
334
+ steps: generic.steps,
335
+ assertion: generic.assertion,
336
+ evidence: [],
337
+ fallbackReason: reason,
338
+ });
339
+ }
340
+ return result;
341
+ }
342
+ for (const btn of customButtons) {
343
+ const htmlBtn = htmlButtonMap.get(btn.label);
344
+ if (!htmlBtn || !htmlBtn.onclick) {
345
+ const generic = renderStepsAndAssertion(btn.label, 'generic');
346
+ result.set(btn.label, {
347
+ label: btn.label,
348
+ pattern: 'generic',
349
+ steps: generic.steps,
350
+ assertion: generic.assertion,
351
+ evidence: [],
352
+ fallbackReason: !htmlBtn
353
+ ? `HTML 中未找到 label 为「${btn.label}」的按钮`
354
+ : `HTML 中按钮「${btn.label}」缺少 onclick 属性`,
355
+ });
356
+ continue;
357
+ }
358
+ const onclick = htmlBtn.onclick;
359
+ const functionName = extractFunctionNameFromOnclick(onclick);
360
+ let functionBody = '';
361
+ if (functionName && functionName !== 'epoint') {
362
+ functionBody = extractFunctionBody(scriptContent, functionName) ?? '';
363
+ }
364
+ const combinedJs = `${onclick}\n${functionBody}`;
365
+ const pattern = detectPattern(combinedJs);
366
+ const confirmMessage = pattern === 'confirm_then_execute' ? extractConfirmMessage(combinedJs) : undefined;
367
+ const rendered = renderStepsAndAssertion(btn.label, pattern, confirmMessage);
368
+ const evidence = [onclick];
369
+ if (functionName)
370
+ evidence.push(`functionName=${functionName}`);
371
+ if (functionBody)
372
+ evidence.push(functionBody.slice(0, 500));
373
+ if (confirmMessage)
374
+ evidence.push(`confirmMessage=${confirmMessage}`);
375
+ result.set(btn.label, {
376
+ label: btn.label,
377
+ pattern,
378
+ functionName,
379
+ steps: rendered.steps,
380
+ assertion: rendered.assertion,
381
+ evidence,
382
+ });
383
+ }
384
+ return result;
385
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * JSON5 新协议产线的 contract + testcase 装配器(Task 3)。
3
+ *
4
+ * 职责:
5
+ * 把 {@link ExtractedCodeListJson5}(code_list.json5 解析产出)装配成
6
+ * - {@link EpStageContractV1Draft}:含 scenario.skeletonSlots.crud(形态对齐
7
+ * generators/stage-skeleton-script.ts 的 StageSkeletonCrudFlowSlots)
8
+ * - {@link EpStageTestcaseV1Draft}:10 条 glue cases + uncoveredCandidates
9
+ *
10
+ * 填充策略(brief + 交接文档 §4.2):
11
+ * - queryFields:input/listbox 二分;input 取 entities 中 dataKey 字段作 seed 来源
12
+ * (placename 为 dataKey),listbox 取第一项作 value
13
+ * - frames:code_list 不提供 iframe keyword → 用模块名相关 keyword 占位(014 为 directUrl 模式),
14
+ * 默认策略,不进 unresolvedSlots,在 inference-trace 记账
15
+ * - locators:用 014 草案已确认的框架默认(MiniUI 默认规则 + text=确定 等),不进 unresolvedSlots
16
+ * - assertions.deletePolicy='success_delete'(MiniUI 默认删除成功策略)
17
+ */
18
+ import type { ExtractedCodeListJson5 } from '../extractors/code-list-json5.js';
19
+ import type { EpStageContractV1Draft } from '../contracts/glue-contract-v1.js';
20
+ import type { EpStageTestcaseV1Draft } from './testcase-v1-draft.js';
21
+ /** assembler 输入。 */
22
+ export type AssembleInput = {
23
+ /** code_list.json5 解析后的强类型产出。 */
24
+ codeList: ExtractedCodeListJson5;
25
+ /** 待测列表页直达地址,由 skill 交互或运行配置显式传入。 */
26
+ directUrl?: string;
27
+ /** 源码根目录覆盖(可选),用于解析 pages.list.path。 */
28
+ sourceRoot?: string;
29
+ };
30
+ /** assembler 输出:契约 + 用例 + 推理过程账本(后两者供 generate-testcase 落盘)。 */
31
+ export type AssembleResult = {
32
+ contract: EpStageContractV1Draft;
33
+ testcase: EpStageTestcaseV1Draft;
34
+ /** inference-trace 的 reasoningSummary + skeletonSlots 默认策略来源账本。 */
35
+ inferenceTrace: Record<string, unknown>;
36
+ };
37
+ /**
38
+ * 装配 contract + testcase + inference-trace 账本。
39
+ *
40
+ * @param input - 含 code_list 解析产出的输入。
41
+ * @returns contract / testcase / inferenceTrace。
42
+ */
43
+ export declare function assembleJson5ContractAndTestcase(input: AssembleInput): AssembleResult;
44
+ //# sourceMappingURL=contract-testcase-assembler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-testcase-assembler.d.ts","sourceRoot":"","sources":["../../../src/testcase/contract-testcase-assembler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAA2E,MAAM,kCAAkC,CAAC;AACxJ,OAAO,KAAK,EAAE,sBAAsB,EAAqB,MAAM,kCAAkC,CAAC;AAElG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AASrE,oBAAoB;AACpB,MAAM,MAAM,aAAa,GAAG;IAC1B,iCAAiC;IACjC,QAAQ,EAAE,sBAAsB,CAAC;IACjC,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,gEAAgE;AAChE,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,sBAAsB,CAAC;IACjC,QAAQ,EAAE,sBAAsB,CAAC;IACjC,mEAAmE;IACnE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC,CAAC;AAyyBF;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,aAAa,GAAG,cAAc,CAiKrF"}