@kkeve/suite 0.0.15 → 0.0.18

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 (115) hide show
  1. package/dist/bin/keve.js +2 -1
  2. package/dist/commands/clean.js +2 -2
  3. package/dist/commands/config.js +7 -7
  4. package/dist/commands/init.js +2 -2
  5. package/dist/commands/report.js +7 -7
  6. package/dist/commands/run.js +12 -11
  7. package/dist/commands/test.js +7 -7
  8. package/dist/config.js +7 -7
  9. package/dist/core/action-log-writer.js +4 -1
  10. package/dist/core/decorator/keve-decorators.d.ts +128 -0
  11. package/dist/core/decorator/keve-decorators.js +254 -0
  12. package/dist/core/decorator/keve-host.d.ts +40 -0
  13. package/dist/core/decorator/keve-host.js +33 -0
  14. package/dist/core/{keve-registry.d.ts → decorator/keve-registry.d.ts} +19 -0
  15. package/dist/core/{keve-registry.js → decorator/keve-registry.js} +13 -1
  16. package/dist/core/generateReport/report.d.ts +1 -1
  17. package/dist/core/generateReport/report.js +2 -2
  18. package/dist/core/generateReport/reportData.d.ts +3 -0
  19. package/dist/core/generateReport/reportData.js +49 -15
  20. package/dist/core/judge/confidence.d.ts +119 -0
  21. package/dist/core/judge/confidence.js +295 -0
  22. package/dist/core/judge/goal-heuristics.d.ts +32 -0
  23. package/dist/core/judge/goal-heuristics.js +128 -0
  24. package/dist/core/judge/pngCodec.d.ts +23 -0
  25. package/dist/core/judge/pngCodec.js +215 -0
  26. package/dist/core/judge/screenshotDiff.d.ts +112 -0
  27. package/dist/core/judge/screenshotDiff.js +420 -0
  28. package/dist/core/page-object/definePage.d.ts +95 -0
  29. package/dist/core/page-object/definePage.js +62 -0
  30. package/dist/core/page-object/elementResolver.d.ts +32 -0
  31. package/dist/core/page-object/elementResolver.js +236 -0
  32. package/dist/core/page-object/engine-types.d.ts +140 -0
  33. package/dist/core/page-object/engine-types.js +34 -0
  34. package/dist/core/sso-cookie.d.ts +18 -0
  35. package/dist/core/sso-cookie.js +10 -0
  36. package/dist/dsl/index.d.ts +15 -0
  37. package/dist/dsl/index.js +29 -0
  38. package/dist/dsl/locatorSource.d.ts +25 -0
  39. package/dist/dsl/locatorSource.js +323 -0
  40. package/dist/dsl/ops.d.ts +46 -0
  41. package/dist/dsl/ops.js +95 -0
  42. package/dist/dsl/toDecorator.d.ts +98 -0
  43. package/dist/dsl/toDecorator.js +747 -0
  44. package/dist/engine-cypress/adapter.d.ts +108 -0
  45. package/dist/engine-cypress/adapter.js +514 -0
  46. package/dist/engine-cypress/agent-page.d.ts +159 -0
  47. package/dist/engine-cypress/agent-page.js +566 -0
  48. package/dist/engine-cypress/agentDom.d.ts +22 -0
  49. package/dist/engine-cypress/agentDom.js +590 -0
  50. package/dist/engine-cypress/bridge.d.ts +39 -0
  51. package/dist/engine-cypress/bridge.js +598 -0
  52. package/dist/engine-cypress/cdp.d.ts +78 -0
  53. package/dist/engine-cypress/cdp.js +339 -0
  54. package/dist/engine-cypress/host.d.ts +22 -0
  55. package/dist/engine-cypress/host.js +480 -0
  56. package/dist/engine-cypress/index.d.ts +26 -0
  57. package/dist/engine-cypress/index.js +58 -0
  58. package/dist/engine-cypress/keveGoal.d.ts +43 -0
  59. package/dist/engine-cypress/keveGoal.js +265 -0
  60. package/dist/engine-cypress/pageOps.d.ts +41 -0
  61. package/dist/engine-cypress/pageOps.js +439 -0
  62. package/dist/engine-cypress/reporter.d.ts +20 -0
  63. package/dist/engine-cypress/reporter.js +128 -0
  64. package/dist/engine-cypress/setup.d.ts +21 -0
  65. package/dist/engine-cypress/setup.js +27 -0
  66. package/dist/engine-cypress/shotBuffer.d.ts +40 -0
  67. package/dist/engine-cypress/shotBuffer.js +41 -0
  68. package/dist/engine-cypress/state.d.ts +56 -0
  69. package/dist/engine-cypress/state.js +81 -0
  70. package/dist/engine-playwright/engineAdapter.d.ts +78 -0
  71. package/dist/engine-playwright/engineAdapter.js +371 -0
  72. package/dist/engine-playwright/global-setup.d.ts +28 -0
  73. package/dist/engine-playwright/global-setup.js +142 -0
  74. package/dist/{core → engine-playwright}/keve-report.d.ts +0 -1
  75. package/dist/engine-playwright/keve-report.js +137 -0
  76. package/dist/{core → engine-playwright}/keve-test.d.ts +10 -8
  77. package/dist/{core → engine-playwright}/keve-test.js +204 -200
  78. package/dist/{core → engine-playwright}/pw-config.js +8 -7
  79. package/dist/index.d.ts +13 -8
  80. package/dist/index.js +43 -28
  81. package/dist/page-agent/agent.d.ts +82 -7
  82. package/dist/page-agent/agent.js +252 -88
  83. package/dist/page-agent/diagnostics.d.ts +184 -0
  84. package/dist/page-agent/diagnostics.js +377 -0
  85. package/dist/page-agent/hooks.d.ts +3 -2
  86. package/dist/page-agent/hooks.js +4 -3
  87. package/dist/page-agent/page-like.d.ts +116 -0
  88. package/dist/page-agent/page-like.js +14 -0
  89. package/dist/page-agent/playwright-page.d.ts +20 -0
  90. package/dist/page-agent/playwright-page.js +98 -0
  91. package/dist/page-agent/system-prompt.d.ts +7 -0
  92. package/dist/page-agent/system-prompt.js +10 -0
  93. package/dist/page-agent/tools.d.ts +1 -1
  94. package/dist/page-agent/tools.js +144 -8
  95. package/package.json +24 -11
  96. package/scripts/gen-system-prompt.mjs +36 -0
  97. package/scripts/run-dsl-task.mjs +652 -0
  98. package/scripts/runtime/cypress-preprocessor.mjs +44 -0
  99. package/scripts/runtime/cypress.config.mjs +35 -0
  100. package/scripts/runtime/playwright.config.mjs +41 -0
  101. package/scripts/verify-shot-diff.mjs +243 -0
  102. package/dist/commands/auth-update.d.ts +0 -16
  103. package/dist/commands/auth-update.js +0 -88
  104. package/dist/core/auth.d.ts +0 -17
  105. package/dist/core/auth.js +0 -224
  106. package/dist/core/cdp-resolver.d.ts +0 -5
  107. package/dist/core/cdp-resolver.js +0 -93
  108. package/dist/core/global-setup.d.ts +0 -6
  109. package/dist/core/global-setup.js +0 -123
  110. package/dist/core/keve-decorators.d.ts +0 -51
  111. package/dist/core/keve-decorators.js +0 -95
  112. package/dist/core/keve-report.js +0 -376
  113. /package/dist/core/{keve-aspect.d.ts → decorator/keve-aspect.d.ts} +0 -0
  114. /package/dist/core/{keve-aspect.js → decorator/keve-aspect.js} +0 -0
  115. /package/dist/{core → engine-playwright}/pw-config.d.ts +0 -0
