@kkeve/suite 0.0.15 → 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 (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 +619 -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
@@ -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,57 +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
- if (browser) {
108
- await use(browser);
109
- // CDP 模式下不关闭浏览器(共享连接,关闭会影响后续用例)
110
- }
111
- else {
112
- // CDP 不可达 → graceful fallback 到独立启动浏览器
113
- console.log(`[keve-test] CDP unavailable, falling back to chromium.launch()`);
114
- const fallback = await test_1.chromium.launch(testInfo.project?.use?.launchOptions || {});
115
- await use(fallback);
116
- await fallback.close();
117
- }
118
- }
119
- else {
120
- const browser = await test_1.chromium.launch(testInfo.project?.use?.launchOptions || {});
121
- await use(browser);
122
- await browser.close();
123
- }
116
+ const browser = await test_1.chromium.launch(testInfo.project?.use?.launchOptions || {});
117
+ await use(browser);
118
+ await browser.close();
124
119
  },
125
- page: async ({ browser }, use) => {
126
- const isCdp = (0, global_setup_1.isCdpMode)() && browser.isConnected();
127
- if (isCdp) {
128
- const contexts = browser.contexts();
129
- const cdpContext = contexts.length > 0 ? contexts[0] : await browser.newContext();
130
- // CDP 模式:优先复用已有 page(保留登录状态),仅在无可用 page 时新开
131
- const existingPages = cdpContext.pages();
132
- let page;
133
- if (existingPages.length > 0) {
134
- // 复用第一个非 about:blank 的 page(通常是用户已登录的 tab)
135
- page = existingPages.find(p => p.url() !== 'about:blank') || existingPages[0];
136
- // 导航到 about:blank 作为起始页(清空之前的状态,但保留 cookie)
137
- try {
138
- await page.goto('about:blank', { timeout: 3000 });
139
- }
140
- catch { /* ignore */ }
141
- }
142
- else {
143
- 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);
144
129
  }
145
- await use(page);
146
- // CDP 模式下不关闭 page(保留浏览器 tab 给下一个用例复用)
130
+ catch { /* 写失败则裸跑,用例会自行暴露登录问题 */ }
147
131
  }
148
- else {
149
- const page = await browser.newPage();
150
- await use(page);
151
- 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 { /* 无视频不阻塞 */ }
152
152
  }
153
153
  },
154
154
  // ── keveGoal: 唯一操作原语 ────────────────────────────────────────
