@kkeve/suite 0.0.1 → 0.0.2
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/README.md +53 -0
- package/dist/bin/keve.d.ts +13 -0
- package/dist/bin/keve.js +164 -0
- package/dist/commands/auth-update.d.ts +16 -0
- package/dist/commands/auth-update.js +88 -0
- package/dist/commands/clean.d.ts +10 -0
- package/dist/commands/clean.js +78 -0
- package/dist/commands/config.d.ts +17 -0
- package/dist/commands/config.js +99 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +292 -0
- package/dist/commands/report.d.ts +12 -0
- package/dist/commands/report.js +159 -0
- package/dist/commands/run.d.ts +29 -0
- package/dist/commands/run.js +417 -0
- package/dist/commands/test.d.ts +12 -0
- package/dist/commands/test.js +102 -0
- package/dist/commands/vitest.d.ts +14 -0
- package/dist/commands/vitest.js +166 -0
- package/dist/config.d.ts +109 -0
- package/dist/config.js +291 -0
- package/dist/core/action-log-writer.d.ts +47 -0
- package/dist/{tools/write-file.js → core/action-log-writer.js} +42 -26
- package/dist/core/auth.d.ts +17 -0
- package/dist/core/auth.js +224 -0
- package/dist/core/cdp-resolver.d.ts +5 -0
- package/dist/core/cdp-resolver.js +93 -0
- package/dist/core/generateReport/report.d.ts +19 -0
- package/dist/core/generateReport/report.js +91 -0
- package/dist/core/generateReport/reportData.d.ts +215 -0
- package/dist/core/generateReport/reportData.js +735 -0
- package/dist/core/global-setup.d.ts +6 -0
- package/dist/core/global-setup.js +113 -0
- package/dist/core/keve-aspect.d.ts +54 -0
- package/dist/core/keve-aspect.js +52 -0
- package/dist/core/keve-decorators.d.ts +51 -0
- package/dist/core/keve-decorators.js +95 -0
- package/dist/core/keve-registry.d.ts +38 -0
- package/dist/core/keve-registry.js +25 -0
- package/dist/core/keve-report.d.ts +27 -0
- package/dist/core/keve-report.js +357 -0
- package/dist/core/keve-test.d.ts +36 -0
- package/dist/core/keve-test.js +364 -0
- package/dist/core/learned-actions.d.ts +24 -0
- package/dist/core/learned-actions.js +65 -0
- package/dist/core/pw-config.d.ts +19 -0
- package/dist/core/pw-config.js +180 -0
- package/dist/index.d.ts +8 -23
- package/dist/index.js +36 -43
- package/dist/page-agent/agent.d.ts +129 -0
- package/dist/page-agent/agent.js +585 -0
- package/dist/page-agent/hooks.d.ts +13 -0
- package/dist/page-agent/hooks.js +218 -0
- package/dist/page-agent/tools.d.ts +29 -0
- package/dist/page-agent/tools.js +297 -0
- package/doc/README.md +124 -0
- package/doc/agents/keve-dev-test-agent.md +140 -0
- package/doc/agents/keve-test-agent.md +178 -0
- package/doc/asserts//346/265/213/350/257/225/345/220/204/351/230/266/346/256/265/350/257/264/346/230/216.png +0 -0
- package/doc/keve-agent-demo.html +370 -0
- package/doc/skills/keve-bddcase-gen/SKILL.md +332 -0
- package/doc/skills/keve-bddcase-gen/references/test-case-rules.md +91 -0
- package/doc/skills/keve-bddcase-gen/references/test-cases-example.md +103 -0
- package/doc/skills/keve-component-gen/SKILL.md +255 -0
- package/doc/skills/keve-component-gen/references/component-mock-strategy.md +74 -0
- package/doc/skills/keve-component-gen/references/component-model-template.md +98 -0
- package/doc/skills/keve-component-gen/references/component-test-template.md +100 -0
- package/doc/skills/keve-component-gen/references/vue-test-utils-guide.md +91 -0
- package/doc/skills/keve-e2e-gen/SKILL.md +179 -0
- package/doc/skills/keve-e2e-gen/references/code-analysis-guide.md +208 -0
- package/doc/skills/keve-e2e-gen/references/decisions.md +36 -0
- package/doc/skills/keve-e2e-gen/references/pom-generation-guide.md +140 -0
- package/doc/skills/keve-e2e-gen/references/script-generation-guide.md +236 -0
- package/doc/skills/keve-e2e-gen/references/test-auditor-guide.md +189 -0
- package/doc/skills/keve-e2e-gen/scripts/extract-components.sh +85 -0
- package/doc/skills/keve-e2e-gen/scripts/inject-testids.sh +95 -0
- package/doc/skills/keve-static-analysis/SKILL.md +362 -0
- package/doc/skills/keve-static-analysis/references/codegraph-analysis-guide.md +108 -0
- package/doc/skills/keve-static-analysis/references/git-impact-analysis.md +81 -0
- package/doc/skills/keve-static-analysis/references/lint-fix-guide.md +69 -0
- package/doc/skills/keve-static-analysis/references/runtime-risk-patterns.md +92 -0
- package/doc/skills/keve-static-analysis/references/security-scan-guide.md +32 -0
- package/doc/skills/keve-static-analysis/references/ts-error-patterns.md +98 -0
- package/doc/skills/keve-static-analysis/references/vue-security-guide.md +72 -0
- package/doc/skills/keve-test-eval/SKILL.md +78 -0
- package/doc/skills/keve-test-eval/references/common-fix-guide.md +166 -0
- package/doc/skills/keve-test-eval/references/confidence-data-guide.md +78 -0
- package/doc/skills/keve-test-eval/references/fix-loop-guide.md +67 -0
- package/doc/skills/keve-unit-gen/SKILL.md +203 -0
- package/doc/skills/keve-unit-gen/references/boundary-conditions.md +78 -0
- package/doc/skills/keve-unit-gen/references/unit-mock-strategy.md +71 -0
- package/doc/skills/keve-unit-gen/references/unit-test-template.md +105 -0
- package/package.json +47 -25
- package/dist/agent/factory.d.ts +0 -25
- package/dist/agent/factory.js +0 -99
- package/dist/agent/index.d.ts +0 -3
- package/dist/agent/index.js +0 -7
- package/dist/agent/loader.d.ts +0 -22
- package/dist/agent/loader.js +0 -117
- package/dist/agent/types.d.ts +0 -9
- package/dist/agent/types.js +0 -2
- package/dist/agent-loop/complete-task.d.ts +0 -28
- package/dist/agent-loop/complete-task.js +0 -70
- package/dist/agent-loop/index.d.ts +0 -16
- package/dist/agent-loop/index.js +0 -26
- package/dist/agent-loop/loop.d.ts +0 -42
- package/dist/agent-loop/loop.js +0 -282
- package/dist/agent-loop/runtime.d.ts +0 -40
- package/dist/agent-loop/runtime.js +0 -163
- package/dist/agent-loop/types.d.ts +0 -161
- package/dist/agent-loop/types.js +0 -105
- package/dist/context/index.d.ts +0 -8
- package/dist/context/index.js +0 -29
- package/dist/context/tokenCalculation.d.ts +0 -55
- package/dist/context/tokenCalculation.js +0 -95
- package/dist/context/truncation.d.ts +0 -67
- package/dist/context/truncation.js +0 -142
- package/dist/context/types.d.ts +0 -7
- package/dist/context/types.js +0 -27
- package/dist/llm/OpenAIClient.d.ts +0 -31
- package/dist/llm/OpenAIClient.js +0 -455
- package/dist/llm/constants.d.ts +0 -2
- package/dist/llm/constants.js +0 -6
- package/dist/llm/errors.d.ts +0 -25
- package/dist/llm/errors.js +0 -51
- package/dist/llm/index.d.ts +0 -49
- package/dist/llm/index.js +0 -158
- package/dist/llm/types.d.ts +0 -124
- package/dist/llm/types.js +0 -2
- package/dist/llm/utils.d.ts +0 -29
- package/dist/llm/utils.js +0 -284
- package/dist/skill/frontmatter-parser.d.ts +0 -50
- package/dist/skill/frontmatter-parser.js +0 -152
- package/dist/skill/index.d.ts +0 -12
- package/dist/skill/index.js +0 -17
- package/dist/skill/loader.d.ts +0 -96
- package/dist/skill/loader.js +0 -256
- package/dist/tools/ask-user.d.ts +0 -8
- package/dist/tools/ask-user.js +0 -47
- package/dist/tools/bash.d.ts +0 -47
- package/dist/tools/bash.js +0 -213
- package/dist/tools/edit-file.d.ts +0 -9
- package/dist/tools/edit-file.js +0 -76
- package/dist/tools/glob.d.ts +0 -8
- package/dist/tools/glob.js +0 -101
- package/dist/tools/grep.d.ts +0 -10
- package/dist/tools/grep.js +0 -76
- package/dist/tools/index.d.ts +0 -24
- package/dist/tools/index.js +0 -47
- package/dist/tools/read-files.d.ts +0 -8
- package/dist/tools/read-files.js +0 -81
- package/dist/tools/write-file.d.ts +0 -8
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* keve-test — Playwright test fixture: keveGoal (唯一操作原语)
|
|
4
|
+
*
|
|
5
|
+
* keveGoal 执行 fn 后 → 截图+fn逻辑+expected → AI判断是否一致
|
|
6
|
+
* 一致 → 通过
|
|
7
|
+
* 不一致 → AI 告诉你还差什么 → Re-Act 探索 → 探索成功
|
|
8
|
+
* fn 无 → Re-Act 直接探索
|
|
9
|
+
* fn 异常 → Re-Act 自愈
|
|
10
|
+
*
|
|
11
|
+
* keveAssert 已被吸收:keveGoal 的 expected 即断言语义
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* import { test, expect } from '@ks-data/keve-suite/keve-test';
|
|
15
|
+
*
|
|
16
|
+
* test('my test', async ({ page, keveGoal }) => {
|
|
17
|
+
* await keveGoal({ step: '导航到列表页', expected: '页面加载完成' }, async () => {
|
|
18
|
+
* await page.goto('/agents');
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // AI explore (no fn):
|
|
22
|
+
* await keveGoal({ step: '点击新建按钮', expected: '弹出创建对话框' });
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
28
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
29
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
30
|
+
}
|
|
31
|
+
Object.defineProperty(o, k2, desc);
|
|
32
|
+
}) : (function(o, m, k, k2) {
|
|
33
|
+
if (k2 === undefined) k2 = k;
|
|
34
|
+
o[k2] = m[k];
|
|
35
|
+
}));
|
|
36
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
37
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
38
|
+
}) : function(o, v) {
|
|
39
|
+
o["default"] = v;
|
|
40
|
+
});
|
|
41
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
42
|
+
var ownKeys = function(o) {
|
|
43
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
44
|
+
var ar = [];
|
|
45
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
46
|
+
return ar;
|
|
47
|
+
};
|
|
48
|
+
return ownKeys(o);
|
|
49
|
+
};
|
|
50
|
+
return function (mod) {
|
|
51
|
+
if (mod && mod.__esModule) return mod;
|
|
52
|
+
var result = {};
|
|
53
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
54
|
+
__setModuleDefault(result, mod);
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
})();
|
|
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;
|
|
60
|
+
const test_1 = require("@playwright/test");
|
|
61
|
+
Object.defineProperty(exports, "expect", { enumerable: true, get: function () { return test_1.expect; } });
|
|
62
|
+
const keve_registry_1 = require("./keve-registry");
|
|
63
|
+
const agent_1 = require("../page-agent/agent");
|
|
64
|
+
const keve_aspect_1 = require("./keve-aspect");
|
|
65
|
+
const learned_actions_1 = require("./learned-actions");
|
|
66
|
+
const fs = __importStar(require("node:fs"));
|
|
67
|
+
const path = __importStar(require("node:path"));
|
|
68
|
+
// ─── CDP Mode ──────────────────────────────────────────────────────
|
|
69
|
+
// CDP 连接逻辑已移到 global-setup.ts,通过全局变量共享 Browser 对象
|
|
70
|
+
const global_setup_1 = require("./global-setup");
|
|
71
|
+
var keve_decorators_1 = require("./keve-decorators");
|
|
72
|
+
Object.defineProperty(exports, "keveModel", { enumerable: true, get: function () { return keve_decorators_1.keveModel; } });
|
|
73
|
+
Object.defineProperty(exports, "keveScene", { enumerable: true, get: function () { return keve_decorators_1.keveScene; } });
|
|
74
|
+
Object.defineProperty(exports, "getModelScenes", { enumerable: true, get: function () { return keve_decorators_1.getModelScenes; } });
|
|
75
|
+
var keve_registry_2 = require("./keve-registry");
|
|
76
|
+
Object.defineProperty(exports, "sceneCodeMap", { enumerable: true, get: function () { return keve_registry_2.sceneCodeMap; } });
|
|
77
|
+
Object.defineProperty(exports, "sceneEvalMetaMap", { enumerable: true, get: function () { return keve_registry_2.sceneEvalMetaMap; } });
|
|
78
|
+
// Re-export goal metadata for consumer convenience
|
|
79
|
+
var keve_registry_3 = require("./keve-registry");
|
|
80
|
+
Object.defineProperty(exports, "sceneGoalsMap", { enumerable: true, get: function () { return keve_registry_3.sceneGoalsMap; } });
|
|
81
|
+
// ─── Register Built-in Aspects ───────────────────────────────────────
|
|
82
|
+
// NOTE: action-log-write aspect 已废弃。
|
|
83
|
+
// AI 探索过程数据现在通过 keveGoalResult attachment 直接流到 KeveReporter,
|
|
84
|
+
// 由 KeveReporter 统一写入 confidence-data.jsonl(含 reactSteps)。
|
|
85
|
+
keve_aspect_1.keveAspect.register({
|
|
86
|
+
name: 'learnedActions-update',
|
|
87
|
+
phase: 'after',
|
|
88
|
+
order: 20,
|
|
89
|
+
async execute(ctx, result) {
|
|
90
|
+
if (!result?.actions?.length || !result.success)
|
|
91
|
+
return;
|
|
92
|
+
learned_actions_1.learnedActions.add(ctx.step, result.actions);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
// ─── (diagnostic extraction moved to page-agent/diagnostic.ts, called via onAfterTask hook) ──
|
|
96
|
+
// ─── Fixture ────────────────────────────────────────────────────────
|
|
97
|
+
exports.test = test_1.test.extend({
|
|
98
|
+
browser: async ({}, use, testInfo) => {
|
|
99
|
+
if ((0, global_setup_1.isCdpMode)()) {
|
|
100
|
+
const browser = await (0, global_setup_1.getCdpBrowser)(); // 复用全局共享的 CDP browser
|
|
101
|
+
await use(browser);
|
|
102
|
+
// CDP 模式下不关闭浏览器(共享连接,关闭会影响后续用例)
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const browser = await test_1.chromium.launch(testInfo.project?.use?.launchOptions || {});
|
|
106
|
+
await use(browser);
|
|
107
|
+
await browser.close();
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
page: async ({ browser }, use) => {
|
|
111
|
+
if ((0, global_setup_1.isCdpMode)()) {
|
|
112
|
+
const contexts = browser.contexts();
|
|
113
|
+
const cdpContext = contexts.length > 0 ? contexts[0] : await browser.newContext();
|
|
114
|
+
// CDP 模式:优先复用已有 page(保留登录状态),仅在无可用 page 时新开
|
|
115
|
+
const existingPages = cdpContext.pages();
|
|
116
|
+
let page;
|
|
117
|
+
if (existingPages.length > 0) {
|
|
118
|
+
// 复用第一个非 about:blank 的 page(通常是用户已登录的 tab)
|
|
119
|
+
page = existingPages.find(p => p.url() !== 'about:blank') || existingPages[0];
|
|
120
|
+
// 导航到 about:blank 作为起始页(清空之前的状态,但保留 cookie)
|
|
121
|
+
try {
|
|
122
|
+
await page.goto('about:blank', { timeout: 3000 });
|
|
123
|
+
}
|
|
124
|
+
catch { /* ignore */ }
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
page = await cdpContext.newPage();
|
|
128
|
+
}
|
|
129
|
+
await use(page);
|
|
130
|
+
// CDP 模式下不关闭 page(保留浏览器 tab 给下一个用例复用)
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const page = await browser.newPage();
|
|
134
|
+
await use(page);
|
|
135
|
+
await page.close();
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
// ── keveGoal: 唯一操作原语 ────────────────────────────────────────
|
|
139
|
+
keveGoal: async ({ page }, use, testInfo) => {
|
|
140
|
+
let goalOrder = 0;
|
|
141
|
+
const keveGoalFn = async (options, fn) => {
|
|
142
|
+
const goalMeta = {
|
|
143
|
+
precondition: options.precondition,
|
|
144
|
+
step: options.step,
|
|
145
|
+
expected: options.expected,
|
|
146
|
+
order: goalOrder++,
|
|
147
|
+
};
|
|
148
|
+
const sceneTitle = testInfo.title;
|
|
149
|
+
if (!keve_registry_1.sceneGoalsMap.has(sceneTitle))
|
|
150
|
+
keve_registry_1.sceneGoalsMap.set(sceneTitle, []);
|
|
151
|
+
keve_registry_1.sceneGoalsMap.get(sceneTitle).push(goalMeta);
|
|
152
|
+
const ctx = {
|
|
153
|
+
page,
|
|
154
|
+
step: options.step,
|
|
155
|
+
expected: options.expected,
|
|
156
|
+
precondition: options.precondition,
|
|
157
|
+
order: goalMeta.order,
|
|
158
|
+
testTitle: sceneTitle,
|
|
159
|
+
specFilePath: testInfo.file,
|
|
160
|
+
};
|
|
161
|
+
// Run before aspects
|
|
162
|
+
await keve_aspect_1.keveAspect.runPhase('before', ctx);
|
|
163
|
+
let result;
|
|
164
|
+
const fnSource = fn?.toString() || undefined;
|
|
165
|
+
let fnError;
|
|
166
|
+
if (fn) {
|
|
167
|
+
try {
|
|
168
|
+
await fn();
|
|
169
|
+
console.log(`[keveGoal] "${options.step}" fn executed`);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
fnError = err?.message || String(err);
|
|
173
|
+
console.log(`[keveGoal] "${options.step}" fn error: ${fnError}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// ── Capture "before" screenshot (page state before agent starts) ──
|
|
177
|
+
let goalScreenshotBefore = '';
|
|
178
|
+
try {
|
|
179
|
+
const buf = await page.screenshot({ type: 'png', timeout: 15000 });
|
|
180
|
+
const taskDir = process.env.KEVE_TASK_DIR || '.keve';
|
|
181
|
+
const round = process.env.KEVE_ROUND || 'latest';
|
|
182
|
+
const screenshotsDir = path.join(taskDir, 'test-artifacts', `round-${round}`, 'screenshots');
|
|
183
|
+
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
184
|
+
const safeName = options.step.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '_').slice(0, 30);
|
|
185
|
+
const file = path.join(screenshotsDir, `goal-before-${goalMeta.order}-${safeName}-${Date.now()}.png`);
|
|
186
|
+
fs.writeFileSync(file, buf);
|
|
187
|
+
goalScreenshotBefore = path.relative(taskDir, file);
|
|
188
|
+
}
|
|
189
|
+
catch { /* non-critical */ }
|
|
190
|
+
// ── Hand off to agent: agent does Re-Act ──
|
|
191
|
+
const learnedHint = learned_actions_1.learnedActions.getHint(options.step);
|
|
192
|
+
let reactResult;
|
|
193
|
+
let reactTimedOut = false;
|
|
194
|
+
// ── Per-goal timeout: prevent one slow goal from exhausting the test timeout ──
|
|
195
|
+
// Default 120s per goal; total test timeout should be ≥ (goals × 120s + overhead)
|
|
196
|
+
const GOAL_TIMEOUT_MS = 120_000;
|
|
197
|
+
const goalTimeoutController = new AbortController();
|
|
198
|
+
const goalTimeout = setTimeout(() => {
|
|
199
|
+
goalTimeoutController.abort();
|
|
200
|
+
console.log(`[keveGoal] ⏰ "${options.step}" timed out after ${GOAL_TIMEOUT_MS}ms — stopping agent`);
|
|
201
|
+
}, GOAL_TIMEOUT_MS);
|
|
202
|
+
try {
|
|
203
|
+
reactResult = await (0, agent_1.reactLoop)(page, options.step, options.expected, {
|
|
204
|
+
learnedActionsHint: learnedHint,
|
|
205
|
+
specFilePath: ctx.specFilePath,
|
|
206
|
+
fnSource,
|
|
207
|
+
fnResult: fnError ? { error: fnError } : { success: true },
|
|
208
|
+
signal: goalTimeoutController.signal,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch (reactErr) {
|
|
212
|
+
reactTimedOut = true;
|
|
213
|
+
const isGoalTimeout = goalTimeoutController.signal.aborted;
|
|
214
|
+
const msg = isGoalTimeout
|
|
215
|
+
? `Goal timed out (${GOAL_TIMEOUT_MS}ms)`
|
|
216
|
+
: (reactErr?.message || String(reactErr));
|
|
217
|
+
console.log(`[keveGoal] reactLoop interrupted: ${msg.slice(0, 200)}`);
|
|
218
|
+
reactResult = {
|
|
219
|
+
expectedMet: false,
|
|
220
|
+
actions: [],
|
|
221
|
+
finalSnapshot: '',
|
|
222
|
+
agentScreenshots: [],
|
|
223
|
+
conclusion: isGoalTimeout ? 'blocked' : 'fail',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
clearTimeout(goalTimeout);
|
|
228
|
+
}
|
|
229
|
+
result = {
|
|
230
|
+
success: reactResult.expectedMet,
|
|
231
|
+
actions: reactResult.actions,
|
|
232
|
+
finalSnapshot: reactResult.finalSnapshot,
|
|
233
|
+
error: reactResult.expectedMet
|
|
234
|
+
? undefined
|
|
235
|
+
: reactResult.conclusion === 'blocked'
|
|
236
|
+
? new Error(`Blocked: ${reactTimedOut ? `Agent 超时 (${GOAL_TIMEOUT_MS}ms)` : (reactResult.actions?.filter((a) => a.action?.tool === 'done').pop()?.action?.text || 'Agent blocked')}`)
|
|
237
|
+
: new Error(`Expected not achieved: ${options.expected}`),
|
|
238
|
+
};
|
|
239
|
+
// Attach Agent conclusion for downstream consumers
|
|
240
|
+
if (reactResult.conclusion)
|
|
241
|
+
result.conclusion = reactResult.conclusion;
|
|
242
|
+
console.log(reactResult.expectedMet
|
|
243
|
+
? `[keveGoal] ✅ "${options.step}" PASSED`
|
|
244
|
+
: `[keveGoal] ❌ "${options.step}" FAILED (${reactResult.conclusion || 'fail'}) — ${result.error?.message || 'expected not achieved'}`);
|
|
245
|
+
// if (reactResult.refinePatch) (result as any).refinePatch = reactResult.refinePatch; // 已注释:scriptRefine 已禁用
|
|
246
|
+
if (reactResult.agentScreenshots)
|
|
247
|
+
result.agentScreenshots = reactResult.agentScreenshots;
|
|
248
|
+
// ── 核心:在 keveGoal 内部、throw 之前,写 attachment ──
|
|
249
|
+
// AI 探索的完整数据(actions、diagnosticHint)
|
|
250
|
+
// 通过 Playwright attachment 流到 KeveReporter.onTestEnd
|
|
251
|
+
// 注意:screenshotBase64 不再写入 attachment(截图已保存到文件系统,路径见 goalScreenshotBefore/goalScreenshotAfter)
|
|
252
|
+
const diagnosticHints = reactResult.diagnosticHints || [];
|
|
253
|
+
let goalScreenshotAfter = '';
|
|
254
|
+
try {
|
|
255
|
+
const buf = await page.screenshot({ type: 'png', timeout: 15000 });
|
|
256
|
+
const taskDir = process.env.KEVE_TASK_DIR || '.keve';
|
|
257
|
+
const round = process.env.KEVE_ROUND || 'latest';
|
|
258
|
+
const screenshotsDir = path.join(taskDir, 'test-artifacts', `round-${round}`, 'screenshots');
|
|
259
|
+
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
260
|
+
const safeName = options.step.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '_').slice(0, 30);
|
|
261
|
+
const file = path.join(screenshotsDir, `goal-after-${goalMeta.order}-${safeName}-${Date.now()}.png`);
|
|
262
|
+
fs.writeFileSync(file, buf);
|
|
263
|
+
goalScreenshotAfter = path.relative(taskDir, file);
|
|
264
|
+
console.log(`[keveGoal] screenshot captured: ${buf.length} bytes (path: ${goalScreenshotAfter})`);
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
console.log(`[keveGoal] screenshot failed: ${e.message}`);
|
|
268
|
+
}
|
|
269
|
+
await testInfo.attach('keveGoalResult', {
|
|
270
|
+
contentType: 'application/json',
|
|
271
|
+
body: Buffer.from(JSON.stringify({
|
|
272
|
+
step: options.step,
|
|
273
|
+
expected: options.expected,
|
|
274
|
+
precondition: options.precondition,
|
|
275
|
+
order: goalMeta.order,
|
|
276
|
+
success: result.success,
|
|
277
|
+
actions: (result.actions || []).map((a) => {
|
|
278
|
+
// Derive done action conclusion reliably (4-level backoff):
|
|
279
|
+
// - Level 1: step-level conclusion (from agent.ts, handles verdict/result/success/text)
|
|
280
|
+
// - Level 2: a.action?.verdict (new, no schema conflict)
|
|
281
|
+
// - Level 3: a.action?.result if it's a valid 3-state value
|
|
282
|
+
// - Level 4: a.action?.success (legacy boolean) or text parsing
|
|
283
|
+
// Do NOT use a.action?.result blindly (MacroTool schema conflict: "ok" ≠ "pass")
|
|
284
|
+
const actionTool = a.action?.tool;
|
|
285
|
+
let actionConclusion;
|
|
286
|
+
if (actionTool === 'done') {
|
|
287
|
+
if (result.conclusion) {
|
|
288
|
+
actionConclusion = result.conclusion;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const verdict = a.action?.verdict;
|
|
292
|
+
if (verdict === 'pass' || verdict === 'fail' || verdict === 'blocked') {
|
|
293
|
+
actionConclusion = verdict;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const raw = a.action?.result;
|
|
297
|
+
if (raw === 'pass' || raw === 'fail' || raw === 'blocked')
|
|
298
|
+
actionConclusion = raw;
|
|
299
|
+
else if (typeof a.action?.success === 'boolean')
|
|
300
|
+
actionConclusion = a.action.success ? 'pass' : 'fail';
|
|
301
|
+
else {
|
|
302
|
+
// Level 4: parse text for verdict keyword (includes match, not exact)
|
|
303
|
+
// IMPORTANT: Check fail/blocked BEFORE pass to avoid "不通过" hitting "通过"
|
|
304
|
+
const tv = String(a.action?.text || '').trim().toLowerCase();
|
|
305
|
+
if (tv.includes('fail') || tv.includes('failure')
|
|
306
|
+
|| tv.includes('失败') || tv.includes('未通过') || tv.includes('不通过'))
|
|
307
|
+
actionConclusion = 'fail';
|
|
308
|
+
else if (tv.includes('blocked') || tv.includes('阻塞') || tv.includes('阻止'))
|
|
309
|
+
actionConclusion = 'blocked';
|
|
310
|
+
else if (tv.includes('pass') || tv.includes('success') || tv.includes('ok')
|
|
311
|
+
|| tv.includes('完成') || tv.includes('成功') || tv.includes('通过') || tv.includes('验证通过'))
|
|
312
|
+
actionConclusion = 'pass';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
tool: actionTool,
|
|
319
|
+
role: a.action?.role,
|
|
320
|
+
name: a.action?.name,
|
|
321
|
+
url: a.action?.url,
|
|
322
|
+
text: a.action?.text,
|
|
323
|
+
toolOutput: a.toolOutput?.slice(0, 500),
|
|
324
|
+
reason: a.action?.reason,
|
|
325
|
+
success: a.action?.success,
|
|
326
|
+
// Agent 3-state verdict (pass/fail/blocked) — new field, no schema conflict
|
|
327
|
+
verdict: a.action?.verdict,
|
|
328
|
+
// Agent 3-state conclusion (pass/fail/blocked) — reliably derived
|
|
329
|
+
conclusion: actionConclusion,
|
|
330
|
+
result: a.result,
|
|
331
|
+
error: a.error,
|
|
332
|
+
evaluation: a.evaluation?.slice(0, 300),
|
|
333
|
+
memory: a.memory?.slice(0, 200),
|
|
334
|
+
nextGoal: a.nextGoal?.slice(0, 200),
|
|
335
|
+
screenshotPath: a.screenshotPath,
|
|
336
|
+
};
|
|
337
|
+
}),
|
|
338
|
+
finalSnapshot: result.finalSnapshot ? String(result.finalSnapshot).slice(0, 500) : undefined,
|
|
339
|
+
diagnosticHints,
|
|
340
|
+
goalScreenshotBefore: goalScreenshotBefore || undefined,
|
|
341
|
+
goalScreenshotAfter: goalScreenshotAfter || undefined,
|
|
342
|
+
agentScreenshots: result.agentScreenshots || [],
|
|
343
|
+
// refinePatch: (result as any).refinePatch || undefined, // 已注释:scriptRefine 已禁用
|
|
344
|
+
conclusion: result.conclusion || undefined,
|
|
345
|
+
}), 'utf-8'),
|
|
346
|
+
});
|
|
347
|
+
// Run after aspects
|
|
348
|
+
await keve_aspect_1.keveAspect.runPhase('after', ctx, result, result.error);
|
|
349
|
+
// Throw if not successful
|
|
350
|
+
if (!result.success) {
|
|
351
|
+
const failReason = result.error?.message || 'expected state not achieved';
|
|
352
|
+
console.log(`[keveGoal] ❌ "${options.step}" FAILED — reason: ${failReason}`);
|
|
353
|
+
throw result.error || new Error(`keveGoal "${options.step}" failed: ${failReason}`);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
await use(keveGoalFn);
|
|
357
|
+
// keveGoals attachment 已删除:step/expected/precondition/order 已合并进
|
|
358
|
+
// keveGoalResult attachment,不再单独输出 keveGoals 附件
|
|
359
|
+
// sceneGoalsMap 保留供内部分类使用
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
// Register extended test with keve-decorators so @keveModel uses the correct test
|
|
363
|
+
const keve_decorators_2 = require("./keve-decorators");
|
|
364
|
+
(0, keve_decorators_2.setKeveTest)(exports.test);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* learned-actions — In-memory cache for same-round Re-Act discovery sharing
|
|
3
|
+
*
|
|
4
|
+
* When one keveGoal's Re-Act loop discovers a working action sequence,
|
|
5
|
+
* other goals in the same round can use those discoveries as hints.
|
|
6
|
+
*
|
|
7
|
+
* Key format: "${step}" — the step description
|
|
8
|
+
* Value: ActionLogEntry[] — the successful action sequence
|
|
9
|
+
*/
|
|
10
|
+
export declare class LearnedActions {
|
|
11
|
+
private cache;
|
|
12
|
+
/** Store discovered actions for a step */
|
|
13
|
+
add(step: string, actions: any[]): void;
|
|
14
|
+
/** Retrieve discovered actions for a step */
|
|
15
|
+
get(step: string): any[] | undefined;
|
|
16
|
+
/** Get a JSON hint string for injecting into Re-Act prompt */
|
|
17
|
+
getHint(step: string): string | undefined;
|
|
18
|
+
/** Get all cached entries */
|
|
19
|
+
getAll(): Map<string, any[]>;
|
|
20
|
+
/** Clear all cached entries (typically at round start) */
|
|
21
|
+
clear(): void;
|
|
22
|
+
}
|
|
23
|
+
/** Global singleton for same-round sharing */
|
|
24
|
+
export declare const learnedActions: LearnedActions;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* learned-actions — In-memory cache for same-round Re-Act discovery sharing
|
|
4
|
+
*
|
|
5
|
+
* When one keveGoal's Re-Act loop discovers a working action sequence,
|
|
6
|
+
* other goals in the same round can use those discoveries as hints.
|
|
7
|
+
*
|
|
8
|
+
* Key format: "${step}" — the step description
|
|
9
|
+
* Value: ActionLogEntry[] — the successful action sequence
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.learnedActions = exports.LearnedActions = void 0;
|
|
13
|
+
class LearnedActions {
|
|
14
|
+
cache = new Map();
|
|
15
|
+
/** Store discovered actions for a step */
|
|
16
|
+
add(step, actions) {
|
|
17
|
+
// Only store successful action sequences (ending with expected met)
|
|
18
|
+
if (actions.length > 0) {
|
|
19
|
+
this.cache.set(step, actions);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Retrieve discovered actions for a step */
|
|
23
|
+
get(step) {
|
|
24
|
+
return this.cache.get(step);
|
|
25
|
+
}
|
|
26
|
+
/** Get a JSON hint string for injecting into Re-Act prompt */
|
|
27
|
+
getHint(step) {
|
|
28
|
+
const actions = this.cache.get(step);
|
|
29
|
+
if (!actions || actions.length === 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
// Summarize: only include action type + role/name, not full snapshot
|
|
32
|
+
const summary = actions
|
|
33
|
+
.filter(a => a.action.tool !== 'done')
|
|
34
|
+
.map(a => {
|
|
35
|
+
const act = a.action;
|
|
36
|
+
if (act.tool === 'navigate')
|
|
37
|
+
return `navigate(${act.url})`;
|
|
38
|
+
if (act.tool === 'click')
|
|
39
|
+
return `click(${act.role} "${act.name}")`;
|
|
40
|
+
if (act.tool === 'type')
|
|
41
|
+
return `type(${act.role} "${act.name}", "${act.text}"${act.submit ? ', Enter' : ''})`;
|
|
42
|
+
if (act.tool === 'hover')
|
|
43
|
+
return `hover(${act.role} "${act.name}")`;
|
|
44
|
+
if (act.tool === 'pressKey')
|
|
45
|
+
return `pressKey(${act.key})`;
|
|
46
|
+
if (act.tool === 'scroll')
|
|
47
|
+
return `scroll(${act.direction})`;
|
|
48
|
+
if (act.tool === 'wait')
|
|
49
|
+
return `wait(${act.time}ms)`;
|
|
50
|
+
return act.tool;
|
|
51
|
+
});
|
|
52
|
+
return `Previous successful actions for "${step}": ${summary.join(' → ')}`;
|
|
53
|
+
}
|
|
54
|
+
/** Get all cached entries */
|
|
55
|
+
getAll() {
|
|
56
|
+
return this.cache;
|
|
57
|
+
}
|
|
58
|
+
/** Clear all cached entries (typically at round start) */
|
|
59
|
+
clear() {
|
|
60
|
+
this.cache.clear();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.LearnedActions = LearnedActions;
|
|
64
|
+
/** Global singleton for same-round sharing */
|
|
65
|
+
exports.learnedActions = new LearnedActions();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright config template generator
|
|
3
|
+
*
|
|
4
|
+
* Shared by `keve config`, `keve init`, and pipeline agents
|
|
5
|
+
* to produce consistent keve-test.config.{ts,mjs} files.
|
|
6
|
+
*/
|
|
7
|
+
export interface PwConfigOptions {
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
cdp?: boolean;
|
|
10
|
+
cdpEndpoint?: string;
|
|
11
|
+
webServer?: boolean;
|
|
12
|
+
workers?: number;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
retries?: number;
|
|
15
|
+
format?: 'ts' | 'mjs';
|
|
16
|
+
}
|
|
17
|
+
export declare function generatePwConfig(options?: PwConfigOptions): string;
|
|
18
|
+
export declare function resolveConfigOutputPath(explicitOutput?: string, format?: 'ts' | 'mjs'): string;
|
|
19
|
+
export declare function findExistingConfig(keveDir: string | null, taskDir: string | null): string | null;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Playwright config template generator
|
|
4
|
+
*
|
|
5
|
+
* Shared by `keve config`, `keve init`, and pipeline agents
|
|
6
|
+
* to produce consistent keve-test.config.{ts,mjs} files.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.generatePwConfig = generatePwConfig;
|
|
43
|
+
exports.resolveConfigOutputPath = resolveConfigOutputPath;
|
|
44
|
+
exports.findExistingConfig = findExistingConfig;
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
const config_1 = require("../config");
|
|
47
|
+
function generatePwConfig(options = {}) {
|
|
48
|
+
const { baseUrl, cdp = false, cdpEndpoint, webServer = !cdp, workers = 3, timeout = 1200000, retries = 0, format = 'ts', } = options;
|
|
49
|
+
const baseURLLine = baseUrl
|
|
50
|
+
? `process.env.BASE_URL || '${baseUrl}'`
|
|
51
|
+
: `process.env.BASE_URL || process.env.PWGEN_TARGET_URL || 'http://localhost:3000'`;
|
|
52
|
+
const cdpBlock = cdp
|
|
53
|
+
? `\n // CDP: connect to running Chrome (preserves login state)\n // Set KEVE_CDP_ENDPOINT=http://[::1]:9222 at runtime\n // Or: KEVE_CDP_WS_ENDPOINT=ws://...\n connectOptions: ${cdpEndpoint ? `{ endpoint: '${cdpEndpoint}' }` : `process.env.KEVE_CDP_ENDPOINT ? { endpoint: process.env.KEVE_CDP_ENDPOINT } : undefined`},`
|
|
54
|
+
: '';
|
|
55
|
+
const webServerBlock = webServer
|
|
56
|
+
? `\n webServer: {\n command: 'pnpm dev',\n url: ${baseUrl ? `'${baseUrl}'` : `'http://localhost:3000'`},\n reuseExistingServer: true,\n },`
|
|
57
|
+
: `\n // webServer disabled — CDP mode assumes app is already running`;
|
|
58
|
+
if (format === 'mjs') {
|
|
59
|
+
return generateMjsConfig({ baseURLLine, cdp, cdpBlock, webServerBlock, workers, timeout, retries });
|
|
60
|
+
}
|
|
61
|
+
return `import { defineConfig } from '@playwright/test';
|
|
62
|
+
import * as path from 'node:path';
|
|
63
|
+
|
|
64
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
65
|
+
|
|
66
|
+
// Task directory — set KEVE_TASK_DIR env at execution time
|
|
67
|
+
// e.g. KEVE_TASK_DIR=.keve/task_xxx npx playwright test --config=.keve/task_xxx/keve-test.config.ts
|
|
68
|
+
const taskDir = process.env.KEVE_TASK_DIR
|
|
69
|
+
? path.resolve(projectRoot, process.env.KEVE_TASK_DIR)
|
|
70
|
+
: path.resolve(projectRoot, '.keve/default');
|
|
71
|
+
|
|
72
|
+
const round = process.env.KEVE_ROUND || 'latest';
|
|
73
|
+
const resultDir = path.join(taskDir, 'test-artifacts', \`round-\${round}\`);
|
|
74
|
+
|
|
75
|
+
export default defineConfig({
|
|
76
|
+
globalSetup: require.resolve('@ks-data/keve-suite/global-setup'),
|
|
77
|
+
testDir: path.join(taskDir, 'keve_test_spec'),
|
|
78
|
+
fullyParallel: true,
|
|
79
|
+
workers: ${workers},
|
|
80
|
+
retries: ${retries},
|
|
81
|
+
timeout: ${timeout},
|
|
82
|
+
expect: { timeout: 10000 },
|
|
83
|
+
reporter: [
|
|
84
|
+
['list'],
|
|
85
|
+
['json', { outputFile: path.join(resultDir, 'test-results.json') }],
|
|
86
|
+
[require.resolve('@ks-data/keve-suite/keve-report')],
|
|
87
|
+
],
|
|
88
|
+
use: {
|
|
89
|
+
baseURL: ${baseURLLine},${cdp ? `\n // CDP mode: login state preserved via connected Chrome` : `\n storageState: path.resolve(projectRoot, '.auth/storage-state.json'),`}
|
|
90
|
+
trace: 'on-first-retry',
|
|
91
|
+
screenshot: 'only-on-failure',
|
|
92
|
+
video: 'retain-on-failure',
|
|
93
|
+
actionTimeout: 5000,${cdpBlock}
|
|
94
|
+
},
|
|
95
|
+
outputDir: path.join(resultDir, 'test-results'),${webServerBlock}
|
|
96
|
+
});
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
function generateMjsConfig(params) {
|
|
100
|
+
const { baseURLLine, cdp, cdpBlock, webServerBlock, workers, timeout, retries } = params;
|
|
101
|
+
const keveSuiteDir = `resolve(dirname(fileURLToPath(import.meta.url)), '..', 'node_modules', '@ks-data', 'keve-suite')`;
|
|
102
|
+
const globalSetupExpr = `resolve(${keveSuiteDir}, 'global-setup.js')`;
|
|
103
|
+
const keveReportExpr = `resolve(${keveSuiteDir}, 'keve-report.js')`;
|
|
104
|
+
return `import { defineConfig } from '@playwright/test';
|
|
105
|
+
import { resolve, join, dirname } from 'node:path';
|
|
106
|
+
import { fileURLToPath } from 'node:url';
|
|
107
|
+
|
|
108
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
109
|
+
const projectRoot = resolve(__dirname, '..');
|
|
110
|
+
|
|
111
|
+
const taskDir = process.env.KEVE_TASK_DIR
|
|
112
|
+
? resolve(projectRoot, process.env.KEVE_TASK_DIR)
|
|
113
|
+
: resolve(projectRoot, '.keve/default');
|
|
114
|
+
|
|
115
|
+
process.env.KEVE_TASK_DIR = taskDir;
|
|
116
|
+
|
|
117
|
+
const round = process.env.KEVE_ROUND || 'latest';
|
|
118
|
+
const resultDir = join(taskDir, 'test-artifacts', \`round-\${round}\`);
|
|
119
|
+
|
|
120
|
+
const globalSetup = ${globalSetupExpr};
|
|
121
|
+
const keveReporter = ${keveReportExpr};
|
|
122
|
+
|
|
123
|
+
export default defineConfig({
|
|
124
|
+
globalSetup,
|
|
125
|
+
testDir: join(taskDir, 'keve_test_spec'),
|
|
126
|
+
fullyParallel: true,
|
|
127
|
+
workers: ${workers},
|
|
128
|
+
retries: ${retries},
|
|
129
|
+
timeout: ${timeout},
|
|
130
|
+
expect: { timeout: 10000 },
|
|
131
|
+
reporter: [
|
|
132
|
+
['list'],
|
|
133
|
+
['json', { outputFile: join(resultDir, 'test-results.json') }],
|
|
134
|
+
[keveReporter],
|
|
135
|
+
],
|
|
136
|
+
use: {
|
|
137
|
+
baseURL: ${baseURLLine},${cdp ? `\n // CDP mode: login state preserved via connected Chrome` : `\n storageState: resolve(projectRoot, '.auth', 'storage-state.json'),`}
|
|
138
|
+
trace: 'on-first-retry',
|
|
139
|
+
screenshot: 'only-on-failure',
|
|
140
|
+
video: 'retain-on-failure',
|
|
141
|
+
actionTimeout: 5000,${cdpBlock}
|
|
142
|
+
},
|
|
143
|
+
outputDir: join(resultDir, 'test-results'),${webServerBlock}
|
|
144
|
+
});
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
function resolveConfigOutputPath(explicitOutput, format = 'ts') {
|
|
148
|
+
const ext = format === 'mjs' ? '.mjs' : '.ts';
|
|
149
|
+
if (explicitOutput)
|
|
150
|
+
return path.resolve(explicitOutput);
|
|
151
|
+
const keveDir = (0, config_1.findKeveDir)();
|
|
152
|
+
const taskDir = (0, config_1.resolveTaskDir)(keveDir);
|
|
153
|
+
if (taskDir)
|
|
154
|
+
return path.join(taskDir, `keve-test.config${ext}`);
|
|
155
|
+
if (keveDir) {
|
|
156
|
+
const branchDir = (0, config_1.getBranchDir)(keveDir);
|
|
157
|
+
if (branchDir)
|
|
158
|
+
return path.join(branchDir, `keve-test.config${ext}`);
|
|
159
|
+
return path.join(keveDir, `keve-test.config${ext}`);
|
|
160
|
+
}
|
|
161
|
+
return path.join(process.cwd(), 'keve_test_spec', `keve-test.config${ext}`);
|
|
162
|
+
}
|
|
163
|
+
function findExistingConfig(keveDir, taskDir) {
|
|
164
|
+
const searchPaths = [];
|
|
165
|
+
if (taskDir) {
|
|
166
|
+
searchPaths.push(path.join(taskDir, 'keve-test.config.mjs'));
|
|
167
|
+
searchPaths.push(path.join(taskDir, 'keve-test.config.ts'));
|
|
168
|
+
}
|
|
169
|
+
if (keveDir) {
|
|
170
|
+
searchPaths.push(path.join(keveDir, 'keve-test.config.mjs'));
|
|
171
|
+
searchPaths.push(path.join(keveDir, 'keve-test.config.ts'));
|
|
172
|
+
}
|
|
173
|
+
searchPaths.push(path.join(process.cwd(), 'keve_test_spec', 'keve-test.config.mjs'));
|
|
174
|
+
searchPaths.push(path.join(process.cwd(), 'keve_test_spec', 'keve-test.config.ts'));
|
|
175
|
+
for (const p of searchPaths) {
|
|
176
|
+
if (require('fs').existsSync(p))
|
|
177
|
+
return p;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|