@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.
- package/dist/core/generateReport/reportData.d.ts +4 -2
- package/dist/core/generateReport/reportData.js +7 -11
- package/dist/core/keve-registry.d.ts +1 -1
- package/dist/core/keve-report.js +47 -28
- package/dist/core/keve-test.d.ts +2 -0
- package/dist/core/keve-test.js +186 -36
- package/dist/core/learned-actions.d.ts +37 -8
- package/dist/core/learned-actions.js +214 -22
- package/dist/page-agent/agent.d.ts +5 -0
- package/dist/page-agent/agent.js +202 -2
- package/dist/page-agent/system_prompt.md +85 -10
- package/dist/page-agent/tools.js +532 -15
- package/package.json +1 -1
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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' | '
|
|
15
|
+
export type ErrorCategory = 'script' | 'env' | 'assert' | 'visual' | 'text-mismatch' | 'incomplete' | 'pass' | 'unknown';
|
|
16
16
|
/** 场景执行后的评估元数据,由 @keveScene 装饰器写入,Reporter 读取 */
|
|
17
17
|
export interface KeveEvalMeta {
|
|
18
18
|
/** 错误分类 */
|
package/dist/core/keve-report.js
CHANGED
|
@@ -76,7 +76,7 @@ class KeveReporter {
|
|
|
76
76
|
visual: 80, // 视觉断言已评估,置信度较高
|
|
77
77
|
'text-mismatch': 70, // UI文案格式不匹配(功能逻辑正确,文案措辞变了)
|
|
78
78
|
assert: 50, // Playwright 断言失败,需人工确认
|
|
79
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
|
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 = '
|
|
151
|
-
skipAI =
|
|
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 = '
|
|
162
|
-
skipAI =
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/core/keve-test.d.ts
CHANGED
|
@@ -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>;
|
package/dist/core/keve-test.js
CHANGED
|
@@ -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
|
|
90
|
+
if (!result?.actions?.length)
|
|
91
91
|
return;
|
|
92
|
-
|
|
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
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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:
|
|
223
|
+
conclusion: 'blocked',
|
|
227
224
|
};
|
|
228
225
|
}
|
|
229
|
-
|
|
230
|
-
|
|
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 超时 (
|
|
240
|
-
:
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
/**
|
|
17
|
-
|
|
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,
|
|
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 {};
|