@kkeve/suite 0.0.11 → 0.0.13

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>;
@@ -87,14 +87,20 @@ keve_aspect_1.keveAspect.register({
87
87
  phase: 'after',
88
88
  order: 20,
89
89
  async execute(ctx, result) {
90
- if (!result?.actions?.length || !result.success)
90
+ if (!result?.actions?.length)
91
91
  return;
92
- learned_actions_1.learnedActions.add(ctx.step, result.actions);
92
+ const url = ctx.page.url();
93
+ learned_actions_1.learnedActions.add(ctx.step, result.actions, url, result.success);
93
94
  },
94
95
  });
95
96
  // ─── (diagnostic extraction moved to page-agent/diagnostic.ts, called via onAfterTask hook) ──
96
97
  // ─── Fixture ────────────────────────────────────────────────────────
97
98
  exports.test = test_1.test.extend({
99
+ // ── 将内置 testInfo 包装为可解构 fixture ─────────────────────────
100
+ // 测试方法可直接写 { page, keveGoal, testInfo },不再报 "unknown parameter" 错误
101
+ testInfo: async ({}, use, testInfo) => {
102
+ await use(testInfo);
103
+ },
98
104
  browser: async ({}, use, testInfo) => {
99
105
  if ((0, global_setup_1.isCdpMode)()) {
100
106
  const browser = await (0, global_setup_1.getCdpBrowser)(); // 复用全局共享的 CDP browser
@@ -170,6 +176,7 @@ exports.test = test_1.test.extend({
170
176
  let result;
171
177
  const fnSource = fn?.toString() || undefined;
172
178
  let fnError;
179
+ let fnBlocked = false; // true when fn error is environment-blocked (skip agent Re-Act)
173
180
  if (fn) {
174
181
  try {
175
182
  await fn();
@@ -178,6 +185,13 @@ exports.test = test_1.test.extend({
178
185
  catch (err) {
179
186
  fnError = err?.message || String(err);
180
187
  console.log(`[keveGoal] "${options.step}" fn error: ${fnError}`);
188
+ // ── Detect environment-blocked errors: skip Agent Re-Act, directly BLOCKED ──
189
+ // When fn fails due to empty URL, connection refused, timeout, or navigation to invalid URL,
190
+ // Agent exploration cannot fix environment issues — waste of time and tokens.
191
+ fnBlocked = isEnvironmentBlockedError(err);
192
+ if (fnBlocked) {
193
+ console.log(`[keveGoal] 🚫 "${options.step}" BLOCKED — environment error detected, skipping agent Re-Act`);
194
+ }
181
195
  }
182
196
  }
183
197
  // ── Capture fn-after screenshot (fn 执行后的页面状态,传给 agent 说明执行前后) ──
@@ -188,46 +202,77 @@ exports.test = test_1.test.extend({
188
202
  }
189
203
  catch { /* non-critical */ }
190
204
  }
191
- // ── Hand off to agent: agent does Re-Act ──
192
- const learnedHint = learned_actions_1.learnedActions.getHint(options.step);
205
+ // ── Hand off to agent: agent does Re-Act (OR shortcut to blocked if fnBlocked) ──
206
+ const currentUrl = page.url();
207
+ const learnedHint = learned_actions_1.learnedActions.getHint(options.step, currentUrl);
193
208
  let reactResult;
194
209
  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)}`);
210
+ if (fnBlocked) {
211
+ // ── Shortcut: environment error detected in fn, skip Agent Re-Act entirely ──
212
+ // Agent exploration cannot fix empty URLs, connection refused, or navigation failures.
213
+ // Directly produce a blocked result with the fn error as explanation.
221
214
  reactResult = {
222
215
  expectedMet: false,
223
- actions: [],
216
+ actions: [{
217
+ action: { tool: 'done', verdict: 'blocked', text: `Environment blocked: ${fnError}` },
218
+ toolOutput: '',
219
+ result: 'ok',
220
+ }],
224
221
  finalSnapshot: '',
225
222
  agentScreenshots: [],
226
- conclusion: isGoalTimeout ? 'blocked' : 'fail',
223
+ conclusion: 'blocked',
227
224
  };
228
225
  }
229
- finally {
230
- clearTimeout(goalTimeout);
226
+ 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);
235
+ 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
+ });
245
+ }
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)}`);
253
+ reactResult = {
254
+ expectedMet: false,
255
+ actions: [],
256
+ finalSnapshot: '',
257
+ agentScreenshots: [],
258
+ conclusion: isGoalTimeout ? 'blocked' : 'fail',
259
+ };
260
+ }
261
+ finally {
262
+ clearTimeout(goalTimeout);
263
+ }
264
+ } // end of else (non-blocked path)
265
+ // ── Deterministic override: fn assertion failure cannot be overridden by Agent "pass" ──
266
+ // When fn has a hard assertion failure (Expected X, Received Y), the value mismatch
267
+ // is a fact, not a judgment. The Agent may rationalize "consistency" (e.g. "both pages
268
+ // show 15px, so consistent") and override to pass, but 15px ≠ 16px is a definitive failure.
269
+ // This is the per-goal equivalent of pipeline afterHook (case-gen-validate) —
270
+ // deterministic check > LLM judgment.
271
+ if (fnError && !fnBlocked && reactResult.expectedMet && isAssertionFailure(fnError)) {
272
+ console.log(`[keveGoal] ⚠️ "${options.step}" Agent said PASS but fn had assertion failure — overriding to FAIL`);
273
+ console.log(`[keveGoal] fn assertion: ${fnError}`);
274
+ reactResult.expectedMet = false;
275
+ reactResult.conclusion = 'fail';
231
276
  }
232
277
  result = {
233
278
  success: reactResult.expectedMet,
@@ -236,8 +281,10 @@ exports.test = test_1.test.extend({
236
281
  error: reactResult.expectedMet
237
282
  ? undefined
238
283
  : 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}`),
284
+ ? 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}`),
241
288
  };
242
289
  // Attach Agent conclusion for downstream consumers
243
290
  if (reactResult.conclusion)
@@ -363,6 +410,109 @@ exports.test = test_1.test.extend({
363
410
  const keve_decorators_2 = require("./keve-decorators");
364
411
  (0, keve_decorators_2.setKeveTest)(exports.test);
365
412
  // ── 辅助函数 ──
413
+ /**
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.
427
+ */
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;
470
+ }
471
+ /**
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.
484
+ */
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
+ }
497
+ }
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
+ }
366
516
  /**
367
517
  * 截图并保存到 test-artifacts,返回相对 taskDir 的路径
368
518
  * @param phase 'before' | 'after-fn' — 用于文件名前缀
@@ -4,21 +4,50 @@
4
4
  * When one keveGoal's Re-Act loop discovers a working action sequence,
5
5
  * other goals in the same round can use those discoveries as hints.
6
6
  *
7
- * Key format: "${step}" — the step description
8
- * Value: ActionLogEntry[] — the successful action sequence
7
+ * v2: Fuzzy matching by token Jaccard similarity + page URL pattern.
8
+ * Exact match is tried first; if no hit, fuzzy search across all entries.
9
9
  */
10
+ interface CacheEntry {
11
+ step: string;
12
+ actions: any[];
13
+ url?: string;
14
+ success: boolean;
15
+ }
10
16
  export declare class LearnedActions {
11
17
  private cache;
12
- /** Store discovered actions for a step */
13
- add(step: string, actions: any[]): void;
14
- /** Retrieve discovered actions for a step */
18
+ /** Store discovered actions for a step (with optional page URL for context matching)
19
+ * @param success Whether the step succeeded — failed entries are stored with warnings only
20
+ */
21
+ add(step: string, actions: any[], url?: string, success?: boolean): void;
22
+ /** Retrieve discovered actions for a step (exact match) */
15
23
  get(step: string): any[] | undefined;
16
- /** Get a JSON hint string for injecting into Re-Act prompt */
17
- getHint(step: string): string | undefined;
24
+ /** Tokenize a Chinese/mixed step description for fuzzy matching.
25
+ * Uses bigram (character pair) extraction for Chinese — more granular than
26
+ * whitespace splitting, captures shared sub-phrases like "指标标准名称".
27
+ */
28
+ private tokenize;
29
+ /** Compute Jaccard similarity between two token sets */
30
+ private jaccard;
31
+ /** Find the best matching cached entry for a given step description */
32
+ private findBestMatch;
33
+ /** Get a hint string for injecting into Re-Act prompt.
34
+ * 1. Exact match → direct reuse
35
+ * 2. Fuzzy match by token similarity → similar step reuse
36
+ * 3. Same-page fallback → if same URL path, use the most recent successful entry
37
+ */
38
+ getHint(step: string, currentUrl?: string): string | undefined;
39
+ /** Extract path portion of URL for same-page matching (ignore query/hash) */
40
+ private extractPath;
41
+ /** Format a hint from a cache entry's actions.
42
+ * Success: action sequence summary + key discoveries
43
+ * Failure: warnings/blockers only — avoid misleading Agent into reusing failed paths
44
+ */
45
+ private formatHint;
18
46
  /** Get all cached entries */
19
- getAll(): Map<string, any[]>;
47
+ getAll(): Map<string, CacheEntry>;
20
48
  /** Clear all cached entries (typically at round start) */
21
49
  clear(): void;
22
50
  }
23
51
  /** Global singleton for same-round sharing */
24
52
  export declare const learnedActions: LearnedActions;
53
+ export {};