@kkeve/suite 0.0.15 → 0.0.18
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/bin/keve.js +2 -1
- package/dist/commands/clean.js +2 -2
- package/dist/commands/config.js +7 -7
- package/dist/commands/init.js +2 -2
- package/dist/commands/report.js +7 -7
- package/dist/commands/run.js +12 -11
- package/dist/commands/test.js +7 -7
- package/dist/config.js +7 -7
- package/dist/core/action-log-writer.js +4 -1
- package/dist/core/decorator/keve-decorators.d.ts +128 -0
- package/dist/core/decorator/keve-decorators.js +254 -0
- package/dist/core/decorator/keve-host.d.ts +40 -0
- package/dist/core/decorator/keve-host.js +33 -0
- package/dist/core/{keve-registry.d.ts → decorator/keve-registry.d.ts} +19 -0
- package/dist/core/{keve-registry.js → decorator/keve-registry.js} +13 -1
- package/dist/core/generateReport/report.d.ts +1 -1
- package/dist/core/generateReport/report.js +2 -2
- package/dist/core/generateReport/reportData.d.ts +3 -0
- package/dist/core/generateReport/reportData.js +49 -15
- package/dist/core/judge/confidence.d.ts +119 -0
- package/dist/core/judge/confidence.js +295 -0
- package/dist/core/judge/goal-heuristics.d.ts +32 -0
- package/dist/core/judge/goal-heuristics.js +128 -0
- package/dist/core/judge/pngCodec.d.ts +23 -0
- package/dist/core/judge/pngCodec.js +215 -0
- package/dist/core/judge/screenshotDiff.d.ts +112 -0
- package/dist/core/judge/screenshotDiff.js +420 -0
- package/dist/core/page-object/definePage.d.ts +95 -0
- package/dist/core/page-object/definePage.js +62 -0
- package/dist/core/page-object/elementResolver.d.ts +32 -0
- package/dist/core/page-object/elementResolver.js +236 -0
- package/dist/core/page-object/engine-types.d.ts +140 -0
- package/dist/core/page-object/engine-types.js +34 -0
- package/dist/core/sso-cookie.d.ts +18 -0
- package/dist/core/sso-cookie.js +10 -0
- package/dist/dsl/index.d.ts +15 -0
- package/dist/dsl/index.js +29 -0
- package/dist/dsl/locatorSource.d.ts +25 -0
- package/dist/dsl/locatorSource.js +323 -0
- package/dist/dsl/ops.d.ts +46 -0
- package/dist/dsl/ops.js +95 -0
- package/dist/dsl/toDecorator.d.ts +98 -0
- package/dist/dsl/toDecorator.js +747 -0
- package/dist/engine-cypress/adapter.d.ts +108 -0
- package/dist/engine-cypress/adapter.js +514 -0
- package/dist/engine-cypress/agent-page.d.ts +159 -0
- package/dist/engine-cypress/agent-page.js +566 -0
- package/dist/engine-cypress/agentDom.d.ts +22 -0
- package/dist/engine-cypress/agentDom.js +590 -0
- package/dist/engine-cypress/bridge.d.ts +39 -0
- package/dist/engine-cypress/bridge.js +598 -0
- package/dist/engine-cypress/cdp.d.ts +78 -0
- package/dist/engine-cypress/cdp.js +339 -0
- package/dist/engine-cypress/host.d.ts +22 -0
- package/dist/engine-cypress/host.js +480 -0
- package/dist/engine-cypress/index.d.ts +26 -0
- package/dist/engine-cypress/index.js +58 -0
- package/dist/engine-cypress/keveGoal.d.ts +43 -0
- package/dist/engine-cypress/keveGoal.js +265 -0
- package/dist/engine-cypress/pageOps.d.ts +41 -0
- package/dist/engine-cypress/pageOps.js +439 -0
- package/dist/engine-cypress/reporter.d.ts +20 -0
- package/dist/engine-cypress/reporter.js +128 -0
- package/dist/engine-cypress/setup.d.ts +21 -0
- package/dist/engine-cypress/setup.js +27 -0
- package/dist/engine-cypress/shotBuffer.d.ts +40 -0
- package/dist/engine-cypress/shotBuffer.js +41 -0
- package/dist/engine-cypress/state.d.ts +56 -0
- package/dist/engine-cypress/state.js +81 -0
- package/dist/engine-playwright/engineAdapter.d.ts +78 -0
- package/dist/engine-playwright/engineAdapter.js +371 -0
- package/dist/engine-playwright/global-setup.d.ts +28 -0
- package/dist/engine-playwright/global-setup.js +142 -0
- package/dist/{core → engine-playwright}/keve-report.d.ts +0 -1
- package/dist/engine-playwright/keve-report.js +137 -0
- package/dist/{core → engine-playwright}/keve-test.d.ts +10 -8
- package/dist/{core → engine-playwright}/keve-test.js +204 -200
- package/dist/{core → engine-playwright}/pw-config.js +8 -7
- package/dist/index.d.ts +13 -8
- package/dist/index.js +43 -28
- package/dist/page-agent/agent.d.ts +82 -7
- package/dist/page-agent/agent.js +252 -88
- package/dist/page-agent/diagnostics.d.ts +184 -0
- package/dist/page-agent/diagnostics.js +377 -0
- package/dist/page-agent/hooks.d.ts +3 -2
- package/dist/page-agent/hooks.js +4 -3
- package/dist/page-agent/page-like.d.ts +116 -0
- package/dist/page-agent/page-like.js +14 -0
- package/dist/page-agent/playwright-page.d.ts +20 -0
- package/dist/page-agent/playwright-page.js +98 -0
- package/dist/page-agent/system-prompt.d.ts +7 -0
- package/dist/page-agent/system-prompt.js +10 -0
- package/dist/page-agent/tools.d.ts +1 -1
- package/dist/page-agent/tools.js +144 -8
- package/package.json +24 -11
- package/scripts/gen-system-prompt.mjs +36 -0
- package/scripts/run-dsl-task.mjs +652 -0
- package/scripts/runtime/cypress-preprocessor.mjs +44 -0
- package/scripts/runtime/cypress.config.mjs +35 -0
- package/scripts/runtime/playwright.config.mjs +41 -0
- package/scripts/verify-shot-diff.mjs +243 -0
- package/dist/commands/auth-update.d.ts +0 -16
- package/dist/commands/auth-update.js +0 -88
- package/dist/core/auth.d.ts +0 -17
- package/dist/core/auth.js +0 -224
- package/dist/core/cdp-resolver.d.ts +0 -5
- package/dist/core/cdp-resolver.js +0 -93
- package/dist/core/global-setup.d.ts +0 -6
- package/dist/core/global-setup.js +0 -123
- package/dist/core/keve-decorators.d.ts +0 -51
- package/dist/core/keve-decorators.js +0 -95
- package/dist/core/keve-report.js +0 -376
- /package/dist/core/{keve-aspect.d.ts → decorator/keve-aspect.d.ts} +0 -0
- /package/dist/core/{keve-aspect.js → decorator/keve-aspect.js} +0 -0
- /package/dist/{core → engine-playwright}/pw-config.d.ts +0 -0
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* keve-test — Playwright test fixture: keveGoal (唯一操作原语)
|
|
4
4
|
*
|
|
5
|
-
* keveGoal 执行 fn 后 → 截图+fn逻辑+expected →
|
|
6
|
-
* 一致 → 通过
|
|
7
|
-
* 不一致 → AI 告诉你还差什么 → Re-Act 探索 → 探索成功
|
|
5
|
+
* keveGoal 执行 fn 后 → 截图+fn逻辑+expected → Agent Re-Act 观察页面后给最终结论
|
|
8
6
|
* fn 无 → Re-Act 直接探索
|
|
7
|
+
* fn 成功 → 仍进 Re-Act,确认预期达成并产出推理轨迹
|
|
9
8
|
* fn 异常 → Re-Act 自愈
|
|
9
|
+
* 确定性事实(环境阻断 / 页面已关闭 / 截图差异超阈值)→ 短路,不烧 token
|
|
10
10
|
*
|
|
11
11
|
* keveAssert 已被吸收:keveGoal 的 expected 即断言语义
|
|
12
12
|
*
|
|
@@ -56,33 +56,38 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
56
56
|
};
|
|
57
57
|
})();
|
|
58
58
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
59
|
-
exports.test = exports.sceneGoalsMap = exports.sceneEvalMetaMap = exports.sceneCodeMap = exports.getModelScenes = exports.keveScene = exports.keveModel = exports.expect = void 0;
|
|
59
|
+
exports.test = exports.sceneGoalsMap = exports.sceneMetaMap = exports.sceneEvalMetaMap = exports.sceneCodeMap = exports.getModelScenes = exports.keveScene = exports.keveModel = exports.expect = void 0;
|
|
60
60
|
const test_1 = require("@playwright/test");
|
|
61
61
|
Object.defineProperty(exports, "expect", { enumerable: true, get: function () { return test_1.expect; } });
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
62
|
+
const keve_registry_js_1 = require("../core/decorator/keve-registry.js");
|
|
63
|
+
const agent_js_1 = require("../page-agent/agent.js");
|
|
64
|
+
const keve_aspect_js_1 = require("../core/decorator/keve-aspect.js");
|
|
65
|
+
const learned_actions_js_1 = require("../core/learned-actions.js");
|
|
66
|
+
const config_js_1 = require("../config.js");
|
|
66
67
|
const fs = __importStar(require("node:fs"));
|
|
67
68
|
const path = __importStar(require("node:path"));
|
|
68
|
-
// ───
|
|
69
|
-
// CDP
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Object.defineProperty(exports, "
|
|
77
|
-
Object.defineProperty(exports, "
|
|
69
|
+
// ─── 登录态注入 ─────────────────────────────────────────────────────
|
|
70
|
+
// 不再由测试侧启动浏览器 / CDP 接管。登录 cookie 由服务端解析后随环境变量下发,
|
|
71
|
+
// 这里只负责在启动浏览器前把 storageState 就位(global-setup 已写好文件)。
|
|
72
|
+
const global_setup_js_1 = require("./global-setup.js");
|
|
73
|
+
const screenshotDiff_js_1 = require("../core/judge/screenshotDiff.js");
|
|
74
|
+
const goal_heuristics_js_1 = require("../core/judge/goal-heuristics.js");
|
|
75
|
+
var keve_decorators_js_1 = require("../core/decorator/keve-decorators.js");
|
|
76
|
+
Object.defineProperty(exports, "keveModel", { enumerable: true, get: function () { return keve_decorators_js_1.keveModel; } });
|
|
77
|
+
Object.defineProperty(exports, "keveScene", { enumerable: true, get: function () { return keve_decorators_js_1.keveScene; } });
|
|
78
|
+
Object.defineProperty(exports, "getModelScenes", { enumerable: true, get: function () { return keve_decorators_js_1.getModelScenes; } });
|
|
79
|
+
var keve_registry_js_2 = require("../core/decorator/keve-registry.js");
|
|
80
|
+
Object.defineProperty(exports, "sceneCodeMap", { enumerable: true, get: function () { return keve_registry_js_2.sceneCodeMap; } });
|
|
81
|
+
Object.defineProperty(exports, "sceneEvalMetaMap", { enumerable: true, get: function () { return keve_registry_js_2.sceneEvalMetaMap; } });
|
|
82
|
+
Object.defineProperty(exports, "sceneMetaMap", { enumerable: true, get: function () { return keve_registry_js_2.sceneMetaMap; } });
|
|
78
83
|
// Re-export goal metadata for consumer convenience
|
|
79
|
-
var
|
|
80
|
-
Object.defineProperty(exports, "sceneGoalsMap", { enumerable: true, get: function () { return
|
|
84
|
+
var keve_registry_js_3 = require("../core/decorator/keve-registry.js");
|
|
85
|
+
Object.defineProperty(exports, "sceneGoalsMap", { enumerable: true, get: function () { return keve_registry_js_3.sceneGoalsMap; } });
|
|
81
86
|
// ─── Register Built-in Aspects ───────────────────────────────────────
|
|
82
87
|
// NOTE: action-log-write aspect 已废弃。
|
|
83
88
|
// AI 探索过程数据现在通过 keveGoalResult attachment 直接流到 KeveReporter,
|
|
84
89
|
// 由 KeveReporter 统一写入 confidence-data.jsonl(含 reactSteps)。
|
|
85
|
-
|
|
90
|
+
keve_aspect_js_1.keveAspect.register({
|
|
86
91
|
name: 'learnedActions-update',
|
|
87
92
|
phase: 'after',
|
|
88
93
|
order: 20,
|
|
@@ -90,7 +95,7 @@ keve_aspect_1.keveAspect.register({
|
|
|
90
95
|
if (!result?.actions?.length)
|
|
91
96
|
return;
|
|
92
97
|
const url = ctx.page.url();
|
|
93
|
-
|
|
98
|
+
learned_actions_js_1.learnedActions.add(ctx.step, result.actions, url, result.success);
|
|
94
99
|
},
|
|
95
100
|
});
|
|
96
101
|
// ─── (diagnostic extraction moved to page-agent/diagnostic.ts, called via onAfterTask hook) ──
|
|
@@ -98,57 +103,52 @@ keve_aspect_1.keveAspect.register({
|
|
|
98
103
|
exports.test = test_1.test.extend({
|
|
99
104
|
// ── 将内置 testInfo 包装为可解构 fixture ─────────────────────────
|
|
100
105
|
// 测试方法可直接写 { page, keveGoal, testInfo },不再报 "unknown parameter" 错误
|
|
106
|
+
// ── engine: 引擎无关操作适配器 ──────────────────────────────────────
|
|
107
|
+
engine: async ({ page }, use) => {
|
|
108
|
+
const { createEngine } = await Promise.resolve().then(() => __importStar(require('./engineAdapter.js')));
|
|
109
|
+
const engine = createEngine(page);
|
|
110
|
+
await use(engine);
|
|
111
|
+
},
|
|
101
112
|
testInfo: async ({}, use, testInfo) => {
|
|
102
113
|
await use(testInfo);
|
|
103
114
|
},
|
|
104
115
|
browser: async ({}, use, testInfo) => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
await use(browser);
|
|
109
|
-
// CDP 模式下不关闭浏览器(共享连接,关闭会影响后续用例)
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
// CDP 不可达 → graceful fallback 到独立启动浏览器
|
|
113
|
-
console.log(`[keve-test] CDP unavailable, falling back to chromium.launch()`);
|
|
114
|
-
const fallback = await test_1.chromium.launch(testInfo.project?.use?.launchOptions || {});
|
|
115
|
-
await use(fallback);
|
|
116
|
-
await fallback.close();
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
const browser = await test_1.chromium.launch(testInfo.project?.use?.launchOptions || {});
|
|
121
|
-
await use(browser);
|
|
122
|
-
await browser.close();
|
|
123
|
-
}
|
|
116
|
+
const browser = await test_1.chromium.launch(testInfo.project?.use?.launchOptions || {});
|
|
117
|
+
await use(browser);
|
|
118
|
+
await browser.close();
|
|
124
119
|
},
|
|
125
|
-
page: async ({ browser }, use) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// 复用第一个非 about:blank 的 page(通常是用户已登录的 tab)
|
|
135
|
-
page = existingPages.find(p => p.url() !== 'about:blank') || existingPages[0];
|
|
136
|
-
// 导航到 about:blank 作为起始页(清空之前的状态,但保留 cookie)
|
|
137
|
-
try {
|
|
138
|
-
await page.goto('about:blank', { timeout: 3000 });
|
|
139
|
-
}
|
|
140
|
-
catch { /* ignore */ }
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
page = await cdpContext.newPage();
|
|
120
|
+
page: async ({ browser }, use, testInfo) => {
|
|
121
|
+
// 建 context 时带上 storageState(服务端 cookies 写入的登录态),
|
|
122
|
+
// 裸 newPage() 会丢失 cookies 导致用例未登录失败。
|
|
123
|
+
// global-setup 正常会先写好;直接裸跑 playwright test 时这里兜底就地生成。
|
|
124
|
+
let storageState = testInfo.project?.use?.storageState;
|
|
125
|
+
if (!storageState && (0, global_setup_js_1.readInjectedCookies)().length > 0) {
|
|
126
|
+
storageState = (0, global_setup_js_1.resolveStorageStatePath)();
|
|
127
|
+
try {
|
|
128
|
+
(0, global_setup_js_1.writeStorageState)(storageState);
|
|
144
129
|
}
|
|
145
|
-
|
|
146
|
-
// CDP 模式下不关闭 page(保留浏览器 tab 给下一个用例复用)
|
|
130
|
+
catch { /* 写失败则裸跑,用例会自行暴露登录问题 */ }
|
|
147
131
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
132
|
+
// 视频录制:playwright config use.video = { mode:'on', dir } → recordVideo
|
|
133
|
+
//(自定义 context 不会自动附加视频附件,需在 context 关闭后显式 attach)
|
|
134
|
+
const videoOpt = testInfo.project?.use?.video;
|
|
135
|
+
const videoOn = !!videoOpt && (videoOpt === 'on' || videoOpt.mode === 'on' || videoOpt.mode === 'retain-on-failure');
|
|
136
|
+
const context = await browser.newContext({
|
|
137
|
+
storageState,
|
|
138
|
+
...(videoOn ? { recordVideo: { dir: typeof videoOpt === 'object' && videoOpt.dir ? videoOpt.dir : undefined } } : {}),
|
|
139
|
+
});
|
|
140
|
+
const page = await context.newPage();
|
|
141
|
+
await use(page);
|
|
142
|
+
const video = page.video();
|
|
143
|
+
await page.close();
|
|
144
|
+
await context.close();
|
|
145
|
+
// 视频文件在 context 关闭后才定稿:显式挂附件 → results.json attachments
|
|
146
|
+
if (video) {
|
|
147
|
+
try {
|
|
148
|
+
const videoPath = await video.path();
|
|
149
|
+
await testInfo.attach('video', { path: videoPath });
|
|
150
|
+
}
|
|
151
|
+
catch { /* 无视频不阻塞 */ }
|
|
152
152
|
}
|
|
153
153
|
},
|
|
154
154
|
// ── keveGoal: 唯一操作原语 ────────────────────────────────────────
|
|
@@ -162,9 +162,9 @@ exports.test = test_1.test.extend({
|
|
|
162
162
|
order: goalOrder++,
|
|
163
163
|
};
|
|
164
164
|
const sceneTitle = testInfo.title;
|
|
165
|
-
if (!
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
if (!keve_registry_js_1.sceneGoalsMap.has(sceneTitle))
|
|
166
|
+
keve_registry_js_1.sceneGoalsMap.set(sceneTitle, []);
|
|
167
|
+
keve_registry_js_1.sceneGoalsMap.get(sceneTitle).push(goalMeta);
|
|
168
168
|
const ctx = {
|
|
169
169
|
page,
|
|
170
170
|
step: options.step,
|
|
@@ -175,7 +175,7 @@ exports.test = test_1.test.extend({
|
|
|
175
175
|
specFilePath: testInfo.file,
|
|
176
176
|
};
|
|
177
177
|
// Run before aspects
|
|
178
|
-
await
|
|
178
|
+
await keve_aspect_js_1.keveAspect.runPhase('before', ctx);
|
|
179
179
|
// ── Capture goal-before screenshot (fn 之前的页面原始状态) ──
|
|
180
180
|
// 语义:记录"测试动作发生前"的页面,供报告 before/after 对照
|
|
181
181
|
let goalScreenshotBefore = '';
|
|
@@ -186,24 +186,46 @@ exports.test = test_1.test.extend({
|
|
|
186
186
|
let result;
|
|
187
187
|
const fnSource = fn?.toString() || undefined;
|
|
188
188
|
let fnError;
|
|
189
|
+
let fnErrorRaw; // 原始错误对象(ScreenshotDiffError 等带结构的错误需要保留结构)
|
|
190
|
+
let fnResult; // fn 返回值透传(flow 产物,如 createDashboard 的看板名)
|
|
189
191
|
let fnBlocked = false; // true when fn error is environment-blocked (skip agent Re-Act)
|
|
192
|
+
// 截图对比:fn 前开缓冲、fn 后收集 —— engine.expectScreenshot 的结果经此进 attachment
|
|
193
|
+
(0, screenshotDiff_js_1.beginGoalShotDiffCapture)();
|
|
190
194
|
if (fn) {
|
|
191
195
|
try {
|
|
192
|
-
await fn();
|
|
196
|
+
fnResult = await fn();
|
|
193
197
|
console.log(`[keveGoal] "${options.step}" fn executed`);
|
|
194
198
|
}
|
|
195
199
|
catch (err) {
|
|
196
200
|
fnError = err?.message || String(err);
|
|
201
|
+
fnErrorRaw = err;
|
|
197
202
|
console.log(`[keveGoal] "${options.step}" fn error: ${fnError}`);
|
|
198
203
|
// ── Detect environment-blocked errors: skip Agent Re-Act, directly BLOCKED ──
|
|
199
204
|
// When fn fails due to empty URL, connection refused, timeout, or navigation to invalid URL,
|
|
200
205
|
// Agent exploration cannot fix environment issues — waste of time and tokens.
|
|
201
|
-
fnBlocked = isEnvironmentBlockedError(err);
|
|
206
|
+
fnBlocked = (0, goal_heuristics_js_1.isEnvironmentBlockedError)(err);
|
|
202
207
|
if (fnBlocked) {
|
|
203
208
|
console.log(`[keveGoal] 🚫 "${options.step}" BLOCKED — environment error detected, skipping agent Re-Act`);
|
|
204
209
|
}
|
|
210
|
+
// ── Detect page-level permission block (forbidden / unauthorized) ──
|
|
211
|
+
// When the page redirected to a forbidden/unauthorized URL (e.g. kwaibi /pc/dashboard/forbidden),
|
|
212
|
+
// the fn error is typically a locator timeout — but the root cause is access denial,
|
|
213
|
+
// not a missing element. Mark as blocked and annotate the error for accurate reporting.
|
|
214
|
+
if (!fnBlocked && fnError) {
|
|
215
|
+
try {
|
|
216
|
+
const blockedUrl = page.url();
|
|
217
|
+
if (/\/forbidden|\/unauthorized|\/403|\/no-permission/i.test(blockedUrl)) {
|
|
218
|
+
fnBlocked = true;
|
|
219
|
+
fnError = `页面访问被阻断(当前 URL=${blockedUrl}),目标元素未渲染 — 权限或数据问题,非用例缺陷`;
|
|
220
|
+
console.log(`[keveGoal] 🚫 "${options.step}" BLOCKED — page redirected to forbidden/unauthorized: ${blockedUrl}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch { /* page.url() may fail if page is closed */ }
|
|
224
|
+
}
|
|
205
225
|
}
|
|
206
226
|
}
|
|
227
|
+
// fn 结束(无论成败)即取出本 goal 的对比结果;不再有引擎路径会往里写
|
|
228
|
+
const shotDiffs = (0, screenshotDiff_js_1.drainGoalShotDiff)();
|
|
207
229
|
// ── Capture fn-after screenshot (fn 执行后的页面状态,传给 agent 说明执行前后) ──
|
|
208
230
|
let fnAfterScreenshot = '';
|
|
209
231
|
if (fn) {
|
|
@@ -214,7 +236,7 @@ exports.test = test_1.test.extend({
|
|
|
214
236
|
}
|
|
215
237
|
// ── Hand off to agent: agent does Re-Act (OR shortcut to blocked if fnBlocked) ──
|
|
216
238
|
const currentUrl = page.url();
|
|
217
|
-
const learnedHint =
|
|
239
|
+
const learnedHint = learned_actions_js_1.learnedActions.getHint(options.step, currentUrl);
|
|
218
240
|
let reactResult;
|
|
219
241
|
let reactTimedOut = false;
|
|
220
242
|
if (fnBlocked) {
|
|
@@ -258,6 +280,23 @@ exports.test = test_1.test.extend({
|
|
|
258
280
|
conclusion: 'blocked',
|
|
259
281
|
};
|
|
260
282
|
}
|
|
283
|
+
else if (fnErrorRaw instanceof screenshotDiff_js_1.ScreenshotDiffError) {
|
|
284
|
+
// ── 截图对比超阈值:确定性事实,跳过 Agent Re-Act ──
|
|
285
|
+
// diff 不是探索能修复的(页面就该和基线不一样才报错),探索只会烧 token;
|
|
286
|
+
// 对比结果已入 shotDiffs,attachment 会带上完整差异证据。
|
|
287
|
+
console.log(`[keveGoal] 📊 "${options.step}" screenshot diff exceeded — skipping agent Re-Act`);
|
|
288
|
+
reactResult = {
|
|
289
|
+
expectedMet: false,
|
|
290
|
+
actions: [{
|
|
291
|
+
action: { tool: 'done', verdict: 'fail', text: fnError },
|
|
292
|
+
toolOutput: fnErrorRaw.shotDiff?.message || '',
|
|
293
|
+
result: 'ok',
|
|
294
|
+
}],
|
|
295
|
+
finalSnapshot: '',
|
|
296
|
+
agentScreenshots: [],
|
|
297
|
+
conclusion: 'fail',
|
|
298
|
+
};
|
|
299
|
+
}
|
|
261
300
|
else {
|
|
262
301
|
// ── Per-goal timeout: prevent one slow goal from exhausting the test timeout ──
|
|
263
302
|
// Default 300s per goal; total test timeout should be ≥ (goals × 300s + overhead)
|
|
@@ -268,7 +307,7 @@ exports.test = test_1.test.extend({
|
|
|
268
307
|
console.log(`[keveGoal] ⏰ "${options.step}" timed out after ${GOAL_TIMEOUT_MS}ms — stopping agent`);
|
|
269
308
|
}, GOAL_TIMEOUT_MS);
|
|
270
309
|
try {
|
|
271
|
-
reactResult = await (0,
|
|
310
|
+
reactResult = await (0, agent_js_1.reactLoop)(page, options.step, options.expected, {
|
|
272
311
|
learnedActionsHint: learnedHint,
|
|
273
312
|
specFilePath: ctx.specFilePath,
|
|
274
313
|
fnSource,
|
|
@@ -276,6 +315,11 @@ exports.test = test_1.test.extend({
|
|
|
276
315
|
signal: goalTimeoutController.signal,
|
|
277
316
|
goalScreenshotBefore: goalScreenshotBefore || undefined,
|
|
278
317
|
fnAfterScreenshot: fnAfterScreenshot || undefined,
|
|
318
|
+
targetUrl: process.env.KEVE_TARGET_URL || '',
|
|
319
|
+
getEnv: (name) => process.env[name] || '',
|
|
320
|
+
saveScreenshot: saveAgentScreenshot,
|
|
321
|
+
loadInitialImages: loadInitialImages,
|
|
322
|
+
llm: reactLlmConfig(),
|
|
279
323
|
});
|
|
280
324
|
}
|
|
281
325
|
catch (reactErr) {
|
|
@@ -291,6 +335,7 @@ exports.test = test_1.test.extend({
|
|
|
291
335
|
finalSnapshot: '',
|
|
292
336
|
agentScreenshots: [],
|
|
293
337
|
conclusion: isGoalTimeout ? 'blocked' : 'fail',
|
|
338
|
+
diagnostics: undefined,
|
|
294
339
|
};
|
|
295
340
|
}
|
|
296
341
|
finally {
|
|
@@ -304,7 +349,7 @@ exports.test = test_1.test.extend({
|
|
|
304
349
|
// show 15px, so consistent") and override to pass, but 15px ≠ 16px is a definitive failure.
|
|
305
350
|
// This is the per-goal equivalent of pipeline afterHook (case-gen-validate) —
|
|
306
351
|
// deterministic check > LLM judgment.
|
|
307
|
-
if (fnError && !fnBlocked && reactResult.expectedMet && isAssertionFailure(fnError)) {
|
|
352
|
+
if (fnError && !fnBlocked && reactResult.expectedMet && (0, goal_heuristics_js_1.isAssertionFailure)(fnError)) {
|
|
308
353
|
console.log(`[keveGoal] ⚠️ "${options.step}" Agent said PASS but fn had assertion failure — overriding to FAIL`);
|
|
309
354
|
console.log(`[keveGoal] fn assertion: ${fnError}`);
|
|
310
355
|
reactResult.expectedMet = false;
|
|
@@ -318,13 +363,19 @@ exports.test = test_1.test.extend({
|
|
|
318
363
|
? undefined
|
|
319
364
|
: reactResult.conclusion === 'blocked'
|
|
320
365
|
? new Error(`Blocked: ${fnBlocked ? fnError : (reactTimedOut ? `Agent 超时 (300000ms)` : (reactResult.actions?.filter((a) => a.action?.tool === 'done').pop()?.action?.text || 'Agent blocked'))}`)
|
|
321
|
-
: (
|
|
322
|
-
? new Error(
|
|
323
|
-
:
|
|
366
|
+
: (fnErrorRaw instanceof screenshotDiff_js_1.ScreenshotDiffError)
|
|
367
|
+
? new Error(fnError) // 截图对比:保留带差异率/区块数/基线版本的结构化错误文案
|
|
368
|
+
: (fnError && (0, goal_heuristics_js_1.isAssertionFailure)(fnError) && !fnBlocked)
|
|
369
|
+
? new Error(`Assertion failed: ${fnError}`)
|
|
370
|
+
: new Error(`Expected not achieved: ${options.expected}`),
|
|
324
371
|
};
|
|
325
372
|
// Attach Agent conclusion for downstream consumers
|
|
326
373
|
if (reactResult.conclusion)
|
|
327
374
|
result.conclusion = reactResult.conclusion;
|
|
375
|
+
// Attach runtime diagnostics for downstream consumers (Playwright side must
|
|
376
|
+
// copy them here because the attachment reads from `result`).
|
|
377
|
+
if (reactResult.diagnostics)
|
|
378
|
+
result.diagnostics = reactResult.diagnostics;
|
|
328
379
|
console.log(reactResult.expectedMet
|
|
329
380
|
? `[keveGoal] ✅ "${options.step}" PASSED`
|
|
330
381
|
: `[keveGoal] ❌ "${options.step}" FAILED (${reactResult.conclusion || 'fail'}) — ${result.error?.message || 'expected not achieved'}`);
|
|
@@ -420,21 +471,31 @@ exports.test = test_1.test.extend({
|
|
|
420
471
|
}),
|
|
421
472
|
finalSnapshot: result.finalSnapshot ? String(result.finalSnapshot).slice(0, 500) : undefined,
|
|
422
473
|
diagnosticHints,
|
|
474
|
+
diagnostics: result.diagnostics,
|
|
423
475
|
goalScreenshotBefore: goalScreenshotBefore || undefined,
|
|
424
476
|
goalScreenshotAfter: goalScreenshotAfter || undefined,
|
|
425
|
-
|
|
477
|
+
// agent 未单独留图时,用 fn-after 兜底,保证报告至少有一张执行后截图。
|
|
478
|
+
// 不能写 `|| [...]`:空数组是 truthy,会把兜底截图短路丢弃。
|
|
479
|
+
agentScreenshots: result.agentScreenshots?.length
|
|
480
|
+
? result.agentScreenshots
|
|
481
|
+
: (fnAfterScreenshot ? [fnAfterScreenshot] : []),
|
|
482
|
+
// 截图对比证据(matched/created/exceeded 全量保留;平台 stepsDetail 回收为 evidence.screenshotDiff)
|
|
483
|
+
screenshotDiff: shotDiffs.length ? shotDiffs : undefined,
|
|
426
484
|
// refinePatch: (result as any).refinePatch || undefined, // 已注释:scriptRefine 已禁用
|
|
427
485
|
conclusion: result.conclusion || undefined,
|
|
428
486
|
}), 'utf-8'),
|
|
429
487
|
});
|
|
430
488
|
// Run after aspects
|
|
431
|
-
await
|
|
489
|
+
await keve_aspect_js_1.keveAspect.runPhase('after', ctx, result, result.error);
|
|
432
490
|
// Throw if not successful
|
|
433
491
|
if (!result.success) {
|
|
434
492
|
const failReason = result.error?.message || 'expected state not achieved';
|
|
435
493
|
console.log(`[keveGoal] ❌ "${options.step}" FAILED — reason: ${failReason}`);
|
|
436
494
|
throw result.error || new Error(`keveGoal "${options.step}" failed: ${failReason}`);
|
|
437
495
|
}
|
|
496
|
+
// 透传 fn 返回值:flow 辅助函数(如 createDashboard 返回看板名)可包进 keveGoal,
|
|
497
|
+
// 脚本失败时由 Agent Re-Act 自愈,成功时把产物继续传给后续步骤
|
|
498
|
+
return fnResult;
|
|
438
499
|
};
|
|
439
500
|
await use(keveGoalFn);
|
|
440
501
|
// keveGoals attachment 已删除:step/expected/precondition/order 已合并进
|
|
@@ -443,134 +504,77 @@ exports.test = test_1.test.extend({
|
|
|
443
504
|
}
|
|
444
505
|
});
|
|
445
506
|
// Register extended test with keve-decorators so @keveModel uses the correct test
|
|
446
|
-
const
|
|
447
|
-
(0,
|
|
507
|
+
const keve_decorators_js_2 = require("../core/decorator/keve-decorators.js");
|
|
508
|
+
(0, keve_decorators_js_2.setKeveTest)(exports.test);
|
|
448
509
|
// ── 辅助函数 ──
|
|
449
510
|
/**
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
* When fn (deterministic script) fails with these patterns, Agent Re-Act cannot
|
|
453
|
-
* fix the issue — the error is environmental, not application-level:
|
|
454
|
-
*
|
|
455
|
-
* 1. Empty/undefined URL → page.goto(''), page.goto(undefined)
|
|
456
|
-
* 2. Connection refused / net::ERR_CONNECTION_REFUSED
|
|
457
|
-
* 3. Navigation timeout (page not reachable)
|
|
458
|
-
* 4. Invalid URL format
|
|
459
|
-
* 5. SSO redirect / auth failure (not the app's fault)
|
|
460
|
-
*
|
|
461
|
-
* Returns true → keveGoal skips Agent Re-Act and directly reports BLOCKED
|
|
462
|
-
* with the fn error as the blocking reason.
|
|
511
|
+
* 截图并保存到当前轮次的 reports/round-N,返回相对 taskDir 的路径
|
|
512
|
+
* @param phase 'before' | 'after-fn' — 用于文件名前缀
|
|
463
513
|
*/
|
|
464
|
-
function
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|| msg.includes('invalid url') || msg.includes('url is undefined')
|
|
469
|
-
|| msg.includes('navigation to ""') || msg.includes("navigation to ''")) {
|
|
470
|
-
return true;
|
|
471
|
-
}
|
|
472
|
-
// page.goto(undefined): Playwright type error "url: expected string, got undefined"
|
|
473
|
-
// This happens when process.env.PAGE_* is not set — environment config issue, not app issue
|
|
474
|
-
if ((msg.includes('expected string') || msg.includes('expected a string'))
|
|
475
|
-
&& (msg.includes('got undefined') || msg.includes('got null') || msg.includes('received undefined'))) {
|
|
476
|
-
return true;
|
|
477
|
-
}
|
|
478
|
-
// Connection refused / network unreachable
|
|
479
|
-
if (msg.includes('err_connection_refused') || msg.includes('connection refused')
|
|
480
|
-
|| msg.includes('net::err_connection') || msg.includes('err_name_not_resolved')
|
|
481
|
-
|| msg.includes('err_address_unreachable') || msg.includes('err_internet_disconnected')
|
|
482
|
-
|| msg.includes('err_connection_timed_out') || msg.includes('err_connection_reset')) {
|
|
483
|
-
return true;
|
|
484
|
-
}
|
|
485
|
-
// Navigation timeout (page not reachable in time)
|
|
486
|
-
if (msg.includes('navigation timeout of') || msg.includes('timeout of') && msg.includes('exceeded')
|
|
487
|
-
|| msg.includes('page.goto: timeout') || msg.includes('navigating to') && msg.includes('timed out')) {
|
|
488
|
-
return true;
|
|
489
|
-
}
|
|
490
|
-
// Playwright locator/waitForSelector timeout — selector not found on page (fn script issue, not app crash)
|
|
491
|
-
// e.g. "Timeout 15000ms exceeded while waiting for locator('.selector') to be visible"
|
|
492
|
-
// This is a script-quality issue (wrong selector), not an env blocked error.
|
|
493
|
-
// Return false here — let Agent take over rather than treating as BLOCKED.
|
|
494
|
-
// Note: we intentionally do NOT return true; these should fall through to Agent Re-Act.
|
|
495
|
-
// IDC network segment restriction
|
|
496
|
-
if (msg.includes('unable to access on the idc network segment')
|
|
497
|
-
|| msg.includes('idc network segment') || msg.includes('40314')) {
|
|
498
|
-
return true;
|
|
499
|
-
}
|
|
500
|
-
// SSO / auth redirect (not the app's fault)
|
|
501
|
-
if (msg.includes('sso redirect') || msg.includes('login redirect detected')
|
|
502
|
-
|| msg.includes('err_too_many_redirects')) {
|
|
503
|
-
return true;
|
|
504
|
-
}
|
|
505
|
-
// Page/context/browser closed — dead page, Agent cannot explore
|
|
506
|
-
// e.g. "page.waitForLoadState: Target page, context or browser has been closed"
|
|
507
|
-
// e.g. "page.ariaSnapshot: Target page, context or browser has been closed"
|
|
508
|
-
// When the CDP connection drops or the user closes the tab, any page API throws this.
|
|
509
|
-
// Agent Re-Act on a dead page is a waste of time → BLOCKED immediately.
|
|
510
|
-
if (msg.includes('target page, context or browser has been closed')
|
|
511
|
-
|| msg.includes('page has been closed') || msg.includes('browser has been closed')
|
|
512
|
-
|| msg.includes('context has been closed')) {
|
|
513
|
-
return true;
|
|
514
|
-
}
|
|
515
|
-
return false;
|
|
514
|
+
async function captureScreenshot(page, stepName, order, phase) {
|
|
515
|
+
const buf = await page.screenshot({ type: 'png', timeout: 15000 });
|
|
516
|
+
const safeName = stepName.replace(/[^a-zA-Z0-9一-鿿]/g, '_').slice(0, 30);
|
|
517
|
+
return writeScreenshot(buf, `goal-${phase}-${order}-${safeName}`);
|
|
516
518
|
}
|
|
517
519
|
/**
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
* Expected vs Received values, the mismatch is a *fact*, not a judgment.
|
|
522
|
-
* The Agent may rationalize "consistency" (e.g. "both pages show 15px → consistent → pass")
|
|
523
|
-
* and override to pass, but 15px ≠ 16px is a definitive failure.
|
|
524
|
-
*
|
|
525
|
-
* This function identifies such assertion failures so keveGoal can:
|
|
526
|
-
* - Override Agent "pass" → "fail" when the fn proved a value mismatch
|
|
527
|
-
* - Use the precise assertion message in the error (instead of generic "expected not achieved")
|
|
528
|
-
*
|
|
529
|
-
* Returns true for assertion errors, false for environment/runtime errors.
|
|
520
|
+
* 当前轮次截图目录绝对路径(与 Cypress 桥同名同层)。
|
|
521
|
+
* 新布局为 <taskRoot>/reports/round-N/screenshots;缺失时回退旧布局,
|
|
522
|
+
* 兼容历史执行环境。
|
|
530
523
|
*/
|
|
531
|
-
function
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
// e.g. "page.goto: url: expected string, got undefined" — this is an env config error,
|
|
535
|
-
// not a functional assertion failure. isEnvironmentBlockedError() handles it instead.
|
|
536
|
-
if (msg.includes('expected string') || msg.includes('expected a string')
|
|
537
|
-
|| msg.includes('expected number') || msg.includes('expected boolean')) {
|
|
538
|
-
// Only exclude if it looks like an API parameter error (not an assertion)
|
|
539
|
-
if (msg.includes('got undefined') || msg.includes('got null')
|
|
540
|
-
|| msg.includes('received undefined') || msg.includes('received null')) {
|
|
541
|
-
return false;
|
|
542
|
-
}
|
|
524
|
+
function screenshotsDir() {
|
|
525
|
+
if (process.env.KEVE_RESULT_DIR) {
|
|
526
|
+
return path.join(path.resolve(process.env.KEVE_RESULT_DIR), 'screenshots');
|
|
543
527
|
}
|
|
544
|
-
// Jest/Vitest/playwright assertion: "Expected: X, Received: Y"
|
|
545
|
-
if (msg.includes('expected') && msg.includes('received'))
|
|
546
|
-
return true;
|
|
547
|
-
// AssertionError / assert keyword
|
|
548
|
-
if (msg.includes('assertionerror') || msg.includes('assertion failed')
|
|
549
|
-
|| msg.includes('assertion error'))
|
|
550
|
-
return true;
|
|
551
|
-
// expect(x).toBe(y) / expect(x).toEqual(y) pattern
|
|
552
|
-
// Note: must NOT match Playwright timeout messages like "Timeout 15000ms exceeded waiting for locator(...) to be visible"
|
|
553
|
-
// Those are env/timeout errors, not value assertion failures.
|
|
554
|
-
if ((msg.includes(' to be ') && !msg.includes('timeout') && !msg.includes('waiting for') && !msg.includes('exceeded'))
|
|
555
|
-
|| msg.includes(' to equal ') || msg.includes(' to deeply equal '))
|
|
556
|
-
return true;
|
|
557
|
-
// Playwright expect: "expected string, received number" etc.
|
|
558
|
-
if (msg.includes('expected ') && (msg.includes(' but ') || msg.includes(' got ') || msg.includes(' received ')))
|
|
559
|
-
return true;
|
|
560
|
-
return false;
|
|
561
|
-
}
|
|
562
|
-
/**
|
|
563
|
-
* 截图并保存到 test-artifacts,返回相对 taskDir 的路径
|
|
564
|
-
* @param phase 'before' | 'after-fn' — 用于文件名前缀
|
|
565
|
-
*/
|
|
566
|
-
async function captureScreenshot(page, stepName, order, phase) {
|
|
567
|
-
const buf = await page.screenshot({ type: 'png', timeout: 15000 });
|
|
568
528
|
const taskDir = process.env.KEVE_TASK_DIR || '.keve';
|
|
569
529
|
const round = process.env.KEVE_ROUND || 'latest';
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
530
|
+
return path.join(taskDir, 'test-artifacts', `round-${round}`, 'screenshots');
|
|
531
|
+
}
|
|
532
|
+
/** 落盘 PNG,返回相对 taskDir 的路径 */
|
|
533
|
+
function writeScreenshot(png, baseName) {
|
|
534
|
+
const taskDir = process.env.KEVE_TASK_DIR || '.keve';
|
|
535
|
+
const dir = screenshotsDir();
|
|
536
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
537
|
+
const file = path.join(dir, `${baseName}-${Date.now()}.png`);
|
|
538
|
+
fs.writeFileSync(file, png);
|
|
575
539
|
return path.relative(taskDir, file);
|
|
576
540
|
}
|
|
541
|
+
/** Agent 单步截图落盘(宿主注入给 reactLoop,与 Cypress 桥的命名同形) */
|
|
542
|
+
async function saveAgentScreenshot(pngBase64, stepIndex, stepName) {
|
|
543
|
+
const safeName = stepName.replace(/[^a-zA-Z0-9一-鿿]/g, '_').slice(0, 30);
|
|
544
|
+
return writeScreenshot(Buffer.from(pngBase64, 'base64'), `agent-step${stepIndex}-${safeName}`);
|
|
545
|
+
}
|
|
546
|
+
/** 读取 goal-before / fn-after 图片,转成多模态 ContentItem(唯一注意:路径是相对 taskDir) */
|
|
547
|
+
async function loadInitialImages(paths) {
|
|
548
|
+
const taskDir = process.env.KEVE_TASK_DIR || '.keve';
|
|
549
|
+
const items = [];
|
|
550
|
+
for (const [label, rel] of [
|
|
551
|
+
['goal-before', paths.goalScreenshotBefore],
|
|
552
|
+
['fn-after', paths.fnAfterScreenshot],
|
|
553
|
+
]) {
|
|
554
|
+
if (!rel)
|
|
555
|
+
continue;
|
|
556
|
+
try {
|
|
557
|
+
const file = path.isAbsolute(rel) ? rel : path.join(taskDir, rel);
|
|
558
|
+
if (!fs.existsSync(file))
|
|
559
|
+
continue;
|
|
560
|
+
const b64 = fs.readFileSync(file).toString('base64');
|
|
561
|
+
items.push({ type: 'text', text: `Screenshot (${label}):` });
|
|
562
|
+
items.push({ type: 'image_url', image_url: { url: `data:image/png;base64,${b64}` } });
|
|
563
|
+
}
|
|
564
|
+
catch { /* 单张图读失败不影响探索 */ }
|
|
565
|
+
}
|
|
566
|
+
return items;
|
|
567
|
+
}
|
|
568
|
+
/** Re-Act 的 LLM 配置:优先 keve.yaml,其次进程环境变量(与 config.ts DEFAULT_CONFIG 同源) */
|
|
569
|
+
function reactLlmConfig() {
|
|
570
|
+
let cfg = { base_url: '', model: '', api_key: '' };
|
|
571
|
+
try {
|
|
572
|
+
cfg = (0, config_js_1.loadConfig)().llm;
|
|
573
|
+
}
|
|
574
|
+
catch { /* 配置缺失时退回环境变量 */ }
|
|
575
|
+
return {
|
|
576
|
+
baseURL: cfg.base_url || process.env.KEVE_LLM_BASE_URL || '',
|
|
577
|
+
model: cfg.model || process.env.KEVE_LLM_MODEL_NAME || process.env.KEVE_LLM_MODEL || '',
|
|
578
|
+
apiKey: cfg.api_key || process.env.KEVE_LLM_API_KEY || '',
|
|
579
|
+
};
|
|
580
|
+
}
|
|
@@ -43,7 +43,8 @@ exports.generatePwConfig = generatePwConfig;
|
|
|
43
43
|
exports.resolveConfigOutputPath = resolveConfigOutputPath;
|
|
44
44
|
exports.findExistingConfig = findExistingConfig;
|
|
45
45
|
const path = __importStar(require("node:path"));
|
|
46
|
-
const
|
|
46
|
+
const fs = __importStar(require("node:fs"));
|
|
47
|
+
const config_js_1 = require("../config.js");
|
|
47
48
|
function generatePwConfig(options = {}) {
|
|
48
49
|
const { baseUrl, cdp = false, cdpEndpoint, webServer = !cdp, workers = 3, timeout = 1200000, retries = 0, format = 'ts', } = options;
|
|
49
50
|
const baseURLLine = baseUrl
|
|
@@ -89,7 +90,7 @@ export default defineConfig({
|
|
|
89
90
|
baseURL: ${baseURLLine},${cdp ? `\n // CDP mode: login state preserved via connected Chrome` : `\n storageState: path.resolve(projectRoot, '.auth/storage-state.json'),`}
|
|
90
91
|
trace: 'on-first-retry',
|
|
91
92
|
screenshot: 'off',
|
|
92
|
-
video: '
|
|
93
|
+
video: 'on',
|
|
93
94
|
actionTimeout: 5000,${cdpBlock}
|
|
94
95
|
},
|
|
95
96
|
outputDir: path.join(resultDir, 'test-results'),${webServerBlock}
|
|
@@ -137,7 +138,7 @@ export default defineConfig({
|
|
|
137
138
|
baseURL: ${baseURLLine},${cdp ? `\n // CDP mode: login state preserved via connected Chrome` : `\n storageState: resolve(projectRoot, '.auth', 'storage-state.json'),`}
|
|
138
139
|
trace: 'on-first-retry',
|
|
139
140
|
screenshot: 'off',
|
|
140
|
-
video: '
|
|
141
|
+
video: 'on',
|
|
141
142
|
actionTimeout: 5000,${cdpBlock}
|
|
142
143
|
},
|
|
143
144
|
outputDir: join(resultDir, 'test-results'),${webServerBlock}
|
|
@@ -148,12 +149,12 @@ function resolveConfigOutputPath(explicitOutput, format = 'ts') {
|
|
|
148
149
|
const ext = format === 'mjs' ? '.mjs' : '.ts';
|
|
149
150
|
if (explicitOutput)
|
|
150
151
|
return path.resolve(explicitOutput);
|
|
151
|
-
const keveDir = (0,
|
|
152
|
-
const taskDir = (0,
|
|
152
|
+
const keveDir = (0, config_js_1.findKeveDir)();
|
|
153
|
+
const taskDir = (0, config_js_1.resolveTaskDir)(keveDir);
|
|
153
154
|
if (taskDir)
|
|
154
155
|
return path.join(taskDir, `keve-test.config${ext}`);
|
|
155
156
|
if (keveDir) {
|
|
156
|
-
const branchDir = (0,
|
|
157
|
+
const branchDir = (0, config_js_1.getBranchDir)(keveDir);
|
|
157
158
|
if (branchDir)
|
|
158
159
|
return path.join(branchDir, `keve-test.config${ext}`);
|
|
159
160
|
return path.join(keveDir, `keve-test.config${ext}`);
|
|
@@ -173,7 +174,7 @@ function findExistingConfig(keveDir, taskDir) {
|
|
|
173
174
|
searchPaths.push(path.join(process.cwd(), 'keve_test_spec', 'keve-test.config.mjs'));
|
|
174
175
|
searchPaths.push(path.join(process.cwd(), 'keve_test_spec', 'keve-test.config.ts'));
|
|
175
176
|
for (const p of searchPaths) {
|
|
176
|
-
if (
|
|
177
|
+
if (fs.existsSync(p))
|
|
177
178
|
return p;
|
|
178
179
|
}
|
|
179
180
|
return null;
|