@kkeve/suite 0.0.13 → 0.0.17

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 (117) 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 +16 -12
  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 +55 -18
  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 +257 -207
  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 +98 -7
  82. package/dist/page-agent/agent.js +338 -84
  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/system_prompt.md +1 -1
  94. package/dist/page-agent/tools.d.ts +1 -1
  95. package/dist/page-agent/tools.js +441 -9
  96. package/doc/skills/keve-e2e-gen/references/script-generation-guide.md +11 -0
  97. package/package.json +24 -11
  98. package/scripts/gen-system-prompt.mjs +36 -0
  99. package/scripts/run-dsl-task.mjs +619 -0
  100. package/scripts/runtime/cypress-preprocessor.mjs +44 -0
  101. package/scripts/runtime/cypress.config.mjs +35 -0
  102. package/scripts/runtime/playwright.config.mjs +41 -0
  103. package/scripts/verify-shot-diff.mjs +243 -0
  104. package/dist/commands/auth-update.d.ts +0 -16
  105. package/dist/commands/auth-update.js +0 -88
  106. package/dist/core/auth.d.ts +0 -17
  107. package/dist/core/auth.js +0 -224
  108. package/dist/core/cdp-resolver.d.ts +0 -5
  109. package/dist/core/cdp-resolver.js +0 -93
  110. package/dist/core/global-setup.d.ts +0 -6
  111. package/dist/core/global-setup.js +0 -113
  112. package/dist/core/keve-decorators.d.ts +0 -51
  113. package/dist/core/keve-decorators.js +0 -95
  114. package/dist/core/keve-report.js +0 -376
  115. /package/dist/core/{keve-aspect.d.ts → decorator/keve-aspect.d.ts} +0 -0
  116. /package/dist/core/{keve-aspect.js → decorator/keve-aspect.js} +0 -0
  117. /package/dist/{core → engine-playwright}/pw-config.d.ts +0 -0