@@ -162,9 +162,9 @@ exports.test = test_1.test.extend({
162
162
  order: goalOrder++,
163
163
  };
164
164
  const sceneTitle = testInfo.title;
165
- if (!keve_registry_1.sceneGoalsMap.has(sceneTitle))
166
- keve_registry_1.sceneGoalsMap.set(sceneTitle, []);
167
- 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);
168
168
  const ctx = {
169
169
  page,
170
170
  step: options.step,
@@ -175,7 +175,7 @@ exports.test = test_1.test.extend({
175
175
  specFilePath: testInfo.file,
176
176
  };
177
177
  // Run before aspects
178
- await keve_aspect_1.keveAspect.runPhase('before', ctx);
178
+ await keve_aspect_js_1.keveAspect.runPhase('before', ctx);
179
179
  // ── Capture goal-before screenshot (fn 之前的页面原始状态) ──
180
180
  // 语义:记录"测试动作发生前"的页面,供报告 before/after 对照
181
181
  let goalScreenshotBefore = '';
@@ -186,24 +186,46 @@ exports.test = test_1.test.extend({
186
186
  let result;
187
187
  const fnSource = fn?.toString() || undefined;
188
188
  let fnError;
189
+ let fnErrorRaw; // 原始错误对象(ScreenshotDiffError 等带结构的错误需要保留结构)
190
+ let fnResult; // fn 返回值透传(flow 产物,如 createDashboard 的看板名)
189
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)();
190
194
  if (fn) {
191
195
  try {
192
- await fn();
196
+ fnResult = await fn();
193
197
  console.log(`[keveGoal] "${options.step}" fn executed`);
194
198
  }
195
199
  catch (err) {
196
200
  fnError = err?.message || String(err);
201
+ fnErrorRaw = err;
197
202
  console.log(`[keveGoal] "${options.step}" fn error: ${fnError}`);
198
203
  // ── Detect environment-blocked errors: skip Agent Re-Act, directly BLOCKED ──
199
204
  // When fn fails due to empty URL, connection refused, timeout, or navigation to invalid URL,
200
205
  // Agent exploration cannot fix environment issues — waste of time and tokens.
201
- fnBlocked = isEnvironmentBlockedError(err);
206
+ fnBlocked = (0, goal_heuristics_js_1.isEnvironmentBlockedError)(err);
202
207
  if (fnBlocked) {
203
208
  console.log(`[keveGoal] 🚫 "${options.step}" BLOCKED — environment error detected, skipping agent Re-Act`);
204
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
+ }
205
225
  }
206
226
  }
227
+ // fn 结束(无论成败)即取出本 goal 的对比结果;不再有引擎路径会往里写
228
+ const shotDiffs = (0, screenshotDiff_js_1.drainGoalShotDiff)();
207
229
  // ── Capture fn-after screenshot (fn 执行后的页面状态,传给 agent 说明执行前后) ──
208
230
  let fnAfterScreenshot = '';
209
231
  if (fn) {
@@ -214,7 +236,7 @@ exports.test = test_1.test.extend({
214
236
  }
215
237
  // ── Hand off to agent: agent does Re-Act (OR shortcut to blocked if fnBlocked) ──
216
238
  const currentUrl = page.url();
217
- const learnedHint = learned_actions_1.learnedActions.getHint(options.step, currentUrl);
239
+ const learnedHint = learned_actions_js_1.learnedActions.getHint(options.step, currentUrl);
218
240
  let reactResult;
219
241
  let reactTimedOut = false;
220
242
  if (fnBlocked) {
@@ -258,6 +280,23 @@ exports.test = test_1.test.extend({
258
280
  conclusion: 'blocked',
259
281
  };
260
282
  }
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
+ };
299
+ }
261
300
  else {
262
301
  // ── Per-goal timeout: prevent one slow goal from exhausting the test timeout ──
263
302
  // Default 300s per goal; total test timeout should be ≥ (goals × 300s + overhead)
@@ -268,7 +307,7 @@ exports.test = test_1.test.extend({
268
307
  console.log(`[keveGoal] ⏰ "${options.step}" timed out after ${GOAL_TIMEOUT_MS}ms — stopping agent`);
269
308
  }, GOAL_TIMEOUT_MS);
270
309
  try {
271
- reactResult = await (0, agent_1.reactLoop)(page, options.step, options.expected, {
310
+ reactResult = await (0, agent_js_1.reactLoop)(page, options.step, options.expected, {
272
311
  learnedActionsHint: learnedHint,
273
312
  specFilePath: ctx.specFilePath,
274
313
  fnSource,
@@ -276,6 +315,11 @@ exports.test = test_1.test.extend({
276
315
  signal: goalTimeoutController.signal,
277
316
  goalScreenshotBefore: goalScreenshotBefore || undefined,
278
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(),
279
323
  });
280
324
  }
281
325
  catch (reactErr) {
@@ -291,6 +335,7 @@ exports.test = test_1.test.extend({
291
335
  finalSnapshot: '',
292
336
  agentScreenshots: [],
293
337
  conclusion: isGoalTimeout ? 'blocked' : 'fail',
338
+ diagnostics: undefined,
294
339
  };
295
340
  }
296
341
  finally {
@@ -304,7 +349,7 @@ exports.test = test_1.test.extend({
304
349
  // show 15px, so consistent") and override to pass, but 15px ≠ 16px is a definitive failure.
305
350
  // This is the per-goal equivalent of pipeline afterHook (case-gen-validate) —
306
351
  // deterministic check > LLM judgment.
307
- if (fnError && !fnBlocked && reactResult.expectedMet && isAssertionFailure(fnError)) {
352
+ if (fnError && !fnBlocked && reactResult.expectedMet && (0, goal_heuristics_js_1.isAssertionFailure)(fnError)) {
308
353
  console.log(`[keveGoal] ⚠️ "${options.step}" Agent said PASS but fn had assertion failure — overriding to FAIL`);
309
354
  console.log(`[keveGoal] fn assertion: ${fnError}`);
310
355
  reactResult.expectedMet = false;
@@ -318,13 +363,19 @@ exports.test = test_1.test.extend({
318
363
  ? undefined
319
364
  : reactResult.conclusion === 'blocked'
320
365
  ? new Error(`Blocked: ${fnBlocked ? fnError : (reactTimedOut ? `Agent 超时 (300000ms)` : (reactResult.actions?.filter((a) => a.action?.tool === 'done').pop()?.action?.text || 'Agent blocked'))}`)
321
- : (fnError && isAssertionFailure(fnError) && !fnBlocked)
322
- ? new Error(`Assertion failed: ${fnError}`)
323
- : 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}`),
324
371
  };
325
372
  // Attach Agent conclusion for downstream consumers
326
373
  if (reactResult.conclusion)
327
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;
328
379
  console.log(reactResult.expectedMet
329
380
  ? `[keveGoal] ✅ "${options.step}" PASSED`
330
381
  : `[keveGoal] ❌ "${options.step}" FAILED (${reactResult.conclusion || 'fail'}) — ${result.error?.message || 'expected not achieved'}`);
@@ -420,21 +471,31 @@ exports.test = test_1.test.extend({
420
471
  }),
421
472
  finalSnapshot: result.finalSnapshot ? String(result.finalSnapshot).slice(0, 500) : undefined,
422
473
  diagnosticHints,
474
+ diagnostics: result.diagnostics,
423
475
  goalScreenshotBefore: goalScreenshotBefore || undefined,
424
476
  goalScreenshotAfter: goalScreenshotAfter || undefined,
425
- 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,
426
484
  // refinePatch: (result as any).refinePatch || undefined, // 已注释:scriptRefine 已禁用
427
485
  conclusion: result.conclusion || undefined,
428
486
  }), 'utf-8'),
429
487
  });
430
488
  // Run after aspects
431
- await keve_aspect_1.keveAspect.runPhase('after', ctx, result, result.error);
489
+ await keve_aspect_js_1.keveAspect.runPhase('after', ctx, result, result.error);
432
490
  // Throw if not successful
433
491
  if (!result.success) {
434
492
  const failReason = result.error?.message || 'expected state not achieved';
435
493
  console.log(`[keveGoal] ❌ "${options.step}" FAILED — reason: ${failReason}`);
436
494
  throw result.error || new Error(`keveGoal "${options.step}" failed: ${failReason}`);
437
495
  }
496
+ // 透传 fn 返回值:flow 辅助函数(如 createDashboard 返回看板名)可包进 keveGoal,
497
+ // 脚本失败时由 Agent Re-Act 自愈,成功时把产物继续传给后续步骤
498
+ return fnResult;
438
499
  };
439
500
  await use(keveGoalFn);
440
501
  // keveGoals attachment 已删除:step/expected/precondition/order 已合并进
@@ -443,134 +504,77 @@ exports.test = test_1.test.extend({
443
504
  }
444
505
  });
445
506
  // Register extended test with keve-decorators so @keveModel uses the correct test
446
- const keve_decorators_2 = require("./keve-decorators");
447
- (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);
448
509
  // ── 辅助函数 ──
449
510
  /**
450
- * Detect environment-blocked errors from fn execution.
451
- *
452
- * When fn (deterministic script) fails with these patterns, Agent Re-Act cannot
453
- * fix the issue — the error is environmental, not application-level:
454
- *
455
- * 1. Empty/undefined URL → page.goto(''), page.goto(undefined)
456
- * 2. Connection refused / net::ERR_CONNECTION_REFUSED
457
- * 3. Navigation timeout (page not reachable)
458
- * 4. Invalid URL format
459
- * 5. SSO redirect / auth failure (not the app's fault)
460
- *
461
- * Returns true → keveGoal skips Agent Re-Act and directly reports BLOCKED
462
- * with the fn error as the blocking reason.
511
+ * 截图并保存到当前轮次的 reports/round-N,返回相对 taskDir 的路径
512
+ * @param phase 'before' | 'after-fn' — 用于文件名前缀
463
513
  */
464
- function isEnvironmentBlockedError(err) {
465
- const msg = (err?.message || String(err)).toLowerCase();
466
- // Empty/undefined URL: page.goto('') or page.goto(undefined)
467
- if (msg.includes('url must not be empty') || msg.includes('url is empty')
468
- || msg.includes('invalid url') || msg.includes('url is undefined')
469
- || msg.includes('navigation to ""') || msg.includes("navigation to ''")) {
470
- return true;
471
- }
472
- // page.goto(undefined): Playwright type error "url: expected string, got undefined"
473
- // This happens when process.env.PAGE_* is not set — environment config issue, not app issue
474
- if ((msg.includes('expected string') || msg.includes('expected a string'))
475
- && (msg.includes('got undefined') || msg.includes('got null') || msg.includes('received undefined'))) {
476
- return true;
477
- }
478
- // Connection refused / network unreachable
479
- if (msg.includes('err_connection_refused') || msg.includes('connection refused')
480
- || msg.includes('net::err_connection') || msg.includes('err_name_not_resolved')
481
- || msg.includes('err_address_unreachable') || msg.includes('err_internet_disconnected')
482
- || msg.includes('err_connection_timed_out') || msg.includes('err_connection_reset')) {
483
- return true;
484
- }
485
- // Navigation timeout (page not reachable in time)
486
- if (msg.includes('navigation timeout of') || msg.includes('timeout of') && msg.includes('exceeded')
487
- || msg.includes('page.goto: timeout') || msg.includes('navigating to') && msg.includes('timed out')) {
488
- return true;
489
- }
490
- // Playwright locator/waitForSelector timeout — selector not found on page (fn script issue, not app crash)
491
- // e.g. "Timeout 15000ms exceeded while waiting for locator('.selector') to be visible"
492
- // This is a script-quality issue (wrong selector), not an env blocked error.
493
- // Return false here — let Agent take over rather than treating as BLOCKED.
494
- // Note: we intentionally do NOT return true; these should fall through to Agent Re-Act.
495
- // IDC network segment restriction
496
- if (msg.includes('unable to access on the idc network segment')
497
- || msg.includes('idc network segment') || msg.includes('40314')) {
498
- return true;
499
- }
500
- // SSO / auth redirect (not the app's fault)
501
- if (msg.includes('sso redirect') || msg.includes('login redirect detected')
502
- || msg.includes('err_too_many_redirects')) {
503
- return true;
504
- }
505
- // Page/context/browser closed — dead page, Agent cannot explore
506
- // e.g. "page.waitForLoadState: Target page, context or browser has been closed"
507
- // e.g. "page.ariaSnapshot: Target page, context or browser has been closed"
508
- // When the CDP connection drops or the user closes the tab, any page API throws this.
509
- // Agent Re-Act on a dead page is a waste of time → BLOCKED immediately.
510
- if (msg.includes('target page, context or browser has been closed')
511
- || msg.includes('page has been closed') || msg.includes('browser has been closed')
512
- || msg.includes('context has been closed')) {
513
- return true;
514
- }
515
- 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}`);
516
518
  }
517
519
  /**
518
- * Detect assertion failures from fn execution (Expected/Received patterns).
519
- *
520
- * When fn (deterministic script) throws a hard assertion failure with concrete
521
- * Expected vs Received values, the mismatch is a *fact*, not a judgment.
522
- * The Agent may rationalize "consistency" (e.g. "both pages show 15px → consistent → pass")
523
- * and override to pass, but 15px ≠ 16px is a definitive failure.
524
- *
525
- * This function identifies such assertion failures so keveGoal can:
526
- * - Override Agent "pass" → "fail" when the fn proved a value mismatch
527
- * - Use the precise assertion message in the error (instead of generic "expected not achieved")
528
- *
529
- * Returns true for assertion errors, false for environment/runtime errors.
520
+ * 当前轮次截图目录绝对路径(与 Cypress 桥同名同层)。
521
+ * 新布局为 <taskRoot>/reports/round-N/screenshots;缺失时回退旧布局,
522
+ * 兼容历史执行环境。
530
523
  */
531
- function isAssertionFailure(error) {
532
- const msg = (error || '').toLowerCase();
533
- // Exclude Playwright API type errors (parameter type mismatch, not value assertion)
534
- // e.g. "page.goto: url: expected string, got undefined" — this is an env config error,
535
- // not a functional assertion failure. isEnvironmentBlockedError() handles it instead.
536
- if (msg.includes('expected string') || msg.includes('expected a string')
537
- || msg.includes('expected number') || msg.includes('expected boolean')) {
538
- // Only exclude if it looks like an API parameter error (not an assertion)
539
- if (msg.includes('got undefined') || msg.includes('got null')
540
- || msg.includes('received undefined') || msg.includes('received null')) {
541
- return false;
542
- }
524
+ function screenshotsDir() {
525
+ if (process.env.KEVE_RESULT_DIR) {
526
+ return path.join(path.resolve(process.env.KEVE_RESULT_DIR), 'screenshots');
543
527
  }
544
- // Jest/Vitest/playwright assertion: "Expected: X, Received: Y"
545
- if (msg.includes('expected') && msg.includes('received'))
546
- return true;
547
- // AssertionError / assert keyword
548
- if (msg.includes('assertionerror') || msg.includes('assertion failed')
549
- || msg.includes('assertion error'))
550
- return true;
551
- // expect(x).toBe(y) / expect(x).toEqual(y) pattern
552
- // Note: must NOT match Playwright timeout messages like "Timeout 15000ms exceeded waiting for locator(...) to be visible"
553
- // Those are env/timeout errors, not value assertion failures.
554
- if ((msg.includes(' to be ') && !msg.includes('timeout') && !msg.includes('waiting for') && !msg.includes('exceeded'))
555
- || msg.includes(' to equal ') || msg.includes(' to deeply equal '))
556
- return true;
557
- // Playwright expect: "expected string, received number" etc.
558
- if (msg.includes('expected ') && (msg.includes(' but ') || msg.includes(' got ') || msg.includes(' received ')))
559
- return true;
560
- return false;
561
- }
562
- /**
563
- * 截图并保存到 test-artifacts,返回相对 taskDir 的路径
564
- * @param phase 'before' | 'after-fn' — 用于文件名前缀
565
- */
566
- async function captureScreenshot(page, stepName, order, phase) {
567
- const buf = await page.screenshot({ type: 'png', timeout: 15000 });
568
528
  const taskDir = process.env.KEVE_TASK_DIR || '.keve';
569
529
  const round = process.env.KEVE_ROUND || 'latest';
570
- const screenshotsDir = path.join(taskDir, 'test-artifacts', `round-${round}`, 'screenshots');
571
- fs.mkdirSync(screenshotsDir, { recursive: true });
572
- const safeName = stepName.replace(/[^a-zA-Z0-9一-鿿]/g, '_').slice(0, 30);
573
- const file = path.join(screenshotsDir, `goal-${phase}-${order}-${safeName}-${Date.now()}.png`);
574
- 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);
575
539
  return path.relative(taskDir, file);
576
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
+ }
@@ -43,7 +43,8 @@ exports.generatePwConfig = generatePwConfig;
43
43
  exports.resolveConfigOutputPath = resolveConfigOutputPath;
44
44
  exports.findExistingConfig = findExistingConfig;
45
45
  const path = __importStar(require("node:path"));
46
- const config_1 = require("../config");
46
+ const fs = __importStar(require("node:fs"));
47
+ const config_js_1 = require("../config.js");
47
48
  function generatePwConfig(options = {}) {
48
49
  const { baseUrl, cdp = false, cdpEndpoint, webServer = !cdp, workers = 3, timeout = 1200000, retries = 0, format = 'ts', } = options;
49
50
  const baseURLLine = baseUrl
@@ -89,7 +90,7 @@ export default defineConfig({
89
90
  baseURL: ${baseURLLine},${cdp ? `\n // CDP mode: login state preserved via connected Chrome` : `\n storageState: path.resolve(projectRoot, '.auth/storage-state.json'),`}
90
91
  trace: 'on-first-retry',
91
92
  screenshot: 'off',
92
- video: 'retain-on-failure',
93
+ video: 'on',
93
94
  actionTimeout: 5000,${cdpBlock}
94
95
  },
95
96
  outputDir: path.join(resultDir, 'test-results'),${webServerBlock}
@@ -137,7 +138,7 @@ export default defineConfig({
137
138
  baseURL: ${baseURLLine},${cdp ? `\n // CDP mode: login state preserved via connected Chrome` : `\n storageState: resolve(projectRoot, '.auth', 'storage-state.json'),`}
138
139
  trace: 'on-first-retry',
139
140
  screenshot: 'off',
140
- video: 'retain-on-failure',
141
+ video: 'on',
141
142
  actionTimeout: 5000,${cdpBlock}
142
143
  },
143
144
  outputDir: join(resultDir, 'test-results'),${webServerBlock}
@@ -148,12 +149,12 @@ function resolveConfigOutputPath(explicitOutput, format = 'ts') {
148
149
  const ext = format === 'mjs' ? '.mjs' : '.ts';
149
150
  if (explicitOutput)
150
151
  return path.resolve(explicitOutput);
151
- const keveDir = (0, config_1.findKeveDir)();
152
- const taskDir = (0, config_1.resolveTaskDir)(keveDir);
152
+ const keveDir = (0, config_js_1.findKeveDir)();
153
+ const taskDir = (0, config_js_1.resolveTaskDir)(keveDir);
153
154
  if (taskDir)
154
155
  return path.join(taskDir, `keve-test.config${ext}`);
155
156
  if (keveDir) {
156
- const branchDir = (0, config_1.getBranchDir)(keveDir);
157
+ const branchDir = (0, config_js_1.getBranchDir)(keveDir);
157
158
  if (branchDir)
158
159
  return path.join(branchDir, `keve-test.config${ext}`);
159
160
  return path.join(keveDir, `keve-test.config${ext}`);
@@ -173,7 +174,7 @@ function findExistingConfig(keveDir, taskDir) {
173
174
  searchPaths.push(path.join(process.cwd(), 'keve_test_spec', 'keve-test.config.mjs'));
174
175
  searchPaths.push(path.join(process.cwd(), 'keve_test_spec', 'keve-test.config.ts'));
175
176
  for (const p of searchPaths) {
176
- if (require('fs').existsSync(p))
177
+ if (fs.existsSync(p))
177
178
  return p;
178
179
  }
179
180
  return null;