@kkeve/suite 0.0.11 → 0.0.12

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.
@@ -165,11 +165,13 @@ export interface CaseReportItem {
165
165
  stdout: string;
166
166
  /** Playwright 步骤 */
167
167
  pwSteps: any[];
168
- /** AI评估结论: "通过" | "不通过" | "待确认" */
168
+ /** AI评估结论: "通过" | "不通过" | "阻塞" | "跳过" */
169
169
  data: string;
170
170
  /** AI评估置信度 0-100 */
171
171
  confidence: number;
172
- /** AI评估推理过程 */
172
+ /** AI评估 thought(原始推理/原因文本,用于根因展示) */
173
+ thought: string;
174
+ /** AI评估推理过程(同 thought,保持向后兼容) */
173
175
  confidenceReason: string;
174
176
  /** 步骤截图路径列表(前端按需加载,不再内嵌 base64) */
175
177
  stepScreenshots: {
@@ -437,7 +437,7 @@ async function generateReportData(opts) {
437
437
  // Derive taskDir from resultsPath: .../test-artifacts/round-N/test-results.json → taskDir is 2 levels up from test-artifacts/
438
438
  const taskDir = path.resolve(path.dirname(TEST_RESULTS_JSON), '..', '..');
439
439
  const caseResultMap = buildCaseResultMap(specs, taskDir);
440
- let total = 0, passed = 0, failed = 0, skipped = 0;
440
+ let total = 0;
441
441
  const caseResults = [];
442
442
  const confidenceDist = { '95-100': 0, '61-90': 0, '41-60': 0, '0-40': 0 };
443
443
  // Read confidence-data.jsonl (AI evaluation results from KeveReporter)
@@ -516,12 +516,6 @@ async function generateReportData(opts) {
516
516
  total++;
517
517
  // Playwright returns "timedOut" for test timeout — treat as failed
518
518
  const effectiveStatus = result.status === 'timedOut' ? 'failed' : result.status;
519
- if (effectiveStatus === 'passed')
520
- passed++;
521
- else if (effectiveStatus === 'skipped')
522
- skipped++;
523
- else
524
- failed++;
525
519
  // AI evaluation: match by Playwright title (scene.title, the canonical key)
526
520
  const aiEntry = aiConfidenceMap[result.title] || aiConfidenceMap[caseId];
527
521
  const data = effectiveStatus === 'skipped' ? '待确认' : (aiEntry?.data || '待确认');
@@ -652,6 +646,7 @@ async function generateReportData(opts) {
652
646
  stdout: result.stdout,
653
647
  data,
654
648
  confidence,
649
+ thought: aiEntry?.thought || (effectiveStatus === 'skipped' ? '用例被跳过(非自动执行)' : ''),
655
650
  confidenceReason,
656
651
  stepScreenshots: stepScreenshotsPaths,
657
652
  screenshotPath: result.screenshotPath,
@@ -668,7 +663,6 @@ async function generateReportData(opts) {
668
663
  if (caseResultMap[caseDef.id])
669
664
  continue; // Already included above
670
665
  total++;
671
- failed++;
672
666
  // Missing cases: test was not executed by Playwright, so AI evaluation is skipped
673
667
  const data = '跳过';
674
668
  const confidence = 0;
@@ -692,6 +686,7 @@ async function generateReportData(opts) {
692
686
  stdout: '',
693
687
  data,
694
688
  confidence,
689
+ thought: 'YAML中定义但Playwright未执行',
695
690
  confidenceReason: 'YAML中定义但Playwright未执行,无AI评估数据',
696
691
  stepScreenshots: [],
697
692
  screenshotPath: '',
@@ -702,15 +697,16 @@ async function generateReportData(opts) {
702
697
  });
703
698
  }
704
699
  }
705
- const passRate = total > 0 ? ((passed / total) * 100).toFixed(1) : '0';
706
700
  const summaryJson = {
707
- status: failed > 0 ? 'partial' : 'success',
701
+ status: 'partial',
708
702
  mr_id: testCasesData.mr_id, description: testCasesData.description,
709
703
  docs_url: testCasesData.docs_url,
710
704
  generated_at: new Date().toISOString().split('T')[0],
711
705
  round: currentRound,
712
706
  phases: { phase1: 'completed', phase2: 'completed', phase3: 'completed', phase4: 'completed' },
713
- summary: { total, passed, failed, skipped, pending: 0, passRate: `${passRate}%` },
707
+ // passed/failed/skipped/passRate 已移至前端 computedSummary 实时计算
708
+ // 保留 total 供兜底展示
709
+ summary: { total, round: currentRound },
714
710
  confidenceDist: {
715
711
  '95-100%可信': confidenceDist['95-100'],
716
712
  '61-90%需分析': confidenceDist['61-90'],
@@ -12,7 +12,7 @@ export interface KeveGoalMeta {
12
12
  order: number;
13
13
  }
14
14
  /** 错误分类类型 */
15
- export type ErrorCategory = 'script' | 'env' | 'assert' | 'visual' | 'text-mismatch' | 'react-fail' | 'pass' | 'unknown';
15
+ export type ErrorCategory = 'script' | 'env' | 'assert' | 'visual' | 'text-mismatch' | 'incomplete' | 'pass' | 'unknown';
16
16
  /** 场景执行后的评估元数据,由 @keveScene 装饰器写入,Reporter 读取 */
17
17
  export interface KeveEvalMeta {
18
18
  /** 错误分类 */
@@ -76,7 +76,7 @@ class KeveReporter {
76
76
  visual: 80, // 视觉断言已评估,置信度较高
77
77
  'text-mismatch': 70, // UI文案格式不匹配(功能逻辑正确,文案措辞变了)
78
78
  assert: 50, // Playwright 断言失败,需人工确认
79
- 'react-fail': 40, // AI 探索未达预期,需人工确认
79
+ incomplete: 0, // 执行被中断(超时/action 耗尽),无法得出结论
80
80
  env: 0, // 环境问题,测试结果不可信
81
81
  unknown: 0, // 未知异常,无参考价值
82
82
  };
@@ -89,8 +89,18 @@ class KeveReporter {
89
89
  let skipAI = false;
90
90
  let classifiedMessage = '';
91
91
  if (result.status === 'skipped') {
92
- skipAI = true;
93
- effectiveCategory = 'unknown';
92
+ // 主动跳过(testInfo.skip,如"非自动执行")— 独立处理,data='跳过'
93
+ this.appendRecord({
94
+ title: test.title,
95
+ data: '跳过',
96
+ confidence: 0,
97
+ thought: '用例被跳过(非自动执行)',
98
+ errorCategory: 'unknown',
99
+ steps,
100
+ diagnosticHint: undefined,
101
+ });
102
+ console.log(`[keve-reporter] ${test.title}: SKIPPED`);
103
+ return;
94
104
  }
95
105
  else if (result.status === 'passed') {
96
106
  effectiveCategory = 'pass';
@@ -107,6 +117,11 @@ class KeveReporter {
107
117
  effectiveCategory = 'env';
108
118
  skipAI = true;
109
119
  }
120
+ else if (agentConclusion.result === 'incomplete') {
121
+ // Agent 执行被中断(超时/action 耗尽),无法得出结论
122
+ effectiveCategory = 'incomplete';
123
+ skipAI = true;
124
+ }
110
125
  else {
111
126
  // Agent said fail → assert/text-mismatch depending on diagnostic hints
112
127
  const hint = buildDiagnosticHint(result.attachments);
@@ -145,10 +160,10 @@ class KeveReporter {
145
160
  effectiveCategory = 'env';
146
161
  skipAI = true;
147
162
  }
148
- // 5. Re-Act loop 失败:AI 探索未达到预期(关键分类)
163
+ // 5. Re-Act loop 失败:执行被中断(action 耗尽 / 未达预期)→ incomplete
149
164
  else if (rawError.includes('Re-Act loop did not achieve') || rawError.includes('Expected not achieved after agent explore')) {
150
- effectiveCategory = 'react-fail';
151
- skipAI = false;
165
+ effectiveCategory = 'incomplete';
166
+ skipAI = true;
152
167
  }
153
168
  // 5.5 文案不匹配
154
169
  else if (rawError.includes('Expected not achieved')) {
@@ -158,8 +173,8 @@ class KeveReporter {
158
173
  skipAI = false;
159
174
  }
160
175
  else {
161
- effectiveCategory = 'react-fail';
162
- skipAI = false;
176
+ effectiveCategory = 'incomplete';
177
+ skipAI = true;
163
178
  }
164
179
  }
165
180
  // 6. 兜底
@@ -180,9 +195,13 @@ class KeveReporter {
180
195
  ? `无法分类的异常(${classifiedMessage}),跳过AI评估`
181
196
  : effectiveCategory === 'pass'
182
197
  ? `Agent判定通过但fn脚本执行失败(${classifiedMessage}),脚本需修复`
183
- : '用例被跳过';
184
- // AI评估列显示"跳过"(表示AI未参与评估),而非"不通过"
185
- const aiData = (effectiveCategory !== 'assert' && effectiveCategory !== 'react-fail' && effectiveCategory !== 'text-mismatch') ? '跳过' : '不通过';
198
+ : effectiveCategory === 'incomplete'
199
+ ? `执行被中断(${classifiedMessage}),Agent未完成目标,建议重跑`
200
+ : `无法分类的异常(${classifiedMessage})`;
201
+ // AI评估列输出:
202
+ // - assert/text-mismatch → "不通过"(有明确失败结论,skipAI=false)
203
+ // - 其余所有无法得出结论的 → "阻塞"(包括 incomplete/env/script/visual/pass/unknown)
204
+ const aiData = (effectiveCategory === 'assert' || effectiveCategory === 'text-mismatch') ? '不通过' : '阻塞';
186
205
  const keveScreenshots = (0, reportData_1.parseKeveAsserts)(result.attachments).map(ka => ka.label);
187
206
  this.appendRecord({
188
207
  title: test.title,
@@ -307,26 +326,26 @@ function parseSteps(attachments) {
307
326
  }
308
327
  /** Extract Agent conclusion (result + text) from keveGoalResult steps */
309
328
  function extractAgentConclusion(steps) {
310
- // Walk steps in reverse to find the most recent conclusion
311
- for (let i = steps.length - 1; i >= 0; i--) {
312
- const step = steps[i];
313
- // Step-level conclusion (from agent.ts buildResult → keveGoalResult attachment)
314
- if (step.conclusion) {
315
- // Find the done action text for context
316
- const doneAction = step.actions?.find(a => a.tool === 'done');
317
- return { result: step.conclusion, text: doneAction?.text || '' };
318
- }
319
- // Action-level conclusion (from done action's 'result' field)
320
- if (step.actions) {
321
- for (let j = step.actions.length - 1; j >= 0; j--) {
322
- const action = step.actions[j];
323
- if (action?.tool === 'done' && action?.conclusion) {
324
- return { result: action.conclusion, text: action.text || '' };
325
- }
329
+ if (steps.length === 0)
330
+ return undefined;
331
+ // keveGoal 串行执行:取最后一步的结论作为用例整体结论
332
+ const lastStep = steps[steps.length - 1];
333
+ // 最后一步有明确结论(Agent 调用了 done)→ 直接使用
334
+ if (lastStep.conclusion) {
335
+ const doneAction = lastStep.actions?.find(a => a.tool === 'done');
336
+ return { result: lastStep.conclusion, text: doneAction?.text || '' };
337
+ }
338
+ // Action 级别结论兜底(来自 done action 的 result 字段)
339
+ if (lastStep.actions) {
340
+ for (let j = lastStep.actions.length - 1; j >= 0; j--) {
341
+ const action = lastStep.actions[j];
342
+ if (action?.tool === 'done' && action?.conclusion) {
343
+ return { result: action.conclusion, text: action.text || '' };
326
344
  }
327
345
  }
328
346
  }
329
- return undefined;
347
+ // 最后一步无任何结论 → 执行被中断(超时 / action 耗尽)
348
+ return { result: 'incomplete', text: '执行被中断,Agent未完成目标' };
330
349
  }
331
350
  /** Build diagnosticHint from keveGoalResult attachments (primary) + fallback to keveDiagnosticHint */
332
351
  function buildDiagnosticHint(attachments) {
@@ -39,5 +39,7 @@ type KeveFixture = {
39
39
  noImages?: boolean;
40
40
  outputPath?: string;
41
41
  }) => Promise<any>;
42
+ /** 将内置 testInfo 包装为可解构 fixture,允许脚本直接写 { page, keveGoal, testInfo } */
43
+ testInfo: import('@playwright/test').TestInfo;
42
44
  };
43
45
  export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & KeveFixture, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
@@ -95,6 +95,11 @@ keve_aspect_1.keveAspect.register({
95
95
  // ─── (diagnostic extraction moved to page-agent/diagnostic.ts, called via onAfterTask hook) ──
96
96
  // ─── Fixture ────────────────────────────────────────────────────────
97
97
  exports.test = test_1.test.extend({
98
+ // ── 将内置 testInfo 包装为可解构 fixture ─────────────────────────
99
+ // 测试方法可直接写 { page, keveGoal, testInfo },不再报 "unknown parameter" 错误
100
+ testInfo: async ({}, use, testInfo) => {
101
+ await use(testInfo);
102
+ },
98
103
  browser: async ({}, use, testInfo) => {
99
104
  if ((0, global_setup_1.isCdpMode)()) {
100
105
  const browser = await (0, global_setup_1.getCdpBrowser)(); // 复用全局共享的 CDP browser
@@ -170,6 +175,7 @@ exports.test = test_1.test.extend({
170
175
  let result;
171
176
  const fnSource = fn?.toString() || undefined;
172
177
  let fnError;
178
+ let fnBlocked = false; // true when fn error is environment-blocked (skip agent Re-Act)
173
179
  if (fn) {
174
180
  try {
175
181
  await fn();
@@ -178,6 +184,13 @@ exports.test = test_1.test.extend({
178
184
  catch (err) {
179
185
  fnError = err?.message || String(err);
180
186
  console.log(`[keveGoal] "${options.step}" fn error: ${fnError}`);
187
+ // ── Detect environment-blocked errors: skip Agent Re-Act, directly BLOCKED ──
188
+ // When fn fails due to empty URL, connection refused, timeout, or navigation to invalid URL,
189
+ // Agent exploration cannot fix environment issues — waste of time and tokens.
190
+ fnBlocked = isEnvironmentBlockedError(err);
191
+ if (fnBlocked) {
192
+ console.log(`[keveGoal] 🚫 "${options.step}" BLOCKED — environment error detected, skipping agent Re-Act`);
193
+ }
181
194
  }
182
195
  }
183
196
  // ── Capture fn-after screenshot (fn 执行后的页面状态,传给 agent 说明执行前后) ──
@@ -188,46 +201,76 @@ exports.test = test_1.test.extend({
188
201
  }
189
202
  catch { /* non-critical */ }
190
203
  }
191
- // ── Hand off to agent: agent does Re-Act ──
204
+ // ── Hand off to agent: agent does Re-Act (OR shortcut to blocked if fnBlocked) ──
192
205
  const learnedHint = learned_actions_1.learnedActions.getHint(options.step);
193
206
  let reactResult;
194
207
  let reactTimedOut = false;
195
- // ── Per-goal timeout: prevent one slow goal from exhausting the test timeout ──
196
- // Default 300s per goal; total test timeout should be ≥ (goals × 300s + overhead)
197
- const GOAL_TIMEOUT_MS = 300_000;
198
- const goalTimeoutController = new AbortController();
199
- const goalTimeout = setTimeout(() => {
200
- goalTimeoutController.abort();
201
- console.log(`[keveGoal] ⏰ "${options.step}" timed out after ${GOAL_TIMEOUT_MS}ms — stopping agent`);
202
- }, GOAL_TIMEOUT_MS);
203
- try {
204
- reactResult = await (0, agent_1.reactLoop)(page, options.step, options.expected, {
205
- learnedActionsHint: learnedHint,
206
- specFilePath: ctx.specFilePath,
207
- fnSource,
208
- fnResult: fnError ? { error: fnError } : { success: true },
209
- signal: goalTimeoutController.signal,
210
- goalScreenshotBefore: goalScreenshotBefore || undefined,
211
- fnAfterScreenshot: fnAfterScreenshot || undefined,
212
- });
213
- }
214
- catch (reactErr) {
215
- reactTimedOut = true;
216
- const isGoalTimeout = goalTimeoutController.signal.aborted;
217
- const msg = isGoalTimeout
218
- ? `Goal timed out (${GOAL_TIMEOUT_MS}ms)`
219
- : (reactErr?.message || String(reactErr));
220
- console.log(`[keveGoal] reactLoop interrupted: ${msg.slice(0, 200)}`);
208
+ if (fnBlocked) {
209
+ // ── Shortcut: environment error detected in fn, skip Agent Re-Act entirely ──
210
+ // Agent exploration cannot fix empty URLs, connection refused, or navigation failures.
211
+ // Directly produce a blocked result with the fn error as explanation.
221
212
  reactResult = {
222
213
  expectedMet: false,
223
- actions: [],
214
+ actions: [{
215
+ action: { tool: 'done', verdict: 'blocked', text: `Environment blocked: ${fnError}` },
216
+ toolOutput: '',
217
+ result: 'ok',
218
+ }],
224
219
  finalSnapshot: '',
225
220
  agentScreenshots: [],
226
- conclusion: isGoalTimeout ? 'blocked' : 'fail',
221
+ conclusion: 'blocked',
227
222
  };
228
223
  }
229
- finally {
230
- clearTimeout(goalTimeout);
224
+ else {
225
+ // ── Per-goal timeout: prevent one slow goal from exhausting the test timeout ──
226
+ // Default 300s per goal; total test timeout should be ≥ (goals × 300s + overhead)
227
+ const GOAL_TIMEOUT_MS = 300_000;
228
+ const goalTimeoutController = new AbortController();
229
+ const goalTimeout = setTimeout(() => {
230
+ goalTimeoutController.abort();
231
+ console.log(`[keveGoal] ⏰ "${options.step}" timed out after ${GOAL_TIMEOUT_MS}ms — stopping agent`);
232
+ }, GOAL_TIMEOUT_MS);
233
+ try {
234
+ reactResult = await (0, agent_1.reactLoop)(page, options.step, options.expected, {
235
+ learnedActionsHint: learnedHint,
236
+ specFilePath: ctx.specFilePath,
237
+ fnSource,
238
+ fnResult: fnError ? { error: fnError } : { success: true },
239
+ signal: goalTimeoutController.signal,
240
+ goalScreenshotBefore: goalScreenshotBefore || undefined,
241
+ fnAfterScreenshot: fnAfterScreenshot || undefined,
242
+ });
243
+ }
244
+ catch (reactErr) {
245
+ reactTimedOut = true;
246
+ const isGoalTimeout = goalTimeoutController.signal.aborted;
247
+ const msg = isGoalTimeout
248
+ ? `Goal timed out (${GOAL_TIMEOUT_MS}ms)`
249
+ : (reactErr?.message || String(reactErr));
250
+ console.log(`[keveGoal] reactLoop interrupted: ${msg.slice(0, 200)}`);
251
+ reactResult = {
252
+ expectedMet: false,
253
+ actions: [],
254
+ finalSnapshot: '',
255
+ agentScreenshots: [],
256
+ conclusion: isGoalTimeout ? 'blocked' : 'fail',
257
+ };
258
+ }
259
+ finally {
260
+ clearTimeout(goalTimeout);
261
+ }
262
+ } // end of else (non-blocked path)
263
+ // ── Deterministic override: fn assertion failure cannot be overridden by Agent "pass" ──
264
+ // When fn has a hard assertion failure (Expected X, Received Y), the value mismatch
265
+ // is a fact, not a judgment. The Agent may rationalize "consistency" (e.g. "both pages
266
+ // show 15px, so consistent") and override to pass, but 15px ≠ 16px is a definitive failure.
267
+ // This is the per-goal equivalent of pipeline afterHook (case-gen-validate) —
268
+ // deterministic check > LLM judgment.
269
+ if (fnError && !fnBlocked && reactResult.expectedMet && isAssertionFailure(fnError)) {
270
+ console.log(`[keveGoal] ⚠️ "${options.step}" Agent said PASS but fn had assertion failure — overriding to FAIL`);
271
+ console.log(`[keveGoal] fn assertion: ${fnError}`);
272
+ reactResult.expectedMet = false;
273
+ reactResult.conclusion = 'fail';
231
274
  }
232
275
  result = {
233
276
  success: reactResult.expectedMet,
@@ -236,8 +279,10 @@ exports.test = test_1.test.extend({
236
279
  error: reactResult.expectedMet
237
280
  ? undefined
238
281
  : reactResult.conclusion === 'blocked'
239
- ? new Error(`Blocked: ${reactTimedOut ? `Agent 超时 (${GOAL_TIMEOUT_MS}ms)` : (reactResult.actions?.filter((a) => a.action?.tool === 'done').pop()?.action?.text || 'Agent blocked')}`)
240
- : new Error(`Expected not achieved: ${options.expected}`),
282
+ ? new Error(`Blocked: ${fnBlocked ? fnError : (reactTimedOut ? `Agent 超时 (300000ms)` : (reactResult.actions?.filter((a) => a.action?.tool === 'done').pop()?.action?.text || 'Agent blocked'))}`)
283
+ : (fnError && isAssertionFailure(fnError) && !fnBlocked)
284
+ ? new Error(`Assertion failed: ${fnError}`)
285
+ : new Error(`Expected not achieved: ${options.expected}`),
241
286
  };
242
287
  // Attach Agent conclusion for downstream consumers
243
288
  if (reactResult.conclusion)
@@ -363,6 +408,109 @@ exports.test = test_1.test.extend({
363
408
  const keve_decorators_2 = require("./keve-decorators");
364
409
  (0, keve_decorators_2.setKeveTest)(exports.test);
365
410
  // ── 辅助函数 ──
411
+ /**
412
+ * Detect environment-blocked errors from fn execution.
413
+ *
414
+ * When fn (deterministic script) fails with these patterns, Agent Re-Act cannot
415
+ * fix the issue — the error is environmental, not application-level:
416
+ *
417
+ * 1. Empty/undefined URL → page.goto(''), page.goto(undefined)
418
+ * 2. Connection refused / net::ERR_CONNECTION_REFUSED
419
+ * 3. Navigation timeout (page not reachable)
420
+ * 4. Invalid URL format
421
+ * 5. SSO redirect / auth failure (not the app's fault)
422
+ *
423
+ * Returns true → keveGoal skips Agent Re-Act and directly reports BLOCKED
424
+ * with the fn error as the blocking reason.
425
+ */
426
+ function isEnvironmentBlockedError(err) {
427
+ const msg = (err?.message || String(err)).toLowerCase();
428
+ // Empty/undefined URL: page.goto('') or page.goto(undefined)
429
+ if (msg.includes('url must not be empty') || msg.includes('url is empty')
430
+ || msg.includes('invalid url') || msg.includes('url is undefined')
431
+ || msg.includes('navigation to ""') || msg.includes("navigation to ''")) {
432
+ return true;
433
+ }
434
+ // page.goto(undefined): Playwright type error "url: expected string, got undefined"
435
+ // This happens when process.env.PAGE_* is not set — environment config issue, not app issue
436
+ if ((msg.includes('expected string') || msg.includes('expected a string'))
437
+ && (msg.includes('got undefined') || msg.includes('got null') || msg.includes('received undefined'))) {
438
+ return true;
439
+ }
440
+ // Connection refused / network unreachable
441
+ if (msg.includes('err_connection_refused') || msg.includes('connection refused')
442
+ || msg.includes('net::err_connection') || msg.includes('err_name_not_resolved')
443
+ || msg.includes('err_address_unreachable') || msg.includes('err_internet_disconnected')
444
+ || msg.includes('err_connection_timed_out') || msg.includes('err_connection_reset')) {
445
+ return true;
446
+ }
447
+ // Navigation timeout (page not reachable in time)
448
+ if (msg.includes('navigation timeout of') || msg.includes('timeout of') && msg.includes('exceeded')
449
+ || msg.includes('page.goto: timeout') || msg.includes('navigating to') && msg.includes('timed out')) {
450
+ return true;
451
+ }
452
+ // Playwright locator/waitForSelector timeout — selector not found on page (fn script issue, not app crash)
453
+ // e.g. "Timeout 15000ms exceeded while waiting for locator('.selector') to be visible"
454
+ // This is a script-quality issue (wrong selector), not an env blocked error.
455
+ // Return false here — let Agent take over rather than treating as BLOCKED.
456
+ // Note: we intentionally do NOT return true; these should fall through to Agent Re-Act.
457
+ // IDC network segment restriction
458
+ if (msg.includes('unable to access on the idc network segment')
459
+ || msg.includes('idc network segment') || msg.includes('40314')) {
460
+ return true;
461
+ }
462
+ // SSO / auth redirect (not the app's fault)
463
+ if (msg.includes('sso redirect') || msg.includes('login redirect detected')
464
+ || msg.includes('err_too_many_redirects')) {
465
+ return true;
466
+ }
467
+ return false;
468
+ }
469
+ /**
470
+ * Detect assertion failures from fn execution (Expected/Received patterns).
471
+ *
472
+ * When fn (deterministic script) throws a hard assertion failure with concrete
473
+ * Expected vs Received values, the mismatch is a *fact*, not a judgment.
474
+ * The Agent may rationalize "consistency" (e.g. "both pages show 15px → consistent → pass")
475
+ * and override to pass, but 15px ≠ 16px is a definitive failure.
476
+ *
477
+ * This function identifies such assertion failures so keveGoal can:
478
+ * - Override Agent "pass" → "fail" when the fn proved a value mismatch
479
+ * - Use the precise assertion message in the error (instead of generic "expected not achieved")
480
+ *
481
+ * Returns true for assertion errors, false for environment/runtime errors.
482
+ */
483
+ function isAssertionFailure(error) {
484
+ const msg = (error || '').toLowerCase();
485
+ // Exclude Playwright API type errors (parameter type mismatch, not value assertion)
486
+ // e.g. "page.goto: url: expected string, got undefined" — this is an env config error,
487
+ // not a functional assertion failure. isEnvironmentBlockedError() handles it instead.
488
+ if (msg.includes('expected string') || msg.includes('expected a string')
489
+ || msg.includes('expected number') || msg.includes('expected boolean')) {
490
+ // Only exclude if it looks like an API parameter error (not an assertion)
491
+ if (msg.includes('got undefined') || msg.includes('got null')
492
+ || msg.includes('received undefined') || msg.includes('received null')) {
493
+ return false;
494
+ }
495
+ }
496
+ // Jest/Vitest/playwright assertion: "Expected: X, Received: Y"
497
+ if (msg.includes('expected') && msg.includes('received'))
498
+ return true;
499
+ // AssertionError / assert keyword
500
+ if (msg.includes('assertionerror') || msg.includes('assertion failed')
501
+ || msg.includes('assertion error'))
502
+ return true;
503
+ // expect(x).toBe(y) / expect(x).toEqual(y) pattern
504
+ // Note: must NOT match Playwright timeout messages like "Timeout 15000ms exceeded waiting for locator(...) to be visible"
505
+ // Those are env/timeout errors, not value assertion failures.
506
+ if ((msg.includes(' to be ') && !msg.includes('timeout') && !msg.includes('waiting for') && !msg.includes('exceeded'))
507
+ || msg.includes(' to equal ') || msg.includes(' to deeply equal '))
508
+ return true;
509
+ // Playwright expect: "expected string, received number" etc.
510
+ if (msg.includes('expected ') && (msg.includes(' but ') || msg.includes(' got ') || msg.includes(' received ')))
511
+ return true;
512
+ return false;
513
+ }
366
514
  /**
367
515
  * 截图并保存到 test-artifacts,返回相对 taskDir 的路径
368
516
  * @param phase 'before' | 'after-fn' — 用于文件名前缀
@@ -204,6 +204,18 @@ class KevePageAgent {
204
204
  if (execResult.error) {
205
205
  this.pushObservation(`Action "${actionName}" failed: ${execResult.error}. Try a different approach.`);
206
206
  }
207
+ // If execute_javascript returned a business-level rejection (❌ prefix),
208
+ // re-inject the core rules so the LLM corrects its next attempt.
209
+ if (actionName === 'execute_javascript' && execResult.output && execResult.output.startsWith('❌')) {
210
+ const jsRulesHint = [
211
+ 'execute_javascript rules — fix your script:',
212
+ '1. MUST start with `return` to capture the result.',
213
+ '2. Use `var` for declarations — `const`/`let` cause SyntaxError.',
214
+ '3. NO top-level `await` or `async () => {}` wrappers — they return undefined.',
215
+ '4. NO Playwright APIs (`page`, `locator`) — only browser DOM APIs.',
216
+ ].join('\n');
217
+ this.pushObservation(jsRulesHint);
218
+ }
207
219
  // ── Check: if "done", capture done-time screenshot + return with test conclusion ──
208
220
  if (actionName === 'done') {
209
221
  // Capture done-time screenshot as goal-after evidence
@@ -108,6 +108,7 @@ Common pitfalls the screenshot helps catch:
108
108
 
109
109
  <browser_rules>
110
110
  Strictly follow these rules while using the browser:
111
+ - **You are an observer, not an editor**: Your role is to VERIFY the page state, not to CHANGE it. If the actual state differs from expected, report the mismatch — do NOT modify the page to make it match.
111
112
  - Use `ref` to target elements. Find the element by its role and name in the tree, then use its ref value.
112
113
  - If the page changes after an action, re-read the browser state before acting again.
113
114
  - If expected elements are missing from the visible area, use scroll to find them.
@@ -139,12 +140,30 @@ When the screenshot or accessibility tree shows a permission-denied / no-access
139
140
  Important:
140
141
  - Set `verdict` to `"pass"` ONLY if the expected state is fully achieved. Be honest and rigorous.
141
142
  - Set `verdict` to `"fail"` when the page works but does not match the expected state. Describe the mismatch in `text`.
142
- - Set `verdict` to `"blocked"` when the environment prevents testing OR when the page shows a permission-denied / no-access state (test account lacks required access). Not the application's fault.
143
+ - `verdict` to `"blocked"` when the environment prevents testing OR when the page shows a permission-denied / no-access state (test account lacks required access). Not the application's fault.
144
+ - **Environment-blocked → blocked (critical rule):**
145
+ When the `navigate` tool returns an error indicating a missing/empty environment variable (e.g. "❌ Cannot navigate: environment variable PAGE_BI is empty/undefined"), this means the test data is incomplete — the URL for the target page was never provided. Agent exploration CANNOT fix this.
146
+ - Call `done(verdict="blocked")` immediately with the missing variable name in `text`.
147
+ - Do NOT try to navigate to the main page or any alternative URL as a workaround — the test requires the specific page defined by the missing env var.
148
+ - Do NOT continue exploring — the missing URL is a configuration issue, not an application defect.
143
149
  - Do NOT call done with verdict="pass" if the screenshot shows the expected state is NOT achieved.
144
150
  - **When to stop immediately**: If `execute_javascript` or another deterministic tool returns a result that definitively proves the expected state CANNOT be achieved (e.g. computed style value mismatch), call `done` with the appropriate verdict right away. Do NOT continue exploring, scrolling, or navigating — more actions will not change a CSS computed value or a DOM property. Wasting steps on already-failed assertions reduces test reliability.
145
151
  - **When prior_execution shows the conclusion**: If the `<prior_execution>` block shows an assertion failure with `Expected:` and `Received:` values, the deterministic script has already captured the precise value. You may use `execute_javascript` once to confirm the current value, then immediately call `done(verdict="fail")` with the actual vs expected values in `text`. Do NOT explore further — the assertion already proved the mismatch.
146
152
  - **execute_javascript is ONLY for reading precise values**: Use it to query computed styles, DOM properties, or measurements that screenshots/accessibility tree cannot provide. Do NOT use it for clicking, typing, navigating, form filling, or any page interaction — use the dedicated tools (click, type, navigate) instead. Do NOT use it to repeat what the accessibility tree already shows. Do NOT use it for logic/assertions — just return the raw value and judge the result yourself.
147
- - **execute_javascript runs in BROWSER DOM context — no Playwright APIs**: The script executes inside the browser page via `page.evaluate`. There is NO `page` object, NO `locator`, NO top-level `await`. Use ONLY browser DOM APIs: `document.querySelector`, `window.getComputedStyle`, etc. Use `var` for variable declarations — `const`/`let` cause SyntaxError. Do NOT wrap in `async () => {}` — it returns undefined.
153
+ - **NEVER modify the page under test (CRITICAL RULE)**: You are a TESTER — your job is to OBSERVE and REPORT the actual state of the page, never to ALTER it to match expectations. If the actual value differs from the expected value, that is a TEST FAILURE — report it honestly as `done(verdict="fail")`. Do NOT use `execute_javascript`, `click`, `type`, or any other tool to "fix" the page so the test passes. Specifically:
154
+ - Do NOT inject CSS (style.setProperty, style overrides, CSS variable changes)
155
+ - Do NOT modify DOM structure (adding/removing elements)
156
+ - Do NOT change element attributes or classes
157
+ - Do NOT override computed styles to match expected values
158
+ - If `execute_javascript` returns a value that differs from expected, call `done(verdict="fail")` with the actual vs expected — do NOT attempt to change the value
159
+ - **execute_javascript <rule>**:
160
+ 1. Script MUST start with `return` — no auto-wrapping. Omitting `return` causes execution rejection.
161
+ 2. Use `var` for variable declarations. `const`/`let` cause SyntaxError in page.evaluate context.
162
+ 3. NO top-level `await` — the script runs in a synchronous IIFE. There is no async context.
163
+ 4. NO `async () => {}` wrappers — they return undefined. Write flat statements ending with `return`.
164
+ 5. NO Playwright APIs (`page`, `locator`, `expect`) — only browser DOM APIs exist inside the page.
165
+ 6. Read-only: do NOT modify DOM, styles, cookies, or localStorage. Only query/return values. This includes style.setProperty() — it is BLOCKED. If the actual value differs from expected, report it as fail, do NOT override it.
166
+ If your script returns `❌`, the rules will be re-injected in the next step — fix the script immediately instead of trying other approaches.
148
167
 
149
168
  Example — correct use of execute_javascript + immediate done:
150
169
  ```
@@ -131,8 +131,20 @@ exports.tools.set('navigate', tool({
131
131
  }),
132
132
  execute: async function (input) {
133
133
  let url = input.url;
134
+ // ── Resolve environment variable references (e.g., "process.env.PAGE_BI") ──
135
+ const envRefMatch = url.match(/^process\.env\.(\w+)$/);
136
+ if (envRefMatch) {
137
+ const envValue = process.env[envRefMatch[1]] || '';
138
+ if (!envValue) {
139
+ return `❌ Cannot navigate: environment variable ${envRefMatch[1]} is empty/undefined. This is a test data configuration issue — call done(verdict="blocked") immediately.`;
140
+ }
141
+ url = envValue;
142
+ }
134
143
  if (!url.startsWith('http://') && !url.startsWith('https://')) {
135
144
  const base = process.env.KEVE_TARGET_URL || process.env.BASE_URL || '';
145
+ if (!base) {
146
+ return `❌ Cannot navigate: no base URL available (KEVE_TARGET_URL and BASE_URL are both empty). Call done(verdict="blocked") immediately.`;
147
+ }
136
148
  url = new URL(url, base).href;
137
149
  }
138
150
  await this.page.goto(url, { waitUntil: 'networkidle', timeout: 15000 });
@@ -141,9 +153,9 @@ exports.tools.set('navigate', tool({
141
153
  }));
142
154
  // --- execute_javascript ---
143
155
  exports.tools.set('execute_javascript', tool({
144
- description: 'READ-ONLY query tool: execute JavaScript in the BROWSER DOM context to retrieve precise values that screenshots and accessibility trees CANNOT provide — such as computed CSS styles (getComputedStyle), DOM measurements (offsetWidth, getBoundingClientRect), or element properties (checked, disabled, value).\n\n⚠️ EXECUTION CONTEXT: This runs inside the browser page via page.evaluate — it is a plain browser JavaScript sandbox. There is NO `page` object, NO Playwright API, NO `locator`, NO top-level `await`. Using `page.locator(...)`, `page.evaluate(...)`, `await`, or `async () => {}` wrappers will cause SyntaxError or return undefined.\n\nDo NOT use for: clicking, typing, navigating, form filling, or any page interaction — use click/type/navigate tools instead. Do NOT use for: assertions or logical checks — just return the raw value, you judge the result yourself. Do NOT use for: modifying page state (setting styles, cookies, localStorage, DOM mutations).',
156
+ description: 'READ-ONLY query tool: execute JavaScript in the BROWSER DOM context to retrieve precise values that screenshots and accessibility trees CANNOT provide — such as computed CSS styles (getComputedStyle), DOM measurements (offsetWidth, getBoundingClientRect), or element properties (checked, disabled, value).\n\nThis tool is STRICTLY READ-ONLY. You MUST NOT modify the page under test — you are a TESTER who observes and reports, never an editor who alters. If the actual value differs from expected, report it as fail; do NOT override styles/DOM to make it match.\n\n<rule>\n1. Script MUST start with `return` to capture the result. No auto-wrapping — if you omit `return`, execution is rejected.\n2. Use `var` for variable declarations. `const`/`let` cause SyntaxError in page.evaluate context.\n3. NO top-level `await` — the script runs in a synchronous IIFE inside page.evaluate. There is no async context.\n4. NO `async () => {}` wrappers — they return undefined. Write flat statements ending with `return`.\n5. NO Playwright APIs (`page`, `locator`, `expect`) — only browser DOM APIs exist inside the page.\n6. STRICTLY READ-ONLY: do NOT modify DOM, styles, cookies, or localStorage. This includes style.setProperty(), style.removeProperty(), className changes, setAttribute, and any DOM mutation. All write APIs are BLOCKED at execution level. Only query/return values.\n</rule>\n\nDo NOT use for: clicking, typing, navigating, form filling, or any page interaction — use click/type/navigate tools instead. Do NOT use for: assertions or logical checks — just return the raw value, you judge the result yourself.',
145
157
  inputSchema: zod_1.z.object({
146
- script: zod_1.z.string().describe("JavaScript code to execute inside the browser DOM. MUST start with `return` to capture the result. Use `var` for variable declarations (NOT `const`/`let` — they cause SyntaxError in some evaluate contexts). Only browser DOM APIs: document.querySelector, window.getComputedStyle, etc.\n\n✅ Correct: 'return document.querySelector(\".x\").innerText'\n✅ Correct: 'var el = document.querySelector(\".x\"); return window.getComputedStyle(el).backgroundColor'\n❌ Wrong: 'page.locator(\".x\")' — no Playwright `page` object in browser context\n❌ Wrong: 'await page.locator(...)' — no `await` or `async`\n❌ Wrong: 'const el = ...' — use `var` instead of `const`/`let`\n❌ Wrong: 'async () => { ... }' — do NOT wrap in async function, returns undefined\n\nRead-only queries only."),
158
+ script: zod_1.z.string().describe("JavaScript to execute in browser DOM. <rule> MUST start with `return`. Use `var` (not `const`/`let`). No top-level `await` or `async` wrappers. No Playwright APIs. STRICTLY READ-ONLY — no style.setProperty, no DOM mutation. </rule>\n\n✅ 'return document.querySelector(\".x\").innerText'\n✅ 'var el = document.querySelector(\".x\"); return window.getComputedStyle(el).backgroundColor'\n❌ 'page.locator(\".x\")' — no page object\n❌ 'await page.locator(...)' — no await\n❌ 'const el = ...' — use var\n❌ 'async () => { ... }' — returns undefined\n❌ 'document.documentElement.style.setProperty(...)' — WRITE, use getComputedStyle instead"),
147
159
  }),
148
160
  execute: async function (input) {
149
161
  try {
@@ -164,11 +176,15 @@ exports.tools.set('execute_javascript', tool({
164
176
  script = iifeMatch[1].trim();
165
177
  }
166
178
  // Read-only guard: block DOM mutation APIs to prevent Agent from altering page state.
179
+ // CRITICAL: E2E testing must OBSERVE and REPORT the actual page state.
180
+ // Never modify the page under test to make assertions pass.
167
181
  const writePatterns = [
168
182
  /\b(?:document|element|el|node)\.\s*(?:remove|appendChild|insertBefore|replaceChild|removeChild|appendChild)\s*\(/,
169
183
  /\b(?:document|element|el|node)\.\s*(?:innerHTML|outerHTML|textContent|innerText)\s*=/,
170
184
  /\b(?:document|element|el|node)\.\s*(?:setAttribute|removeAttribute|classList)\s*\./,
171
185
  /\b\.style\s*\.\s*\w+\s*=/,
186
+ /\b\.style\s*\.\s*(?:setProperty|removeProperty)\s*\(/,
187
+ /\b(?:document|element|el|node)\.\s*className\s*=/,
172
188
  /\blocation\s*(?:\.href\s*=|\.assign\s*\(|\.replace\s*\()/,
173
189
  /\bwindow\s*\.\s*(?:close|stop|open)\s*\(/,
174
190
  /\bdocument\.\s*(?:write|writeln|execCommand)\s*\(/,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kkeve/suite",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "AI-powered full-stack test agent CLI - static analysis, unit test, component test, E2E test, execute, evaluate, and report",
5
5
  "license": "MIT",
6
6
  "bin": {