@kkeve/suite 0.0.24 → 0.0.25
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.
|
@@ -209,7 +209,7 @@ function createCdp(send) {
|
|
|
209
209
|
async function goto(url, timeout = 30000) {
|
|
210
210
|
const before = await currentUrl();
|
|
211
211
|
await evaluateInAut(`location.href = ${JSON.stringify(url)}`);
|
|
212
|
-
const
|
|
212
|
+
const waitComplete = async () => {
|
|
213
213
|
const snap = await snapshot();
|
|
214
214
|
if (snap.readyState !== 'complete')
|
|
215
215
|
return undefined;
|
|
@@ -217,8 +217,21 @@ function createCdp(send) {
|
|
|
217
217
|
return undefined;
|
|
218
218
|
// 同 URL 的显式 re-goto 场景:URL 不变时直接以 readyState 为准
|
|
219
219
|
return snap;
|
|
220
|
-
}
|
|
221
|
-
|
|
220
|
+
};
|
|
221
|
+
const landed = await pollUntil(waitComplete, timeout);
|
|
222
|
+
if (landed)
|
|
223
|
+
return;
|
|
224
|
+
// 页面存在长连接时 document.readyState 可能一直停在 interactive,
|
|
225
|
+
// 但新文档实际已经可交互且 URL 已切到目标。此时不能再判导航超时。
|
|
226
|
+
const interactive = await pollUntil(async () => {
|
|
227
|
+
const snap = await snapshot();
|
|
228
|
+
if (snap.readyState === 'loading' || snap.readyState === 'uninitialized')
|
|
229
|
+
return undefined;
|
|
230
|
+
if (snap.url !== url)
|
|
231
|
+
return undefined;
|
|
232
|
+
return snap;
|
|
233
|
+
}, 5000);
|
|
234
|
+
if (!interactive) {
|
|
222
235
|
throw new Error(`[cypress-engine] 导航超时(${timeout}ms):${url}`);
|
|
223
236
|
}
|
|
224
237
|
}
|
|
@@ -40,10 +40,108 @@ function logLine(line) {
|
|
|
40
40
|
console.log(line);
|
|
41
41
|
(0, state_js_1.addLog)(line);
|
|
42
42
|
}
|
|
43
|
+
function openGoalPanelLog(order, step) {
|
|
44
|
+
// keveGoal 同源实现也被非 Cypress 宿主引用,必须显式探测运行时能力。
|
|
45
|
+
const cypress = globalThis.Cypress;
|
|
46
|
+
if (typeof cypress?.log !== 'function')
|
|
47
|
+
return null;
|
|
48
|
+
try {
|
|
49
|
+
return cypress.log({
|
|
50
|
+
name: 'keveGoal',
|
|
51
|
+
// order 从 0 起算,展示与旧链路一致从「步骤1」开始。
|
|
52
|
+
displayName: `步骤${order + 1}`,
|
|
53
|
+
message: step,
|
|
54
|
+
}) || null;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.warn(`[keveGoal] 步骤面板日志创建失败(不影响执行):${err?.message || err}`);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function closeGoalPanelLog(log, success, errorMessage) {
|
|
62
|
+
if (!log)
|
|
63
|
+
return;
|
|
64
|
+
try {
|
|
65
|
+
// 失败态必须走 log.error():实测 set('state','failed') 会被 end() 重置回 passed。
|
|
66
|
+
// 成功态不手写 ✅/❌,交给 Cypress 原生状态染色。
|
|
67
|
+
if (!success)
|
|
68
|
+
log.error(new Error(errorMessage || 'keveGoal failed'));
|
|
69
|
+
log.end();
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.warn(`[keveGoal] 步骤面板日志收敛失败(不影响执行):${err?.message || err}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 面板单行承载有限:长错误折叠空白并截断,避免一次失败把后续步骤刷出视口。
|
|
77
|
+
*
|
|
78
|
+
* 折叠空白是必须的 —— 面板实测会把 message 里的 `\n` 折叠成空格,
|
|
79
|
+
* 这里显式做一遍,保证 stdout / 报告 / 面板三处文本一致。
|
|
80
|
+
*/
|
|
81
|
+
function clip(text, max = 220) {
|
|
82
|
+
const s = String(text || '').replace(/\s+/g, ' ').trim();
|
|
83
|
+
return s.length > max ? `${s.slice(0, max)}…` : s;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 把失败归因拆成「实际」与「判定」两段,供面板详情行与 errorMessage 复用。
|
|
87
|
+
*
|
|
88
|
+
* 面板实测限制(见方案文档第十节):
|
|
89
|
+
* - message 里的 `\n` 会被折叠成空格 → 多字段必须用显式分隔符连接;
|
|
90
|
+
* - `consoleProps` 不会展开、点击也不渲染 → 信息必须直接可见,不能藏起来。
|
|
91
|
+
* 因此这里只产出可直接铺在面板上的单段文本,不做多行排版。
|
|
92
|
+
*/
|
|
93
|
+
function describeGoalFailure(input) {
|
|
94
|
+
const actual = input.fnBlocked
|
|
95
|
+
? `环境阻断 — ${clip(input.fnError || '未知环境错误')}`
|
|
96
|
+
: input.shotDiff
|
|
97
|
+
? `截图差异超阈值 — ${clip(input.fnError || '未知差异')}`
|
|
98
|
+
: input.fnError
|
|
99
|
+
? `${input.assertion ? 'fn 断言未通过' : 'fn 执行报错'} — ${clip(input.fnError)}`
|
|
100
|
+
: 'Agent Re-Act 结束后仍未达到预期';
|
|
101
|
+
const judge = input.fnBlocked
|
|
102
|
+
? 'blocked · 环境错误,跳过 Agent Re-Act'
|
|
103
|
+
: input.overriddenByFn
|
|
104
|
+
? 'fail · fn 断言覆盖 Agent 的 PASS'
|
|
105
|
+
: input.fnError
|
|
106
|
+
? input.assertion
|
|
107
|
+
? 'fail · fn 断言未通过'
|
|
108
|
+
: 'fail · fn 报错后 Agent 仍未达成'
|
|
109
|
+
: 'fail · Agent Re-Act 未达成预期';
|
|
110
|
+
// 用「自述」而非「结论」:Agent 判 pass 却因 fn 断言被判 fail 时,
|
|
111
|
+
// 这句话与最终判定可能相反,如实呈现才能暴露冲突,而不是掩盖它。
|
|
112
|
+
const verdict = input.agentText
|
|
113
|
+
? `${judge}(Agent 自述:${clip(input.agentText, 120)})`
|
|
114
|
+
: judge;
|
|
115
|
+
return { actual, verdict };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 失败步骤在红色主行之后追加一条详情行。
|
|
119
|
+
*
|
|
120
|
+
* 独立成条而非塞进主行 message:实测详情条不继承红色,天然是「详情位」的
|
|
121
|
+
* 视觉层次,且主行保持「步骤N + 动作」的整洁形态,长文案不会污染步骤列表。
|
|
122
|
+
*/
|
|
123
|
+
function appendGoalFailurePanelLine(order, detail) {
|
|
124
|
+
const cypress = globalThis.Cypress;
|
|
125
|
+
if (typeof cypress?.log !== 'function')
|
|
126
|
+
return;
|
|
127
|
+
try {
|
|
128
|
+
cypress.log({
|
|
129
|
+
name: 'keveGoal',
|
|
130
|
+
displayName: `↳ 步骤${order + 1} 失败原因`,
|
|
131
|
+
// 不重复步骤文案:主行已显示「步骤N + 动作」,displayName 也带了步骤号,
|
|
132
|
+
// 详情行只承载「期望 / 实际 / 判定」三段,避免同样的信息占两倍行高。
|
|
133
|
+
message: detail,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
console.warn(`[keveGoal] 步骤面板失败详情写入失败(不影响执行):${err?.message || err}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
43
140
|
function createCyKeveGoal(engine) {
|
|
44
141
|
return async (options, fn) => {
|
|
45
142
|
const state = (0, state_js_1.requireTestState)();
|
|
46
143
|
const order = (0, state_js_1.nextGoalOrder)();
|
|
144
|
+
const panelLog = openGoalPanelLog(order, options.step);
|
|
47
145
|
const goalMeta = {
|
|
48
146
|
precondition: options.precondition,
|
|
49
147
|
step: options.step,
|
|
@@ -201,26 +299,50 @@ function createCyKeveGoal(engine) {
|
|
|
201
299
|
};
|
|
202
300
|
}
|
|
203
301
|
// ── 确定性覆盖:fn 断言失败不允许被 Agent 判成 pass ──
|
|
302
|
+
// 覆盖发生在此处,事后无法从 reactResult 反推(conclusion 已被改写),
|
|
303
|
+
// 因此就地留痕,供面板与 errorMessage 说明「为什么和 Agent 的结论相反」。
|
|
304
|
+
let overriddenByFn = false;
|
|
204
305
|
if (fnError && !fnBlocked && reactResult.expectedMet && (0, goal_heuristics_js_1.isAssertionFailure)(fnError)) {
|
|
205
306
|
console.log(`[keveGoal] ⚠️ "${options.step}" Agent 判 PASS 但 fn 断言失败 — 覆盖为 FAIL`);
|
|
206
307
|
reactResult.expectedMet = false;
|
|
207
308
|
reactResult.conclusion = 'fail';
|
|
309
|
+
overriddenByFn = true;
|
|
208
310
|
}
|
|
209
311
|
const conclusion = reactResult.expectedMet ? 'pass' : (reactResult.conclusion || 'fail');
|
|
210
312
|
const success = reactResult.expectedMet;
|
|
313
|
+
// 失败归因必须自带上下文:只报「哪一步 / 为什么」其中的一半,都要回查报告
|
|
314
|
+
// 才能定位。这里把第几步、步骤文案、fn 真实异常、Agent 判定理由一并带上。
|
|
315
|
+
const agentText = (reactResult.actions || [])
|
|
316
|
+
.filter((a) => a.action?.tool === 'done')
|
|
317
|
+
.pop()?.action?.text;
|
|
318
|
+
const failure = describeGoalFailure({
|
|
319
|
+
fnError,
|
|
320
|
+
fnBlocked,
|
|
321
|
+
shotDiff: fnErrorRaw?.name === 'ScreenshotDiffError',
|
|
322
|
+
assertion: Boolean(fnError && (0, goal_heuristics_js_1.isAssertionFailure)(fnError)),
|
|
323
|
+
overriddenByFn,
|
|
324
|
+
agentText,
|
|
325
|
+
});
|
|
326
|
+
const failureDetail = `期望:${clip(options.expected, 160)} | 实际:${failure.actual} | 判定:${failure.verdict}`;
|
|
211
327
|
const errorMessage = success
|
|
212
328
|
? undefined
|
|
213
329
|
: conclusion === 'blocked'
|
|
214
|
-
? `Blocked: ${fnBlocked ? fnError :
|
|
330
|
+
? `Blocked: [步骤${order + 1} ${options.step}] ${fnBlocked ? fnError : agentText || 'Agent blocked'}`
|
|
215
331
|
: fnErrorRaw?.name === 'ScreenshotDiffError'
|
|
216
|
-
? fnError
|
|
332
|
+
? `[步骤${order + 1} ${options.step}] ${fnError}`
|
|
217
333
|
: fnError && (0, goal_heuristics_js_1.isAssertionFailure)(fnError)
|
|
218
|
-
? `Assertion failed: ${fnError}`
|
|
219
|
-
: `Expected not achieved: ${options.expected}`;
|
|
334
|
+
? `Assertion failed: [步骤${order + 1} ${options.step}] ${fnError}`
|
|
335
|
+
: `Expected not achieved: [步骤${order + 1} ${options.step}] ${options.expected} — ${failure.actual}`;
|
|
220
336
|
// 与 Playwright keve-test.ts 的终局日志同形,保证两端执行轨迹可逐行对照
|
|
221
337
|
logLine(success
|
|
222
338
|
? `[keveGoal] ✅ "${options.step}" PASSED`
|
|
223
339
|
: `[keveGoal] ❌ "${options.step}" FAILED (${conclusion}) — ${errorMessage}`);
|
|
340
|
+
closeGoalPanelLog(panelLog, success, errorMessage);
|
|
341
|
+
if (!success) {
|
|
342
|
+
// 主行红条 + 独立详情行:面板上不点开任何东西就能回答
|
|
343
|
+
// 第几步 / 期望什么 / 实际发生什么 / 谁判的。
|
|
344
|
+
appendGoalFailurePanelLine(order, failureDetail);
|
|
345
|
+
}
|
|
224
346
|
// ── goal-after:优先取 agent done 时刻的截图,否则用 fn-after ──
|
|
225
347
|
let goalScreenshotAfter = '';
|
|
226
348
|
try {
|
package/package.json
CHANGED