@@ -0,0 +1,254 @@
1
+ "use strict";
2
+ /**
3
+ * keve-decorators — @keveModel + @keveScene 装饰器
4
+ *
5
+ * 使用 TC39 Stage 3 装饰器 API (Playwright 1.58+ 默认支持)
6
+ *
7
+ * 执行顺序(TC39 Stage 3 规范):
8
+ * 1. Method decorator 函数调用 → 注册 addInitializer
9
+ * 2. Class decorator 函数调用 → 注册 addInitializer
10
+ * 3. Method addInitializers 执行 → @keveScene 注册场景到 WeakMap
11
+ * 4. Class addInitializer 执行 → @keveModel 读取 WeakMap 并调用 test.describe
12
+ *
13
+ * 两层装饰器 + 函数调用:
14
+ * Model (模块) → test.describe → @keveModel(id, desc, { setup, teardown, precondition?, before? })
15
+ * Scene (场景) → test → @keveScene(id, desc, { tag?, precondition?, before?, afterHook? })
16
+ * Goal (步骤) → 步骤元数据 → keveGoal() 函数调用(在方法体内,支持 precondition)
17
+ *
18
+ * @keveGroup 已消除:
19
+ * - setup → @keveModel setup 选项(独立 Page,所有 Scene 前执行)
20
+ * - teardown → @keveModel teardown 选项(所有 Scene 后执行,扫描 E2E_ 前缀)
21
+ * - identity → Scene 内 keveGoal(fn) 中调用 globalThis.switchIdentity
22
+ * - afterHook → @keveScene afterHook 选项(Scene 后保证执行)
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.setKeveTestHost = setKeveTestHost;
26
+ exports.setKeveTest = setKeveTest;
27
+ exports.getKeveTest = getKeveTest;
28
+ exports.hasKeveTest = hasKeveTest;
29
+ exports.keveModel = keveModel;
30
+ exports.keveScene = keveScene;
31
+ exports.getModelScenes = getModelScenes;
32
+ const keve_registry_js_1 = require("./keve-registry.js");
33
+ const keve_host_js_1 = require("./keve-host.js");
34
+ // ─── Configurable test reference ────────────────────────────────────
35
+ // keveModel needs to call test.describe / test — but it must use the EXTENDED test
36
+ // (with keveGoal/keveAssert fixtures), not the base @playwright/test.
37
+ // 引擎入口在创建好扩展 test 后调用 setKeveTest()/setKeveHost() 完成注册:
38
+ // - Playwright: keve-test.ts → setKeveTest(test)
39
+ // - Cypress: engine-cypress → setKeveHost(cypressHost)
40
+ //
41
+ // 装饰器自身不依赖任何测试框架,只面向 KeveHost 接口。
42
+ /**
43
+ * 注册宿主(推荐入口,引擎无关)。
44
+ */
45
+ function setKeveTestHost(host) { (0, keve_host_js_1.setKeveHost)(host); }
46
+ /**
47
+ * Playwright 兼容适配:把 @playwright/test 的 test 对象包成 KeveHost。
48
+ * 保留该名称是为了不破坏既有调用(keve-test.ts / 外部脚本)。
49
+ */
50
+ function setKeveTest(t) {
51
+ (0, keve_host_js_1.setKeveHost)(adaptPlaywrightTest(t));
52
+ }
53
+ /** 把 Playwright 的 test 对象适配成引擎无关宿主 */
54
+ function adaptPlaywrightTest(t) {
55
+ const normalizeTitle = (testInfo) => String(testInfo?.title ?? '');
56
+ return {
57
+ describe: (title, fn) => t.describe(title, fn),
58
+ describeSerial: (title, fn) => (t.describe.serial || t.describe)(title, fn),
59
+ beforeAll: (fn) => t.beforeAll(fn),
60
+ beforeEach: (fn) => t.beforeEach(async ({}, testInfo) => {
61
+ const info = {
62
+ title: normalizeTitle(testInfo),
63
+ skip: (reason) => testInfo.skip(true, reason),
64
+ };
65
+ await fn(info);
66
+ }),
67
+ afterEach: (fn) => t.afterEach(async ({}, testInfo) => {
68
+ const info = {
69
+ title: normalizeTitle(testInfo),
70
+ skip: (reason) => testInfo.skip(true, reason),
71
+ };
72
+ await fn(info);
73
+ }),
74
+ afterAll: (fn) => t.afterAll(fn),
75
+ test: (title, fn) => t(title, fn),
76
+ };
77
+ }
78
+ /** Get the current keve test instance (Playwright 兼容查询;Cypress 下返回宿主) */
79
+ function getKeveTest() { return (0, keve_host_js_1.getKeveHost)(); }
80
+ /** 是否已注册宿主 */
81
+ function hasKeveTest() { return (0, keve_host_js_1.hasKeveHost)(); }
82
+ // ─── Internal Registry (WeakMap per Model constructor) ─────────────
83
+ /** Model → Scene 列表:每个 @keveModel 类下注册的 @keveScene 列表 */
84
+ const modelScenesMap = new WeakMap();
85
+ /** Scene title → afterHook:@keveScene 注册的 afterHook 回调 */
86
+ const sceneAfterHooks = new Map();
87
+ // ─── Error Classification ──────────────────────────────────────────
88
+ // Error classification moved to keve-report.ts onTestEnd —
89
+ // onTestEnd has access to result.error and result.status, which is more reliable
90
+ // than try-catch in the decorator (async rejections, test-level timeouts can bypass decorator catch).
91
+ // ─── TC39 Stage 3 Decorators ──────────────────────────────────────
92
+ /**
93
+ * @keveModel(id, description, options?) — Class decorator (模块层)
94
+ * Uses context.addInitializer to defer test.describe() registration
95
+ * until AFTER all @keveScene addInitializers have run.
96
+ *
97
+ * options.setup: 第一个 test() 调用,拥有独立 Page + keveGoal
98
+ * options.teardown: test.afterAll 回调,保证执行
99
+ */
100
+ function keveModel(id, description, options) {
101
+ return (ctor, context) => {
102
+ context.addInitializer(function () {
103
+ const ctor = this;
104
+ const scenes = modelScenesMap.get(ctor) || [];
105
+ // Merge module-level precondition into each Scene's static meta
106
+ // (scene addInitializers have all run by now — TC39 Stage 3 ordering)
107
+ if (options?.precondition?.length) {
108
+ for (const scene of scenes) {
109
+ const meta = keve_registry_js_1.sceneMetaMap.get(scene.title) || {};
110
+ keve_registry_js_1.sceneMetaMap.set(scene.title, { ...meta, modelPrecondition: options.precondition });
111
+ }
112
+ }
113
+ // 有 setup 时使用 serial:确保 setup 先执行,setup 失败则 Scene 跳过
114
+ // 无 setup 时使用普通 describe:Scene 可并行(实际 CDP 模式下仍串行)
115
+ const host = (0, keve_host_js_1.getKeveHost)();
116
+ const describeFn = options?.setup
117
+ ? host.describeSerial
118
+ : host.describe;
119
+ describeFn(`${id}: ${description}`, () => {
120
+ // ── Module-level before: executable precondition check ──
121
+ // Runs once before all Scenes (beforeAll). Returns false or throws
122
+ // → beforeAll fails → Playwright skips every Scene in this Model.
123
+ // Semantics: precondition unmet = skipped (blocked), NOT failed.
124
+ if (options?.before) {
125
+ host.beforeAll(async () => {
126
+ let ok = false;
127
+ let beforeError;
128
+ try {
129
+ ok = await options.before();
130
+ }
131
+ catch (err) {
132
+ beforeError = err;
133
+ }
134
+ if (!ok) {
135
+ const reason = beforeError
136
+ ? `Module precondition check errored: ${beforeError?.message || beforeError}`
137
+ : `Module precondition not met (before returned false)`;
138
+ console.log(`[keveModel] ⏭️ "${id}" SKIPPED — ${reason}`);
139
+ throw new Error(reason);
140
+ }
141
+ console.log(`[keveModel] ✅ "${id}" module precondition check passed`);
142
+ });
143
+ }
144
+ // ── Setup test (if provided) ──
145
+ // 独立 Page,完成后 Playwright 自动回收;keveGoal fixture 可用
146
+ if (options?.setup) {
147
+ host.test(`${id}: ⚙ Module Setup`, options.setup);
148
+ }
149
+ // ── Scene tests ──
150
+ for (const scene of scenes) {
151
+ host.test(scene.title, scene.fn);
152
+ // ── Scene-level before: executable precondition check ──
153
+ // Scoped to this Scene by title match. Returns false or throws
154
+ // → skip this Scene only (precondition unmet = blocked, NOT fail).
155
+ if (scene.before) {
156
+ const sceneBefore = scene.before;
157
+ host.beforeEach(async (testInfo) => {
158
+ if (testInfo.title !== scene.title)
159
+ return;
160
+ let ok = false;
161
+ let beforeError;
162
+ try {
163
+ ok = await sceneBefore();
164
+ }
165
+ catch (err) {
166
+ beforeError = err;
167
+ }
168
+ if (!ok) {
169
+ const reason = beforeError
170
+ ? `Scene precondition check errored: ${beforeError?.message || beforeError}`
171
+ : `Scene precondition not met (before returned false)`;
172
+ console.log(`[keveScene] ⏭️ "${scene.title}" SKIPPED — ${reason}`);
173
+ testInfo.skip(reason);
174
+ }
175
+ });
176
+ }
177
+ }
178
+ // ── afterEach: run scene afterHooks ──
179
+ // 检查当前 test title 是否注册了 afterHook,有则调用
180
+ // afterHook 无测试框架 fixture —— 纯异步函数
181
+ host.afterEach(async (testInfo) => {
182
+ const afterHook = sceneAfterHooks.get(testInfo.title);
183
+ if (afterHook) {
184
+ try {
185
+ await afterHook();
186
+ }
187
+ catch (err) {
188
+ console.error(`[keveScene] afterHook error for "${testInfo.title}": ${err?.message || err}`);
189
+ }
190
+ }
191
+ });
192
+ // ── afterAll: run module teardown ──
193
+ // 保证执行,即使有 Scene 失败/超时
194
+ if (options?.teardown) {
195
+ host.afterAll(async () => {
196
+ try {
197
+ await options.teardown();
198
+ }
199
+ catch (err) {
200
+ console.error(`[keveModel] teardown error for "${id}": ${err?.message || err}`);
201
+ }
202
+ });
203
+ }
204
+ });
205
+ });
206
+ return ctor;
207
+ };
208
+ }
209
+ /**
210
+ * @keveScene(id, description, options?) — Method decorator (场景层, static methods only)
211
+ * Captures fn.toString() as the complete scene script.
212
+ * Registers scene metadata to both WeakMap and global Map.
213
+ * If options.afterHook is provided, registers it in sceneAfterHooks Map
214
+ * for afterEach consumption.
215
+ *
216
+ * Returns the original function unchanged — Playwright resolves fixture dependencies
217
+ * by parsing fn.toString() / AST. Wrapping would break this resolution.
218
+ */
219
+ function keveScene(id, description, options) {
220
+ // fn 用 any:装饰器原样返回方法本体,类型需与方法签名(fixtures 参数)兼容,Function 会触发 TS1270
221
+ return (fn, context) => {
222
+ const code = fn.toString(); // Capture original code BEFORE any wrapping
223
+ const title = `${id}: ${description}`;
224
+ context.addInitializer(function () {
225
+ const ctor = this;
226
+ if (!modelScenesMap.has(ctor))
227
+ modelScenesMap.set(ctor, []);
228
+ const scene = {
229
+ id, description, title, code, goals: [], fn,
230
+ precondition: options?.precondition,
231
+ before: options?.before,
232
+ afterHook: options?.afterHook,
233
+ };
234
+ modelScenesMap.get(ctor).push(scene);
235
+ // Register to global maps for Reporter access
236
+ keve_registry_js_1.sceneCodeMap.set(title, code); // Stores ORIGINAL code, not wrapped
237
+ // Register scene implementation for engine.callScene composition
238
+ keve_registry_js_1.sceneImplMap.set(id, fn);
239
+ // Register scene-level static meta (model-level precondition merged
240
+ // later in keveModel's addInitializer, which runs after all scenes registered)
241
+ keve_registry_js_1.sceneMetaMap.set(title, { precondition: options?.precondition });
242
+ // Register afterHook for afterEach consumption
243
+ if (options?.afterHook) {
244
+ sceneAfterHooks.set(title, options.afterHook);
245
+ }
246
+ });
247
+ return fn; // Return original function unchanged — preserves fixture parameter signature
248
+ };
249
+ }
250
+ // ─── Utility ───────────────────────────────────────────────────────
251
+ /** 获取 Model 下注册的所有 Scene */
252
+ function getModelScenes(ctor) {
253
+ return modelScenesMap.get(ctor) || null;
254
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * keve-host — 装饰器宿主抽象
3
+ *
4
+ * @keveModel / @keveScene 只依赖本接口,不直接依赖任何测试框架:
5
+ * - Playwright: keve-test.ts 把扩展后的 test 适配成 KeveHost
6
+ * - Cypress: engine-cypress/host.ts 把 cy / describe / it 适配成 KeveHost
7
+ *
8
+ * 这样同一份装饰器脚本可同时跑在两个引擎下,且注册进 keve-registry 的
9
+ * 场景元数据完全一致 —— 这是两端报告能够对齐的根本原因。
10
+ *
11
+ * 本模块必须保持零依赖(浏览器安全),Cypress spec 会直接打包它。
12
+ */
13
+ /** 用例信息:装饰器在 beforeEach / afterEach 中需要的子集 */
14
+ export interface KeveTestInfo {
15
+ /** 用例完整标题(`${id}: ${description}`),用于与 scene.title 匹配 */
16
+ title: string;
17
+ /** 标记跳过。前置条件不满足 → blocked(跳过),不是 fail */
18
+ skip(reason?: string): void;
19
+ }
20
+ /**
21
+ * 测试框架宿主能力:恰好是 @keveModel/@keveScene 需要的全部注册原语。
22
+ * 刻意保持最小 —— 任何新增能力都必须同时能被 Playwright 与 Cypress 表达。
23
+ */
24
+ export interface KeveHost {
25
+ /** 注册一个 describe 分组 */
26
+ describe(title: string, fn: () => void): void;
27
+ /** 注册串行 describe:Model 带 setup 时使用,保证 setup 先于各 Scene 执行 */
28
+ describeSerial(title: string, fn: () => void): void;
29
+ beforeAll(fn: () => Promise<void> | void): void;
30
+ beforeEach(fn: (testInfo: KeveTestInfo) => Promise<void> | void): void;
31
+ afterEach(fn: (testInfo: KeveTestInfo) => Promise<void> | void): void;
32
+ afterAll(fn: () => Promise<void> | void): void;
33
+ /** 注册一个 Scene 用例 */
34
+ test(title: string, fn: Function): void;
35
+ }
36
+ /** 由引擎入口调用(keve-test.ts / engine-cypress)。后注册者生效。 */
37
+ export declare function setKeveHost(host: KeveHost): void;
38
+ /** 获取当前宿主;未注册时抛出可诊断的错误(而非 undefined 崩溃) */
39
+ export declare function getKeveHost(): KeveHost;
40
+ export declare function hasKeveHost(): boolean;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * keve-host — 装饰器宿主抽象
4
+ *
5
+ * @keveModel / @keveScene 只依赖本接口,不直接依赖任何测试框架:
6
+ * - Playwright: keve-test.ts 把扩展后的 test 适配成 KeveHost
7
+ * - Cypress: engine-cypress/host.ts 把 cy / describe / it 适配成 KeveHost
8
+ *
9
+ * 这样同一份装饰器脚本可同时跑在两个引擎下,且注册进 keve-registry 的
10
+ * 场景元数据完全一致 —— 这是两端报告能够对齐的根本原因。
11
+ *
12
+ * 本模块必须保持零依赖(浏览器安全),Cypress spec 会直接打包它。
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.setKeveHost = setKeveHost;
16
+ exports.getKeveHost = getKeveHost;
17
+ exports.hasKeveHost = hasKeveHost;
18
+ let _host = null;
19
+ /** 由引擎入口调用(keve-test.ts / engine-cypress)。后注册者生效。 */
20
+ function setKeveHost(host) {
21
+ _host = host;
22
+ }
23
+ /** 获取当前宿主;未注册时抛出可诊断的错误(而非 undefined 崩溃) */
24
+ function getKeveHost() {
25
+ if (!_host) {
26
+ throw new Error('[keve] 测试宿主未注册:请先 import 引擎入口'
27
+ + '(@kkeve/suite/keve-test 或 @kkeve/suite/engine-cypress)再做场景注册');
28
+ }
29
+ return _host;
30
+ }
31
+ function hasKeveHost() {
32
+ return _host !== null;
33
+ }
@@ -36,3 +36,22 @@ export declare const sceneGoalsMap: Map<string, KeveGoalMeta[]>;
36
36
  * @keveScene 装饰器在执行后写入分类结果,Reporter 在 onTestEnd 中读取
37
37
  */
38
38
  export declare const sceneEvalMetaMap: Map<string, KeveEvalMeta>;
39
+ /** 场景静态元数据(编译期已知,不依赖执行) */
40
+ export interface KeveSceneStaticMeta {
41
+ /** 用例级前置条件描述列表(声明式,来自 @keveScene options) */
42
+ precondition?: string[];
43
+ /** 所属 Model 的模块级前置条件描述列表 */
44
+ modelPrecondition?: string[];
45
+ }
46
+ /**
47
+ * 场景静态元数据映射: scene title → KeveSceneStaticMeta
48
+ * @keveScene 装饰器在注册期写入,Reporter/平台解析侧读取
49
+ * (Reporter 无需等待场景执行即可拿到前置条件,用于报告展示与 blocked 归因)
50
+ */
51
+ export declare const sceneMetaMap: Map<string, KeveSceneStaticMeta>;
52
+ /**
53
+ * 场景实现映射: scene id(如 'S4')→ 场景静态方法本体
54
+ * @keveScene 装饰器在注册期写入;engine.callScene(id, keveGoal) 运行时查找调用,
55
+ * 实现「用例组合用例」而无需 spec 文件互相 import(避免重复注册测试)
56
+ */
57
+ export declare const sceneImplMap: Map<string, Function>;
@@ -7,7 +7,7 @@
7
7
  * can import it without pulling Playwright into the report build.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.sceneEvalMetaMap = exports.sceneGoalsMap = exports.sceneCodeMap = void 0;
10
+ exports.sceneImplMap = exports.sceneMetaMap = exports.sceneEvalMetaMap = exports.sceneGoalsMap = exports.sceneCodeMap = void 0;
11
11
  // ─── Global Registry ──────────────────────────────────────────────
12
12
  /**
13
13
  * 场景代码映射: scene title → scene code (fn.toString())
@@ -23,3 +23,15 @@ exports.sceneGoalsMap = new Map();
23
23
  * @keveScene 装饰器在执行后写入分类结果,Reporter 在 onTestEnd 中读取
24
24
  */
25
25
  exports.sceneEvalMetaMap = new Map();
26
+ /**
27
+ * 场景静态元数据映射: scene title → KeveSceneStaticMeta
28
+ * @keveScene 装饰器在注册期写入,Reporter/平台解析侧读取
29
+ * (Reporter 无需等待场景执行即可拿到前置条件,用于报告展示与 blocked 归因)
30
+ */
31
+ exports.sceneMetaMap = new Map();
32
+ /**
33
+ * 场景实现映射: scene id(如 'S4')→ 场景静态方法本体
34
+ * @keveScene 装饰器在注册期写入;engine.callScene(id, keveGoal) 运行时查找调用,
35
+ * 实现「用例组合用例」而无需 spec 文件互相 import(避免重复注册测试)
36
+ */
37
+ exports.sceneImplMap = new Map();
@@ -4,7 +4,7 @@
4
4
  * 只生成 report-data.json — 前端 Vue 报告页面消费的唯一数据源。
5
5
  * 废弃的产物:execution-summary.json、summary-report.html(数据均已在 report-data.json 中)
6
6
  */
7
- import { type ReportDataOptions } from './reportData';
7
+ import { type ReportDataOptions } from './reportData.js';
8
8
  export type { ReportDataOptions };
9
9
  export interface GenerateReportOptions extends ReportDataOptions {
10
10
  }
@@ -43,13 +43,13 @@ exports.generateReport = generateReport;
43
43
  exports.reportMain = reportMain;
44
44
  const fs = __importStar(require("node:fs"));
45
45
  const path = __importStar(require("node:path"));
46
- const reportData_1 = require("./reportData");
46
+ const reportData_js_1 = require("./reportData.js");
47
47
  /**
48
48
  * 报告生成统一入口:数据生成 → 写入 report-data.json
49
49
  */
50
50
  async function generateReport(opts) {
51
51
  // Step 1: 生成报告数据(纯计算,无 IO)
52
- const reportData = await (0, reportData_1.generateReportData)(opts);
52
+ const reportData = await (0, reportData_js_1.generateReportData)(opts);
53
53
  // Step 2: 写入 round-N/report-data.json,latest/ 用 symlink 指向最新轮次(避免 34MB+ 重复拷贝)
54
54
  const { roundDir, latestDir, summary } = reportData;
55
55
  fs.mkdirSync(roundDir, { recursive: true });
@@ -7,6 +7,7 @@
7
7
  * This file handles ONLY data processing — no HTML rendering.
8
8
  * Rendering is handled by generate-html-renderer.ts.
9
9
  */
10
+ import { type RuntimeDiagnostics } from '../../page-agent/diagnostics.js';
10
11
  export interface ReportDataOptions {
11
12
  projectRoot: string;
12
13
  resultsPath: string;
@@ -192,6 +193,8 @@ export interface CaseReportItem {
192
193
  errorCategory?: string;
193
194
  /** AI 观察到的诊断提示(如 disabled 按钮、SSO 重定向等) */
194
195
  diagnosticHint?: string;
196
+ /** 运行诊断(性能 / 网络 / 浏览器错误 / 质量信号),用例级聚合 */
197
+ diagnostics?: RuntimeDiagnostics;
195
198
  }
196
199
  declare function parseCaseIdFromTitle(title: string): string | null;
197
200
  declare function parseYamlSimple(yamlText: string): TestCaseYaml;
@@ -54,17 +54,19 @@ exports.buildCaseResultMap = buildCaseResultMap;
54
54
  exports.parseYamlSimple = parseYamlSimple;
55
55
  const fs = __importStar(require("node:fs"));
56
56
  const path = __importStar(require("node:path"));
57
- const keve_registry_1 = require("../keve-registry");
57
+ const keve_registry_js_1 = require("../decorator/keve-registry.js");
58
58
  function resolveOptions(opts) {
59
59
  const PROJECT_ROOT = opts.projectRoot;
60
60
  const TEST_RESULTS_JSON = opts.resultsPath;
61
61
  const CONFIDENCE_DATA_JSONL = opts.confidencePath || path.join(path.dirname(TEST_RESULTS_JSON), 'confidence-data.jsonl');
62
62
  const TEST_CASES_YAML = opts.casesPath || path.join(PROJECT_ROOT, 'test-cases.yaml');
63
- // REPORTS_DIR = parent of round-N dirs (e.g. .keve/{plan}/test-artifacts/)
64
- // test-results.json is inside round-N dir, so go up one level
65
- const RESULT_PARENT_DIR = path.dirname(path.dirname(TEST_RESULTS_JSON));
66
- const SCREENSHOTS_DIR = path.join(RESULT_PARENT_DIR, 'screenshots');
67
- const REPORTS_DIR = RESULT_PARENT_DIR;
63
+ // test-results.json 所在目录即当前轮次目录:
64
+ // 新布局 <taskRoot>/reports/round-N
65
+ // 旧布局 <taskDir>/test-artifacts/round-N
66
+ // 因此轮次父目录是 REPORTS_DIR,截图则在轮次目录内部。
67
+ const ROUND_DIR = path.dirname(TEST_RESULTS_JSON);
68
+ const SCREENSHOTS_DIR = path.join(ROUND_DIR, 'screenshots');
69
+ const REPORTS_DIR = path.dirname(ROUND_DIR);
68
70
  return { PROJECT_ROOT, TEST_RESULTS_JSON, CONFIDENCE_DATA_JSONL, TEST_CASES_YAML, SCREENSHOTS_DIR, REPORTS_DIR };
69
71
  }
70
72
  // ─── Helpers ─────────────────────────────────────────────────────
@@ -81,6 +83,18 @@ function parseCaseIdFromTitle(title) {
81
83
  const spaceMatch = title.match(new RegExp(`^(${caseIdPattern})\\s`));
82
84
  return colonMatch?.[1] || bracketMatch?.[1] || spaceMatch?.[1] || null;
83
85
  }
86
+ /**
87
+ * @keveModel 的模块 ID(describe 标题 `"${id}: ${description}"` 中冒号前的部分)。
88
+ *
89
+ * 三种合法形态(与 parseCaseIdFromTitle 的取值域保持一致):
90
+ * 1. DSL 转译:`M_SG90061`(含下划线)
91
+ * 2. 手写/LLM:`AG` / `AG-01` / `SKILL_SHARE-01`(含下划线与短横线)
92
+ * 3. ktest 原始编号:`7473423`(纯数字)
93
+ *
94
+ * 之前这里只认「纯字母 + 短横线」或「纯数字」,导致 DSL 生成的 `M_SG90061` 与
95
+ * `AG-01` 全部匹配失败、模块名退化成 `default` —— 所有用例在报告里被并成一个分组。
96
+ */
97
+ const MODULE_ID_PATTERN = '[A-Z][A-Z0-9]*(?:[_-][A-Z0-9]+)*|\\d+';
84
98
  /**
85
99
  * Strip surrounding quotes from YAML string values.
86
100
  * YAML uses quotes like id: "AG-01" which parseYamlSimple captures as "AG-01"
@@ -397,7 +411,7 @@ function buildCaseResultMap(specs, taskDir) {
397
411
  const goalScreenshots = parseGoalScreenshots(test.attachments || [], taskDir);
398
412
  keveScreenshots.push(...goalScreenshots);
399
413
  // keveGoals: prefer @keveGoal decorator data from sceneGoalsMap, fallback to fixture attachment
400
- const decoratorGoals = keve_registry_1.sceneGoalsMap.get(spec.title);
414
+ const decoratorGoals = keve_registry_js_1.sceneGoalsMap.get(spec.title);
401
415
  const keveGoals = decoratorGoals && decoratorGoals.length > 0
402
416
  ? decoratorGoals
403
417
  : parseKeveGoals(test.attachments || []);
@@ -406,7 +420,7 @@ function buildCaseResultMap(specs, taskDir) {
406
420
  const pwModuleTitle = suitePathParts.length >= 2 ? suitePathParts[suitePathParts.length - 1].trim() : suitePathParts[0].trim();
407
421
  // @keveModel title format: "${id}: ${description}" — extract module ID and description
408
422
  // 模块 ID 可能是字母格式(如 AG-01)或 ktest 纯数字格式(如 7473423)
409
- const moduleIdMatch = pwModuleTitle.match(/^([A-Z]+(?:-[A-Z]+)*|\d+):\s*(.+)$/);
423
+ const moduleIdMatch = pwModuleTitle.match(new RegExp(`^(${MODULE_ID_PATTERN}):\\s*(.+)$`));
410
424
  const pwModuleId = moduleIdMatch?.[1] || '';
411
425
  const pwModuleDescription = moduleIdMatch?.[2] || pwModuleTitle;
412
426
  map[caseId] = {
@@ -491,6 +505,7 @@ async function generateReportData(opts) {
491
505
  errorCategory: entry.errorCategory || undefined,
492
506
  diagnosticHint: entry.diagnosticHint || undefined,
493
507
  steps,
508
+ diagnostics: entry.diagnostics,
494
509
  };
495
510
  }
496
511
  }
@@ -506,6 +521,20 @@ async function generateReportData(opts) {
506
521
  // Primary data source: Playwright test-results.json (from @keveModel/@keveScene decorators)
507
522
  // Optional enhancement: test-cases.yaml (priority, precondition, type, notes)
508
523
  // This ensures decorator-defined structure drives the report, avoiding ID/title mismatch.
524
+ /** 从已执行步骤提取 Agent 最终结论(done.text),用于补全空 thought。 */
525
+ function extractFinalConclusion(steps) {
526
+ for (let i = steps.length - 1; i >= 0; i--) {
527
+ const actions = steps[i]?.actions;
528
+ if (!actions?.length)
529
+ continue;
530
+ for (let j = actions.length - 1; j >= 0; j--) {
531
+ const action = actions[j];
532
+ if (action?.tool === 'done' && action?.text)
533
+ return action.text.trim();
534
+ }
535
+ }
536
+ return '';
537
+ }
509
538
  // Build a YAML lookup by caseId for optional enrichment
510
539
  const yamlCaseLookup = {};
511
540
  const yamlModuleLookup = {};
@@ -521,11 +550,10 @@ async function generateReportData(opts) {
521
550
  const effectiveStatus = result.status === 'timedOut' ? 'failed' : result.status;
522
551
  // AI evaluation: match by Playwright title (scene.title, the canonical key)
523
552
  const aiEntry = aiConfidenceMap[result.title] || aiConfidenceMap[caseId];
524
- const data = effectiveStatus === 'skipped' ? '待确认' : (aiEntry?.data || '待确认');
553
+ const data = effectiveStatus === 'skipped' ? '待确认' : (aiEntry?.data || (effectiveStatus === 'passed' ? '通过' : '待确认'));
525
554
  // When aiEntry is missing, confidence should be 0 (no AI evaluation performed),
526
555
  // not a hardcoded fallback like 70/35 which implies AI evaluated but was uncertain.
527
556
  const confidence = effectiveStatus === 'skipped' ? 0 : (aiEntry?.confidence ?? 0);
528
- const confidenceReason = effectiveStatus === 'skipped' ? '用例被跳过' : (aiEntry?.thought || '无AI评估数据');
529
557
  // Distribute by data value + confidence
530
558
  if (effectiveStatus === 'skipped') { /* skip */ }
531
559
  else if (data === '通过')
@@ -599,6 +627,8 @@ async function generateReportData(opts) {
599
627
  expected: g.expected,
600
628
  precondition: g.precondition,
601
629
  order: g.order,
630
+ status: effectiveStatus === 'passed' ? 'passed' : (g.success === false ? 'failed' : 'unexecuted'),
631
+ success: g.success,
602
632
  }))
603
633
  : []);
604
634
  // ── Merge executed steps with YAML outline ──
@@ -617,19 +647,22 @@ async function generateReportData(opts) {
617
647
  continue;
618
648
  if (steps.some(s => s.order === i))
619
649
  continue;
620
- // Add unexecuted YAML step
650
+ // Add unexecuted YAML step — if case passed, mark as passed; otherwise unexecuted
621
651
  steps.push({
622
652
  step: ys.step,
623
653
  expected: ys.expected,
624
654
  order: i,
625
- success: undefined,
626
- // Mark as unexecuted so frontend can render "未执行" state
627
- status: 'unexecuted',
655
+ success: effectiveStatus === 'passed' ? true : undefined,
656
+ status: effectiveStatus === 'passed' ? 'passed' : 'unexecuted',
628
657
  });
629
658
  }
630
659
  // Sort by order to ensure correct sequencing
631
660
  steps.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
632
661
  }