@@ -2,11 +2,11 @@
2
2
  /**
3
3
  * keve-test — Playwright test fixture: keveGoal (唯一操作原语)
4
4
  *
5
- * keveGoal 执行 fn 后 → 截图+fn逻辑+expected → AI判断是否一致
6
- * 一致 → 通过
7
- * 不一致 → AI 告诉你还差什么 → Re-Act 探索 → 探索成功
5
+ * keveGoal 执行 fn 后 → 截图+fn逻辑+expected → Agent Re-Act 观察页面后给最终结论
8
6
  * fn 无 → Re-Act 直接探索
7
+ * fn 成功 → 仍进 Re-Act,确认预期达成并产出推理轨迹
9
8
  * fn 异常 → Re-Act 自愈
9
+ * 确定性事实(环境阻断 / 页面已关闭 / 截图差异超阈值)→ 短路,不烧 token
10
10
  *
11
11
  * keveAssert 已被吸收:keveGoal 的 expected 即断言语义
12
12
  *
@@ -56,33 +56,38 @@ var __importStar = (this && this.__importStar) || (function () {
56
56
  };
57
57
  })();
58
58
  Object.defineProperty(exports, "__esModule", { value: true });
59
- exports.test = exports.sceneGoalsMap = exports.sceneEvalMetaMap = exports.sceneCodeMap = exports.getModelScenes = exports.keveScene = exports.keveModel = exports.expect = void 0;
59
+ exports.test = exports.sceneGoalsMap = exports.sceneMetaMap = exports.sceneEvalMetaMap = exports.sceneCodeMap = exports.getModelScenes = exports.keveScene = exports.keveModel = exports.expect = void 0;
60
60
  const test_1 = require("@playwright/test");
61
61
  Object.defineProperty(exports, "expect", { enumerable: true, get: function () { return test_1.expect; } });
62
- const keve_registry_1 = require("./keve-registry");
63
- const agent_1 = require("../page-agent/agent");
64
- const keve_aspect_1 = require("./keve-aspect");
65
- const learned_actions_1 = require("./learned-actions");
62
+ const keve_registry_js_1 = require("../core/decorator/keve-registry.js");
63
+ const agent_js_1 = require("../page-agent/agent.js");
64
+ const keve_aspect_js_1 = require("../core/decorator/keve-aspect.js");
65
+ const learned_actions_js_1 = require("../core/learned-actions.js");
66
+ const config_js_1 = require("../config.js");
66
67
  const fs = __importStar(require("node:fs"));
67
68
  const path = __importStar(require("node:path"));
68
- // ─── CDP Mode ──────────────────────────────────────────────────────
69
- // CDP 连接逻辑已移到 global-setup.ts,通过全局变量共享 Browser 对象
70
- const global_setup_1 = require("./global-setup");
71
- var keve_decorators_1 = require("./keve-decorators");
72
- Object.defineProperty(exports, "keveModel", { enumerable: true, get: function () { return keve_decorators_1.keveModel; } });
73
- Object.defineProperty(exports, "keveScene", { enumerable: true, get: function () { return keve_decorators_1.keveScene; } });
74
- Object.defineProperty(exports, "getModelScenes", { enumerable: true, get: function () { return keve_decorators_1.getModelScenes; } });
75
- var keve_registry_2 = require("./keve-registry");
76
- Object.defineProperty(exports, "sceneCodeMap", { enumerable: true, get: function () { return keve_registry_2.sceneCodeMap; } });
77
- Object.defineProperty(exports, "sceneEvalMetaMap", { enumerable: true, get: function () { return keve_registry_2.sceneEvalMetaMap; } });
69
+ // ─── 登录态注入 ─────────────────────────────────────────────────────
70
+ // 不再由测试侧启动浏览器 / CDP 接管。登录 cookie 由服务端解析后随环境变量下发,
71
+ // 这里只负责在启动浏览器前把 storageState 就位(global-setup 已写好文件)。
72
+ const global_setup_js_1 = require("./global-setup.js");
73
+ const screenshotDiff_js_1 = require("../core/judge/screenshotDiff.js");
74
+ const goal_heuristics_js_1 = require("../core/judge/goal-heuristics.js");
75
+ var keve_decorators_js_1 = require("../core/decorator/keve-decorators.js");
76
+ Object.defineProperty(exports, "keveModel", { enumerable: true, get: function () { return keve_decorators_js_1.keveModel; } });
77
+ Object.defineProperty(exports, "keveScene", { enumerable: true, get: function () { return keve_decorators_js_1.keveScene; } });
78
+ Object.defineProperty(exports, "getModelScenes", { enumerable: true, get: function () { return keve_decorators_js_1.getModelScenes; } });
79
+ var keve_registry_js_2 = require("../core/decorator/keve-registry.js");
80
+ Object.defineProperty(exports, "sceneCodeMap", { enumerable: true, get: function () { return keve_registry_js_2.sceneCodeMap; } });
81
+ Object.defineProperty(exports, "sceneEvalMetaMap", { enumerable: true, get: function () { return keve_registry_js_2.sceneEvalMetaMap; } });
82
+ Object.defineProperty(exports, "sceneMetaMap", { enumerable: true, get: function () { return keve_registry_js_2.sceneMetaMap; } });
78
83
  // Re-export goal metadata for consumer convenience
79
- var keve_registry_3 = require("./keve-registry");
80
- Object.defineProperty(exports, "sceneGoalsMap", { enumerable: true, get: function () { return keve_registry_3.sceneGoalsMap; } });
84
+ var keve_registry_js_3 = require("../core/decorator/keve-registry.js");
85
+ Object.defineProperty(exports, "sceneGoalsMap", { enumerable: true, get: function () { return keve_registry_js_3.sceneGoalsMap; } });
81
86
  // ─── Register Built-in Aspects ───────────────────────────────────────
82
87
  // NOTE: action-log-write aspect 已废弃。
83
88
  // AI 探索过程数据现在通过 keveGoalResult attachment 直接流到 KeveReporter,
84
89
  // 由 KeveReporter 统一写入 confidence-data.jsonl(含 reactSteps)。
85
- keve_aspect_1.keveAspect.register({
90
+ keve_aspect_js_1.keveAspect.register({
86
91
  name: 'learnedActions-update',
87
92
  phase: 'after',
88
93
  order: 20,
@@ -90,7 +95,7 @@ keve_aspect_1.keveAspect.register({
90
95
  if (!result?.actions?.length)
91
96
  return;
92
97
  const url = ctx.page.url();
93
- learned_actions_1.learnedActions.add(ctx.step, result.actions, url, result.success);
98
+ learned_actions_js_1.learnedActions.add(ctx.step, result.actions, url, result.success);
94
99
  },
95
100
  });
96
101
  // ─── (diagnostic extraction moved to page-agent/diagnostic.ts, called via onAfterTask hook) ──
@@ -98,47 +103,52 @@ keve_aspect_1.keveAspect.register({
98
103
  exports.test = test_1.test.extend({
99
104
  // ── 将内置 testInfo 包装为可解构 fixture ─────────────────────────
100
105
  // 测试方法可直接写 { page, keveGoal, testInfo },不再报 "unknown parameter" 错误
106
+ // ── engine: 引擎无关操作适配器 ──────────────────────────────────────
107
+ engine: async ({ page }, use) => {
108
+ const { createEngine } = await Promise.resolve().then(() => __importStar(require('./engineAdapter.js')));
109
+ const engine = createEngine(page);
110
+ await use(engine);
111
+ },
101
112
  testInfo: async ({}, use, testInfo) => {
102
113
  await use(testInfo);
103
114
  },
104
115
  browser: async ({}, use, testInfo) => {
105
- if ((0, global_setup_1.isCdpMode)()) {
106
- const browser = await (0, global_setup_1.getCdpBrowser)(); // 复用全局共享的 CDP browser
107
- await use(browser);
108
- // CDP 模式下不关闭浏览器(共享连接,关闭会影响后续用例)
109
- }
110
- else {
111
- const browser = await test_1.chromium.launch(testInfo.project?.use?.launchOptions || {});
112
- await use(browser);
113
- await browser.close();
114
- }
116
+ const browser = await test_1.chromium.launch(testInfo.project?.use?.launchOptions || {});
117
+ await use(browser);
118
+ await browser.close();
115
119
  },
116
- page: async ({ browser }, use) => {
117
- if ((0, global_setup_1.isCdpMode)()) {
118
- const contexts = browser.contexts();
119
- const cdpContext = contexts.length > 0 ? contexts[0] : await browser.newContext();
120
- // CDP 模式:优先复用已有 page(保留登录状态),仅在无可用 page 时新开
121
- const existingPages = cdpContext.pages();
122
- let page;
123
- if (existingPages.length > 0) {
124
- // 复用第一个非 about:blank 的 page(通常是用户已登录的 tab)
125
- page = existingPages.find(p => p.url() !== 'about:blank') || existingPages[0];
126
- // 导航到 about:blank 作为起始页(清空之前的状态,但保留 cookie)
127
- try {
128
- await page.goto('about:blank', { timeout: 3000 });
129
- }
130
- catch { /* ignore */ }
131
- }
132
- else {
133
- page = await cdpContext.newPage();
120
+ page: async ({ browser }, use, testInfo) => {
121
+ // 建 context 时带上 storageState(服务端 cookies 写入的登录态),
122
+ // 裸 newPage() 会丢失 cookies 导致用例未登录失败。
123
+ // global-setup 正常会先写好;直接裸跑 playwright test 时这里兜底就地生成。
124
+ let storageState = testInfo.project?.use?.storageState;
125
+ if (!storageState && (0, global_setup_js_1.readInjectedCookies)().length > 0) {
126
+ storageState = (0, global_setup_js_1.resolveStorageStatePath)();
127
+ try {
128
+ (0, global_setup_js_1.writeStorageState)(storageState);
134
129
  }
135
- await use(page);
136
- // CDP 模式下不关闭 page(保留浏览器 tab 给下一个用例复用)
130
+ catch { /* 写失败则裸跑,用例会自行暴露登录问题 */ }
137
131
  }
138
- else {
139
- const page = await browser.newPage();
140
- await use(page);
141
- await page.close();
132
+ // 视频录制:playwright config use.video = { mode:'on', dir } → recordVideo
133
+ //(自定义 context 不会自动附加视频附件,需在 context 关闭后显式 attach)
134
+ const videoOpt = testInfo.project?.use?.video;
135
+ const videoOn = !!videoOpt && (videoOpt === 'on' || videoOpt.mode === 'on' || videoOpt.mode === 'retain-on-failure');
136
+ const context = await browser.newContext({
137
+ storageState,
138
+ ...(videoOn ? { recordVideo: { dir: typeof videoOpt === 'object' && videoOpt.dir ? videoOpt.dir : undefined } } : {}),
139
+ });
140
+ const page = await context.newPage();
141
+ await use(page);
142
+ const video = page.video();
143
+ await page.close();
144
+ await context.close();
145
+ // 视频文件在 context 关闭后才定稿:显式挂附件 → results.json attachments
146
+ if (video) {
147
+ try {
148
+ const videoPath = await video.path();
149
+ await testInfo.attach('video', { path: videoPath });
150
+ }
151
+ catch { /* 无视频不阻塞 */ }
142
152
  }
143
153
  },
144
154
  // ── keveGoal: 唯一操作原语 ────────────────────────────────────────
@@ -152,9 +162,9 @@ exports.test = test_1.test.extend({
152
162
  order: goalOrder++,
153
163
  };
154
164
  const sceneTitle = testInfo.title;
155
- if (!keve_registry_1.sceneGoalsMap.has(sceneTitle))
156
- keve_registry_1.sceneGoalsMap.set(sceneTitle, []);
157
- keve_registry_1.sceneGoalsMap.get(sceneTitle).push(goalMeta);
165
+ if (!keve_registry_js_1.sceneGoalsMap.has(sceneTitle))
166
+ keve_registry_js_1.sceneGoalsMap.set(sceneTitle, []);
167
+ keve_registry_js_1.sceneGoalsMap.get(sceneTitle).push(goalMeta);
158
168
  const ctx = {
159
169
  page,
160
170
  step: options.step,
@@ -165,7 +175,7 @@ exports.test = test_1.test.extend({
165
175
  specFilePath: testInfo.file,
166
176
  };
167
177
  // Run before aspects
168
- await keve_aspect_1.keveAspect.runPhase('before', ctx);
178
+ await keve_aspect_js_1.keveAspect.runPhase('before', ctx);
169
179
  // ── Capture goal-before screenshot (fn 之前的页面原始状态) ──
170
180
  // 语义:记录"测试动作发生前"的页面,供报告 before/after 对照
171
181
  let goalScreenshotBefore = '';
@@ -176,24 +186,46 @@ exports.test = test_1.test.extend({
176
186
  let result;
177
187
  const fnSource = fn?.toString() || undefined;
178
188
  let fnError;
189
+ let fnErrorRaw; // 原始错误对象(ScreenshotDiffError 等带结构的错误需要保留结构)
190
+ let fnResult; // fn 返回值透传(flow 产物,如 createDashboard 的看板名)
179
191
  let fnBlocked = false; // true when fn error is environment-blocked (skip agent Re-Act)
192
+ // 截图对比:fn 前开缓冲、fn 后收集 —— engine.expectScreenshot 的结果经此进 attachment
193
+ (0, screenshotDiff_js_1.beginGoalShotDiffCapture)();
180
194
  if (fn) {
181
195
  try {
182
- await fn();
196
+ fnResult = await fn();
183
197
  console.log(`[keveGoal] "${options.step}" fn executed`);
184
198
  }
185
199
  catch (err) {
186
200
  fnError = err?.message || String(err);
201
+ fnErrorRaw = err;
187
202
  console.log(`[keveGoal] "${options.step}" fn error: ${fnError}`);
188
203
  // ── Detect environment-blocked errors: skip Agent Re-Act, directly BLOCKED ──
189
204
  // When fn fails due to empty URL, connection refused, timeout, or navigation to invalid URL,
190
205
  // Agent exploration cannot fix environment issues — waste of time and tokens.
191
- fnBlocked = isEnvironmentBlockedError(err);
206
+ fnBlocked = (0, goal_heuristics_js_1.isEnvironmentBlockedError)(err);
192
207
  if (fnBlocked) {
193
208
  console.log(`[keveGoal] 🚫 "${options.step}" BLOCKED — environment error detected, skipping agent Re-Act`);
194
209
  }
210
+ // ── Detect page-level permission block (forbidden / unauthorized) ──
211
+ // When the page redirected to a forbidden/unauthorized URL (e.g. kwaibi /pc/dashboard/forbidden),
212
+ // the fn error is typically a locator timeout — but the root cause is access denial,
213
+ // not a missing element. Mark as blocked and annotate the error for accurate reporting.
214
+ if (!fnBlocked && fnError) {
215
+ try {
216
+ const blockedUrl = page.url();
217
+ if (/\/forbidden|\/unauthorized|\/403|\/no-permission/i.test(blockedUrl)) {
218
+ fnBlocked = true;
219
+ fnError = `页面访问被阻断(当前 URL=${blockedUrl}),目标元素未渲染 — 权限或数据问题,非用例缺陷`;
220
+ console.log(`[keveGoal] 🚫 "${options.step}" BLOCKED — page redirected to forbidden/unauthorized: ${blockedUrl}`);
221
+ }
222
+ }
223
+ catch { /* page.url() may fail if page is closed */ }
224
+ }
195
225
  }
196
226
  }
227
+ // fn 结束(无论成败)即取出本 goal 的对比结果;不再有引擎路径会往里写
228
+ const shotDiffs = (0, screenshotDiff_js_1.drainGoalShotDiff)();
197
229
  // ── Capture fn-after screenshot (fn 执行后的页面状态,传给 agent 说明执行前后) ──
198
230
  let fnAfterScreenshot = '';
199
231
  if (fn) {
@@ -204,7 +236,7 @@ exports.test = test_1.test.extend({
204
236
  }
205
237
  // ── Hand off to agent: agent does Re-Act (OR shortcut to blocked if fnBlocked) ──
206
238
  const currentUrl = page.url();
207
- const learnedHint = learned_actions_1.learnedActions.getHint(options.step, currentUrl);
239
+ const learnedHint = learned_actions_js_1.learnedActions.getHint(options.step, currentUrl);
208
240
  let reactResult;
209
241
  let reactTimedOut = false;
210
242
  if (fnBlocked) {
@@ -224,43 +256,92 @@ exports.test = test_1.test.extend({
224
256
  };
225
257
  }
226
258
  else {
227
- // ── Per-goal timeout: prevent one slow goal from exhausting the test timeout ──
228
- // Default 300s per goal; total test timeout should be ≥ (goals × 300s + overhead)
229
- const GOAL_TIMEOUT_MS = 300_000;
230
- const goalTimeoutController = new AbortController();
231
- const goalTimeout = setTimeout(() => {
232
- goalTimeoutController.abort();
233
- console.log(`[keveGoal] ⏰ "${options.step}" timed out after ${GOAL_TIMEOUT_MS}ms — stopping agent`);
234
- }, GOAL_TIMEOUT_MS);
259
+ // ── Page liveness check: if fn killed the page, skip Agent Re-Act ──
260
+ // When fn error involves page/browser close, the page is dead —
261
+ // Agent cannot take a11y snapshots or interact. Skip immediately.
262
+ let pageAlive = true;
235
263
  try {
236
- reactResult = await (0, agent_1.reactLoop)(page, options.step, options.expected, {
237
- learnedActionsHint: learnedHint,
238
- specFilePath: ctx.specFilePath,
239
- fnSource,
240
- fnResult: fnError ? { error: fnError } : { success: true },
241
- signal: goalTimeoutController.signal,
242
- goalScreenshotBefore: goalScreenshotBefore || undefined,
243
- fnAfterScreenshot: fnAfterScreenshot || undefined,
244
- });
264
+ await page.url();
265
+ }
266
+ catch {
267
+ pageAlive = false;
245
268
  }
246
- catch (reactErr) {
247
- reactTimedOut = true;
248
- const isGoalTimeout = goalTimeoutController.signal.aborted;
249
- const msg = isGoalTimeout
250
- ? `Goal timed out (${GOAL_TIMEOUT_MS}ms)`
251
- : (reactErr?.message || String(reactErr));
252
- console.log(`[keveGoal] reactLoop interrupted: ${msg.slice(0, 200)}`);
269
+ if (!pageAlive) {
270
+ console.log(`[keveGoal] 🚫 "${options.step}" BLOCKED — page is dead after fn error, skipping agent Re-Act`);
253
271
  reactResult = {
254
272
  expectedMet: false,
255
- actions: [],
273
+ actions: [{
274
+ action: { tool: 'done', verdict: 'blocked', text: `Page closed after fn error: ${fnError}` },
275
+ toolOutput: '',
276
+ result: 'ok',
277
+ }],
256
278
  finalSnapshot: '',
257
279
  agentScreenshots: [],
258
- conclusion: isGoalTimeout ? 'blocked' : 'fail',
280
+ conclusion: 'blocked',
259
281
  };
260
282
  }
261
- finally {
262
- clearTimeout(goalTimeout);
283
+ else if (fnErrorRaw instanceof screenshotDiff_js_1.ScreenshotDiffError) {
284
+ // ── 截图对比超阈值:确定性事实,跳过 Agent Re-Act ──
285
+ // diff 不是探索能修复的(页面就该和基线不一样才报错),探索只会烧 token;
286
+ // 对比结果已入 shotDiffs,attachment 会带上完整差异证据。
287
+ console.log(`[keveGoal] 📊 "${options.step}" screenshot diff exceeded — skipping agent Re-Act`);
288
+ reactResult = {
289
+ expectedMet: false,
290
+ actions: [{
291
+ action: { tool: 'done', verdict: 'fail', text: fnError },
292
+ toolOutput: fnErrorRaw.shotDiff?.message || '',
293
+ result: 'ok',
294
+ }],
295
+ finalSnapshot: '',
296
+ agentScreenshots: [],
297
+ conclusion: 'fail',
298
+ };
263
299
  }
300
+ else {
301
+ // ── Per-goal timeout: prevent one slow goal from exhausting the test timeout ──
302
+ // Default 300s per goal; total test timeout should be ≥ (goals × 300s + overhead)
303
+ const GOAL_TIMEOUT_MS = 300_000;
304
+ const goalTimeoutController = new AbortController();
305
+ const goalTimeout = setTimeout(() => {
306
+ goalTimeoutController.abort();
307
+ console.log(`[keveGoal] ⏰ "${options.step}" timed out after ${GOAL_TIMEOUT_MS}ms — stopping agent`);
308
+ }, GOAL_TIMEOUT_MS);
309
+ try {
310
+ reactResult = await (0, agent_js_1.reactLoop)(page, options.step, options.expected, {
311
+ learnedActionsHint: learnedHint,
312
+ specFilePath: ctx.specFilePath,
313
+ fnSource,
314
+ fnResult: fnError ? { error: fnError } : { success: true },
315
+ signal: goalTimeoutController.signal,
316
+ goalScreenshotBefore: goalScreenshotBefore || undefined,
317
+ fnAfterScreenshot: fnAfterScreenshot || undefined,
318
+ targetUrl: process.env.KEVE_TARGET_URL || '',
319
+ getEnv: (name) => process.env[name] || '',
320
+ saveScreenshot: saveAgentScreenshot,
321
+ loadInitialImages: loadInitialImages,
322
+ llm: reactLlmConfig(),
323
+ });
324
+ }
325
+ catch (reactErr) {
326
+ reactTimedOut = true;
327
+ const isGoalTimeout = goalTimeoutController.signal.aborted;
328
+ const msg = isGoalTimeout
329
+ ? `Goal timed out (${GOAL_TIMEOUT_MS}ms)`
330
+ : (reactErr?.message || String(reactErr));
331
+ console.log(`[keveGoal] reactLoop interrupted: ${msg.slice(0, 200)}`);
332
+ reactResult = {
333
+ expectedMet: false,
334
+ actions: [],
335
+ finalSnapshot: '',
336
+ agentScreenshots: [],
337
+ conclusion: isGoalTimeout ? 'blocked' : 'fail',
338
+ diagnostics: undefined,
339
+ };
340
+ }
341
+ finally {
342
+ clearTimeout(goalTimeout);
343
+ }
344
+ } // end of page-alive else (agent Re-Act path)
264
345
  } // end of else (non-blocked path)
265
346
  // ── Deterministic override: fn assertion failure cannot be overridden by Agent "pass" ──
266
347
  // When fn has a hard assertion failure (Expected X, Received Y), the value mismatch
@@ -268,7 +349,7 @@ exports.test = test_1.test.extend({
268
349
  // show 15px, so consistent") and override to pass, but 15px ≠ 16px is a definitive failure.
269
350
  // This is the per-goal equivalent of pipeline afterHook (case-gen-validate) —
270
351
  // deterministic check > LLM judgment.
271
- if (fnError && !fnBlocked && reactResult.expectedMet && isAssertionFailure(fnError)) {
352
+ if (fnError && !fnBlocked && reactResult.expectedMet && (0, goal_heuristics_js_1.isAssertionFailure)(fnError)) {
272
353
  console.log(`[keveGoal] ⚠️ "${options.step}" Agent said PASS but fn had assertion failure — overriding to FAIL`);
273
354
  console.log(`[keveGoal] fn assertion: ${fnError}`);
274
355
  reactResult.expectedMet = false;
@@ -282,13 +363,19 @@ exports.test = test_1.test.extend({
282
363
  ? undefined
283
364
  : reactResult.conclusion === 'blocked'
284
365
  ? new Error(`Blocked: ${fnBlocked ? fnError : (reactTimedOut ? `Agent 超时 (300000ms)` : (reactResult.actions?.filter((a) => a.action?.tool === 'done').pop()?.action?.text || 'Agent blocked'))}`)
285
- : (fnError && isAssertionFailure(fnError) && !fnBlocked)
286
- ? new Error(`Assertion failed: ${fnError}`)
287
- : new Error(`Expected not achieved: ${options.expected}`),
366
+ : (fnErrorRaw instanceof screenshotDiff_js_1.ScreenshotDiffError)
367
+ ? new Error(fnError) // 截图对比:保留带差异率/区块数/基线版本的结构化错误文案
368
+ : (fnError && (0, goal_heuristics_js_1.isAssertionFailure)(fnError) && !fnBlocked)
369
+ ? new Error(`Assertion failed: ${fnError}`)
370
+ : new Error(`Expected not achieved: ${options.expected}`),
288
371
  };
289
372
  // Attach Agent conclusion for downstream consumers
290
373
  if (reactResult.conclusion)
291
374
  result.conclusion = reactResult.conclusion;
375
+ // Attach runtime diagnostics for downstream consumers (Playwright side must
376
+ // copy them here because the attachment reads from `result`).
377
+ if (reactResult.diagnostics)
378
+ result.diagnostics = reactResult.diagnostics;
292
379
  console.log(reactResult.expectedMet
293
380
  ? `[keveGoal] ✅ "${options.step}" PASSED`
294
381
  : `[keveGoal] ❌ "${options.step}" FAILED (${reactResult.conclusion || 'fail'}) — ${result.error?.message || 'expected not achieved'}`);
@@ -384,21 +471,31 @@ exports.test = test_1.test.extend({
384
471
  }),
385
472
  finalSnapshot: result.finalSnapshot ? String(result.finalSnapshot).slice(0, 500) : undefined,
386
473
  diagnosticHints,
474
+ diagnostics: result.diagnostics,
387
475
  goalScreenshotBefore: goalScreenshotBefore || undefined,
388
476
  goalScreenshotAfter: goalScreenshotAfter || undefined,
389
- agentScreenshots: result.agentScreenshots || [],
477
+ // agent 未单独留图时,用 fn-after 兜底,保证报告至少有一张执行后截图。
478
+ // 不能写 `|| [...]`:空数组是 truthy,会把兜底截图短路丢弃。
479
+ agentScreenshots: result.agentScreenshots?.length
480
+ ? result.agentScreenshots
481
+ : (fnAfterScreenshot ? [fnAfterScreenshot] : []),
482
+ // 截图对比证据(matched/created/exceeded 全量保留;平台 stepsDetail 回收为 evidence.screenshotDiff)
483
+ screenshotDiff: shotDiffs.length ? shotDiffs : undefined,
390
484
  // refinePatch: (result as any).refinePatch || undefined, // 已注释:scriptRefine 已禁用
391
485
  conclusion: result.conclusion || undefined,
392
486
  }), 'utf-8'),
393
487
  });
394
488
  // Run after aspects
395
- await keve_aspect_1.keveAspect.runPhase('after', ctx, result, result.error);
489
+ await keve_aspect_js_1.keveAspect.runPhase('after', ctx, result, result.error);
396
490
  // Throw if not successful
397
491
  if (!result.success) {
398
492
  const failReason = result.error?.message || 'expected state not achieved';
399
493
  console.log(`[keveGoal] ❌ "${options.step}" FAILED — reason: ${failReason}`);
400
494
  throw result.error || new Error(`keveGoal "${options.step}" failed: ${failReason}`);
401
495
  }
496
+ // 透传 fn 返回值:flow 辅助函数(如 createDashboard 返回看板名)可包进 keveGoal,
497
+ // 脚本失败时由 Agent Re-Act 自愈,成功时把产物继续传给后续步骤
498
+ return fnResult;
402
499
  };
403
500
  await use(keveGoalFn);
404
501
  // keveGoals attachment 已删除:step/expected/precondition/order 已合并进
@@ -407,124 +504,77 @@ exports.test = test_1.test.extend({
407
504
  }
408
505
  });
409
506
  // Register extended test with keve-decorators so @keveModel uses the correct test
410
- const keve_decorators_2 = require("./keve-decorators");
411
- (0, keve_decorators_2.setKeveTest)(exports.test);
507
+ const keve_decorators_js_2 = require("../core/decorator/keve-decorators.js");
508
+ (0, keve_decorators_js_2.setKeveTest)(exports.test);
412
509
  // ── 辅助函数 ──
413
510
  /**
414
- * Detect environment-blocked errors from fn execution.
415
- *
416
- * When fn (deterministic script) fails with these patterns, Agent Re-Act cannot
417
- * fix the issue — the error is environmental, not application-level:
418
- *
419
- * 1. Empty/undefined URL → page.goto(''), page.goto(undefined)
420
- * 2. Connection refused / net::ERR_CONNECTION_REFUSED
421
- * 3. Navigation timeout (page not reachable)
422
- * 4. Invalid URL format
423
- * 5. SSO redirect / auth failure (not the app's fault)
424
- *
425
- * Returns true → keveGoal skips Agent Re-Act and directly reports BLOCKED
426
- * with the fn error as the blocking reason.
511
+ * 截图并保存到当前轮次的 reports/round-N,返回相对 taskDir 的路径
512
+ * @param phase 'before' | 'after-fn' — 用于文件名前缀
427
513
  */
428
- function isEnvironmentBlockedError(err) {
429
- const msg = (err?.message || String(err)).toLowerCase();
430
- // Empty/undefined URL: page.goto('') or page.goto(undefined)
431
- if (msg.includes('url must not be empty') || msg.includes('url is empty')
432
- || msg.includes('invalid url') || msg.includes('url is undefined')
433
- || msg.includes('navigation to ""') || msg.includes("navigation to ''")) {
434
- return true;
435
- }
436
- // page.goto(undefined): Playwright type error "url: expected string, got undefined"
437
- // This happens when process.env.PAGE_* is not set — environment config issue, not app issue
438
- if ((msg.includes('expected string') || msg.includes('expected a string'))
439
- && (msg.includes('got undefined') || msg.includes('got null') || msg.includes('received undefined'))) {
440
- return true;
441
- }
442
- // Connection refused / network unreachable
443
- if (msg.includes('err_connection_refused') || msg.includes('connection refused')
444
- || msg.includes('net::err_connection') || msg.includes('err_name_not_resolved')
445
- || msg.includes('err_address_unreachable') || msg.includes('err_internet_disconnected')
446
- || msg.includes('err_connection_timed_out') || msg.includes('err_connection_reset')) {
447
- return true;
448
- }
449
- // Navigation timeout (page not reachable in time)
450
- if (msg.includes('navigation timeout of') || msg.includes('timeout of') && msg.includes('exceeded')
451
- || msg.includes('page.goto: timeout') || msg.includes('navigating to') && msg.includes('timed out')) {
452
- return true;
453
- }
454
- // Playwright locator/waitForSelector timeout — selector not found on page (fn script issue, not app crash)
455
- // e.g. "Timeout 15000ms exceeded while waiting for locator('.selector') to be visible"
456
- // This is a script-quality issue (wrong selector), not an env blocked error.
457
- // Return false here — let Agent take over rather than treating as BLOCKED.
458
- // Note: we intentionally do NOT return true; these should fall through to Agent Re-Act.
459
- // IDC network segment restriction
460
- if (msg.includes('unable to access on the idc network segment')
461
- || msg.includes('idc network segment') || msg.includes('40314')) {
462
- return true;
463
- }
464
- // SSO / auth redirect (not the app's fault)
465
- if (msg.includes('sso redirect') || msg.includes('login redirect detected')
466
- || msg.includes('err_too_many_redirects')) {
467
- return true;
468
- }
469
- return false;
514
+ async function captureScreenshot(page, stepName, order, phase) {
515
+ const buf = await page.screenshot({ type: 'png', timeout: 15000 });
516
+ const safeName = stepName.replace(/[^a-zA-Z0-9一-鿿]/g, '_').slice(0, 30);
517
+ return writeScreenshot(buf, `goal-${phase}-${order}-${safeName}`);
470
518
  }
471
519
  /**
472
- * Detect assertion failures from fn execution (Expected/Received patterns).
473
- *
474
- * When fn (deterministic script) throws a hard assertion failure with concrete
475
- * Expected vs Received values, the mismatch is a *fact*, not a judgment.
476
- * The Agent may rationalize "consistency" (e.g. "both pages show 15px → consistent → pass")
477
- * and override to pass, but 15px ≠ 16px is a definitive failure.
478
- *
479
- * This function identifies such assertion failures so keveGoal can:
480
- * - Override Agent "pass" → "fail" when the fn proved a value mismatch
481
- * - Use the precise assertion message in the error (instead of generic "expected not achieved")
482
- *
483
- * Returns true for assertion errors, false for environment/runtime errors.
520
+ * 当前轮次截图目录绝对路径(与 Cypress 桥同名同层)。
521
+ * 新布局为 <taskRoot>/reports/round-N/screenshots;缺失时回退旧布局,
522
+ * 兼容历史执行环境。
484
523
  */
485
- function isAssertionFailure(error) {
486
- const msg = (error || '').toLowerCase();
487
- // Exclude Playwright API type errors (parameter type mismatch, not value assertion)
488
- // e.g. "page.goto: url: expected string, got undefined" — this is an env config error,
489
- // not a functional assertion failure. isEnvironmentBlockedError() handles it instead.
490
- if (msg.includes('expected string') || msg.includes('expected a string')
491
- || msg.includes('expected number') || msg.includes('expected boolean')) {
492
- // Only exclude if it looks like an API parameter error (not an assertion)
493
- if (msg.includes('got undefined') || msg.includes('got null')
494
- || msg.includes('received undefined') || msg.includes('received null')) {
495
- return false;
496
- }
524
+ function screenshotsDir() {
525
+ if (process.env.KEVE_RESULT_DIR) {
526
+ return path.join(path.resolve(process.env.KEVE_RESULT_DIR), 'screenshots');
497
527
  }
498
- // Jest/Vitest/playwright assertion: "Expected: X, Received: Y"
499
- if (msg.includes('expected') && msg.includes('received'))
500
- return true;
501
- // AssertionError / assert keyword
502
- if (msg.includes('assertionerror') || msg.includes('assertion failed')
503
- || msg.includes('assertion error'))
504
- return true;
505
- // expect(x).toBe(y) / expect(x).toEqual(y) pattern
506
- // Note: must NOT match Playwright timeout messages like "Timeout 15000ms exceeded waiting for locator(...) to be visible"
507
- // Those are env/timeout errors, not value assertion failures.
508
- if ((msg.includes(' to be ') && !msg.includes('timeout') && !msg.includes('waiting for') && !msg.includes('exceeded'))
509
- || msg.includes(' to equal ') || msg.includes(' to deeply equal '))
510
- return true;
511
- // Playwright expect: "expected string, received number" etc.
512
- if (msg.includes('expected ') && (msg.includes(' but ') || msg.includes(' got ') || msg.includes(' received ')))
513
- return true;
514
- return false;
515
- }
516
- /**
517
- * 截图并保存到 test-artifacts,返回相对 taskDir 的路径
518
- * @param phase 'before' | 'after-fn' — 用于文件名前缀
519
- */
520
- async function captureScreenshot(page, stepName, order, phase) {
521
- const buf = await page.screenshot({ type: 'png', timeout: 15000 });
522
528
  const taskDir = process.env.KEVE_TASK_DIR || '.keve';
523
529
  const round = process.env.KEVE_ROUND || 'latest';
524
- const screenshotsDir = path.join(taskDir, 'test-artifacts', `round-${round}`, 'screenshots');
525
- fs.mkdirSync(screenshotsDir, { recursive: true });
526
- const safeName = stepName.replace(/[^a-zA-Z0-9一-鿿]/g, '_').slice(0, 30);
527
- const file = path.join(screenshotsDir, `goal-${phase}-${order}-${safeName}-${Date.now()}.png`);
528
- fs.writeFileSync(file, buf);
530
+ return path.join(taskDir, 'test-artifacts', `round-${round}`, 'screenshots');
531
+ }
532
+ /** 落盘 PNG,返回相对 taskDir 的路径 */
533
+ function writeScreenshot(png, baseName) {
534
+ const taskDir = process.env.KEVE_TASK_DIR || '.keve';
535
+ const dir = screenshotsDir();
536
+ fs.mkdirSync(dir, { recursive: true });
537
+ const file = path.join(dir, `${baseName}-${Date.now()}.png`);
538
+ fs.writeFileSync(file, png);
529
539
  return path.relative(taskDir, file);
530
540
  }
541
+ /** Agent 单步截图落盘(宿主注入给 reactLoop,与 Cypress 桥的命名同形) */
542
+ async function saveAgentScreenshot(pngBase64, stepIndex, stepName) {
543
+ const safeName = stepName.replace(/[^a-zA-Z0-9一-鿿]/g, '_').slice(0, 30);
544
+ return writeScreenshot(Buffer.from(pngBase64, 'base64'), `agent-step${stepIndex}-${safeName}`);
545
+ }
546
+ /** 读取 goal-before / fn-after 图片,转成多模态 ContentItem(唯一注意:路径是相对 taskDir) */
547
+ async function loadInitialImages(paths) {
548
+ const taskDir = process.env.KEVE_TASK_DIR || '.keve';
549
+ const items = [];
550
+ for (const [label, rel] of [
551
+ ['goal-before', paths.goalScreenshotBefore],
552
+ ['fn-after', paths.fnAfterScreenshot],
553
+ ]) {
554
+ if (!rel)
555
+ continue;
556
+ try {
557
+ const file = path.isAbsolute(rel) ? rel : path.join(taskDir, rel);
558
+ if (!fs.existsSync(file))
559
+ continue;
560
+ const b64 = fs.readFileSync(file).toString('base64');
561
+ items.push({ type: 'text', text: `Screenshot (${label}):` });
562
+ items.push({ type: 'image_url', image_url: { url: `data:image/png;base64,${b64}` } });
563
+ }
564
+ catch { /* 单张图读失败不影响探索 */ }
565
+ }
566
+ return items;
567
+ }
568
+ /** Re-Act 的 LLM 配置:优先 keve.yaml,其次进程环境变量(与 config.ts DEFAULT_CONFIG 同源) */
569
+ function reactLlmConfig() {
570
+ let cfg = { base_url: '', model: '', api_key: '' };
571
+ try {
572
+ cfg = (0, config_js_1.loadConfig)().llm;
573
+ }
574
+ catch { /* 配置缺失时退回环境变量 */ }
575
+ return {
576
+ baseURL: cfg.base_url || process.env.KEVE_LLM_BASE_URL || '',
577
+ model: cfg.model || process.env.KEVE_LLM_MODEL_NAME || process.env.KEVE_LLM_MODEL || '',
578
+ apiKey: cfg.api_key || process.env.KEVE_LLM_API_KEY || '',
579
+ };
580
+ }