662
+ const extractedConclusion = extractFinalConclusion(steps);
663
+ const confidenceReason = effectiveStatus === 'skipped'
664
+ ? '用例被跳过'
665
+ : (aiEntry?.thought || extractedConclusion || '无AI评估数据');
633
666
  caseResults.push({
634
667
  caseId,
635
668
  module: moduleName,
@@ -649,7 +682,7 @@ async function generateReportData(opts) {
649
682
  stdout: result.stdout,
650
683
  data,
651
684
  confidence,
652
- thought: aiEntry?.thought || (effectiveStatus === 'skipped' ? '用例被跳过(非自动执行)' : ''),
685
+ thought: aiEntry?.thought || extractedConclusion || (effectiveStatus === 'skipped' ? '用例被跳过(非自动执行)' : ''),
653
686
  confidenceReason,
654
687
  stepScreenshots: stepScreenshotsPaths,
655
688
  screenshotPath: result.screenshotPath,
@@ -658,6 +691,7 @@ async function generateReportData(opts) {
658
691
  keveScreenshots: keveScreenshotsPaths,
659
692
  errorCategory: aiEntry?.errorCategory,
660
693
  diagnosticHint: aiEntry?.diagnosticHint,
694
+ diagnostics: aiEntry?.diagnostics,
661
695
  });
662
696
  }
663
697
  // Also include YAML cases that have no matching Playwright result (not executed)