@cotestdev/ai-runner 0.0.4 → 0.0.5
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/package.json +6 -1
- package/playwright.config.ts +0 -38
- package/src/agents/logger.ts +0 -20
- package/src/agents/playwright-executor.ts +0 -252
- package/src/agents/tools/playwright-backend-adapter.ts +0 -134
- package/src/agents/tools/playwright-mcp-types.d.ts +0 -71
- package/src/agents/types.ts +0 -80
- package/src/index.ts +0 -27
- package/src/runner.ts +0 -224
- package/src/tools/index.ts +0 -48
- package/src/tools/playwright-groups.ts +0 -54
- package/src/types/external.ts +0 -7
- package/src/types/index.ts +0 -118
- package/tests/agent/test-heal-agent.spec.ts +0 -54
- package/tsconfig.json +0 -26
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cotestdev/ai-runner",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "AI-powered self-healing SDK for Playwright test scripts",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
7
12
|
"scripts": {
|
|
8
13
|
"build": "tsc",
|
|
9
14
|
"watch": "tsc --watch",
|
package/playwright.config.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Playwright configuration file
|
|
5
|
-
*/
|
|
6
|
-
export default defineConfig({
|
|
7
|
-
testDir: './',
|
|
8
|
-
testMatch: [
|
|
9
|
-
'**/*.spec.ts',
|
|
10
|
-
'**/*.test.ts'
|
|
11
|
-
],
|
|
12
|
-
fullyParallel: false,
|
|
13
|
-
forbidOnly: !!process.env.CI,
|
|
14
|
-
retries: 0,
|
|
15
|
-
workers: 1,
|
|
16
|
-
reporter: [
|
|
17
|
-
['list'],
|
|
18
|
-
['html', { open: 'never', outputFolder: 'playwright-report' }]
|
|
19
|
-
],
|
|
20
|
-
use: {
|
|
21
|
-
baseURL: 'https://example.com',
|
|
22
|
-
trace: 'retain-on-failure',
|
|
23
|
-
screenshot: 'only-on-failure',
|
|
24
|
-
video: 'retain-on-failure',
|
|
25
|
-
// Default to headless mode for convenience debugging
|
|
26
|
-
// Can be controlled via HEADED=true environment variable
|
|
27
|
-
headless: false,
|
|
28
|
-
},
|
|
29
|
-
projects: [
|
|
30
|
-
{
|
|
31
|
-
name: 'chromium',
|
|
32
|
-
use: {
|
|
33
|
-
...devices['Desktop Chrome'],
|
|
34
|
-
// Use slow motion in debug mode
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
],
|
|
38
|
-
});
|
package/src/agents/logger.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
export class Logger {
|
|
3
|
-
|
|
4
|
-
private prefix = '[Agent]';
|
|
5
|
-
|
|
6
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
-
log(message: string, ...optionalParams: any[]) {
|
|
8
|
-
console.log(`${this.prefix} ${message}`, ...optionalParams);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
-
warn(message: string, ...optionalParams: any[]) {
|
|
13
|
-
console.warn(`${this.prefix} ${message}`, ...optionalParams);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
-
error(message: string, ...optionalParams: any[]) {
|
|
18
|
-
console.error(`${this.prefix} ${message}`, ...optionalParams);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Playwright Executor - Uses LangChain's createAgent API to implement ReAct loop
|
|
3
|
-
*/
|
|
4
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
5
|
-
|
|
6
|
-
import { createAgent } from 'langchain';
|
|
7
|
-
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
8
|
-
import type { HealContext } from '../types';
|
|
9
|
-
import z from 'zod';
|
|
10
|
-
import { PlaywrightAgentState } from './types';
|
|
11
|
-
|
|
12
|
-
export class PlaywrightExecutor {
|
|
13
|
-
private state: PlaywrightAgentState;
|
|
14
|
-
|
|
15
|
-
constructor(state: PlaywrightAgentState) {
|
|
16
|
-
this.state = state;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
private async createPlaywrightTools(healContext: HealContext): Promise<DynamicStructuredTool[]> {
|
|
20
|
-
const standardTools = this.state.playwrightGroup.getDefaultTools();
|
|
21
|
-
const playwrightToolGroups = this.state.playwrightGroup.getNonDefaultGroupInfo();
|
|
22
|
-
|
|
23
|
-
const getToolGroupsTool = {
|
|
24
|
-
name: "get_tools",
|
|
25
|
-
description: "Get available tools in a group.",
|
|
26
|
-
schema: z.object({
|
|
27
|
-
group_name: z.string().describe("The name of the tool group."),
|
|
28
|
-
}),
|
|
29
|
-
func: async (param: any) => {
|
|
30
|
-
if (!param.group_name) throw new Error('Missing group_name parameter');
|
|
31
|
-
return JSON.stringify(playwrightToolGroups[param.group_name]);
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const callToolTool = {
|
|
36
|
-
name: "call_tool",
|
|
37
|
-
description: "Call a tool in a group by name.",
|
|
38
|
-
schema: z.object({
|
|
39
|
-
group_name: z.string().describe("Tool group name."),
|
|
40
|
-
tool_name: z.string().describe("Tool name."),
|
|
41
|
-
args: z.object().describe("Tool parameters."),
|
|
42
|
-
}),
|
|
43
|
-
func: async (param: any) => {
|
|
44
|
-
const { group_name, tool_name, args } = param;
|
|
45
|
-
if (!group_name || !tool_name) throw new Error('Missing group_name or tool_name parameter');
|
|
46
|
-
return await this.state.playwrightGroup.callTool(tool_name, args);
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const finishTool = {
|
|
51
|
-
name: "finish_test",
|
|
52
|
-
description: "Call this tool ONLY when the step goal has been fully achieved or failed. After calling this, no more tools should be called.",
|
|
53
|
-
schema: z.object({
|
|
54
|
-
isSuccess: z.boolean().describe("Whether the step goal was successfully achieved."),
|
|
55
|
-
error: z.string().optional().describe("Reason if the step goal fails to fully achieve, otherwise leave empty."),
|
|
56
|
-
variables: z.record(z.string(), z.any()).describe(`
|
|
57
|
-
Key-value variables to assign in the test context to use in subsequent steps.
|
|
58
|
-
- The variable names should be consistent with those in the original test script
|
|
59
|
-
- Avoid conflicts with existing variable names
|
|
60
|
-
- Only include variables that have been changed or newly assigned in the fix
|
|
61
|
-
- The value should be NON-OBJECT types (string, number, boolean, etc.)
|
|
62
|
-
- If subsequent steps get a variable using 'runner.get', make sure to extract and provide the ACTUAL VALUE from test or page context
|
|
63
|
-
`),
|
|
64
|
-
repairedScript: z.string().optional().describe(`
|
|
65
|
-
The complete fixed test script, leave empty if the step fails:
|
|
66
|
-
- Keep original runner.set, runner.get, runner.reuseTest calls for the steps fixed if possible and ensure no semantic changes
|
|
67
|
-
- MUST return The COMPLETE fixed test script
|
|
68
|
-
- ONLY apply fixes relevant to the current step and any necessary previous steps
|
|
69
|
-
- Do NOT wrap in closures or test body - write direct code using runner API
|
|
70
|
-
- Add a 'FIXED' comment for the step code being fixed
|
|
71
|
-
- Example:
|
|
72
|
-
\`\`\`typescript
|
|
73
|
-
// FIXED
|
|
74
|
-
await runner.runStep('Click button', async () => {
|
|
75
|
-
await page.click('#button');
|
|
76
|
-
});
|
|
77
|
-
\`\`\`
|
|
78
|
-
`),
|
|
79
|
-
}),
|
|
80
|
-
func: async (params: any) => {
|
|
81
|
-
healContext.success = params.isSuccess;
|
|
82
|
-
healContext.healError = params.error;
|
|
83
|
-
if (healContext.success) {
|
|
84
|
-
if (params.variables) {
|
|
85
|
-
for (const [key, value] of Object.entries(params.variables)) {
|
|
86
|
-
this.state.variables.set(key, value);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
this.state.originalScript = params.repairedScript;
|
|
91
|
-
this.updateOriginalScript(params.repairedScript);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return 'Test step finished.';
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
return [
|
|
99
|
-
...standardTools,
|
|
100
|
-
new DynamicStructuredTool(getToolGroupsTool),
|
|
101
|
-
new DynamicStructuredTool(callToolTool),
|
|
102
|
-
new DynamicStructuredTool(finishTool)
|
|
103
|
-
];
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Update original script and save to database (heal mode)
|
|
108
|
-
*/
|
|
109
|
-
private updateOriginalScript(script: string): void {
|
|
110
|
-
this.state.storageService.saveTestScript(this.state.testCaseId, script);
|
|
111
|
-
this.state.logger.log('Script fixed');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async execute(context: HealContext) {
|
|
115
|
-
const page = this.state.page;
|
|
116
|
-
if (!page) throw new Error('Missing page object');
|
|
117
|
-
|
|
118
|
-
const tools = await this.createPlaywrightTools(context);
|
|
119
|
-
const agent = createAgent({
|
|
120
|
-
model: this.state.model!,
|
|
121
|
-
tools: tools,
|
|
122
|
-
systemPrompt: this.buildSystemPrompt()
|
|
123
|
-
} as any);
|
|
124
|
-
|
|
125
|
-
const inputStream = {
|
|
126
|
-
messages: [{
|
|
127
|
-
role: 'user' as const,
|
|
128
|
-
content: await this.buildUserMessage(context)
|
|
129
|
-
}]
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const stream = await agent.stream(inputStream, {
|
|
133
|
-
recursionLimit: 25,
|
|
134
|
-
streamMode: 'updates',
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
for await (const event of stream) {
|
|
138
|
-
// Process AI messages (thoughts and tool calls)
|
|
139
|
-
const messages = event.model_request?.messages || event.messages;
|
|
140
|
-
if (messages?.[0]?.type === 'ai') {
|
|
141
|
-
const aiMsg = messages[0] as any;
|
|
142
|
-
|
|
143
|
-
const content = this.extractContent(aiMsg.content);
|
|
144
|
-
// Record thought
|
|
145
|
-
if (content) {
|
|
146
|
-
this.state.logger.log(`${content}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Process tool results
|
|
151
|
-
const toolMsg = event.tools?.messages?.[0];
|
|
152
|
-
if (toolMsg?.type === 'tool') {
|
|
153
|
-
const output = this.extractContent(toolMsg.content) as string;
|
|
154
|
-
this.state.logger.log(`✅ Result [${toolMsg.name}]:`, output.length > 300 ? output.substring(0, 100) + '...' : output);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private buildSystemPrompt(): string {
|
|
160
|
-
return `
|
|
161
|
-
You are a Playwright test expert, skilled at analyzing and fixing errors in browser automation tests.
|
|
162
|
-
|
|
163
|
-
## ** Critial Rules **
|
|
164
|
-
- You MUST run the step using tools first before fixing the script
|
|
165
|
-
- When you step finishes or fails, you MUST call 'finish_test' tool and set the 'isSuccess' parameter correctly.
|
|
166
|
-
- Use tools squentially
|
|
167
|
-
- Analyze the current step code, errors and page context and what variables are being set
|
|
168
|
-
- Make sure the fixed code is concise and consistent with the original code in logic and style
|
|
169
|
-
- CRITICAL: When calling 'finish_test', analyze the full test script to identify variables defined in the current step that are used in subsequent steps
|
|
170
|
-
- IMPORTANT: Extract the ACTUAL VALUES of those variables from your tool execution results (browser_snapshot output, element text, attributes, etc.)
|
|
171
|
-
- Include the extracted variable values in the 'variables' parameter of finish_test
|
|
172
|
-
|
|
173
|
-
## ** Constraints **
|
|
174
|
-
- NEVER set 'isSuccess' to true if the step goal is not fully achieved
|
|
175
|
-
- Fail the step fast and DON'T do unnecessary or unreansonable retries
|
|
176
|
-
- Always check if variables assigned in the current step are referenced in later steps - if so, extract their actual values and include in 'variables'
|
|
177
|
-
`.trim();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
private async buildUserMessage(context: HealContext): Promise<string> {
|
|
181
|
-
const variableList = Object.keys(this.state.variables).length > 0
|
|
182
|
-
? Object.keys(this.state.variables).map(v => ` - ${v}: ${JSON.stringify(this.state.variables.get(v))}`).join('\n')
|
|
183
|
-
: undefined;
|
|
184
|
-
const snapshot = await this.state.adapter.callTool('browser_snapshot', {});
|
|
185
|
-
const pageState = snapshot.isError ? '' : this.extractContent(snapshot);
|
|
186
|
-
|
|
187
|
-
return `
|
|
188
|
-
## Step Description: ${context.stepDescription}
|
|
189
|
-
|
|
190
|
-
${variableList ? `## Variables in Context\n${variableList}` : ''}
|
|
191
|
-
|
|
192
|
-
## Stacktrace
|
|
193
|
-
${context.stackTrace}
|
|
194
|
-
|
|
195
|
-
## Full Test Script
|
|
196
|
-
\`\`\`typescript
|
|
197
|
-
${this.state.originalScript}
|
|
198
|
-
\`\`\`
|
|
199
|
-
|
|
200
|
-
${pageState}
|
|
201
|
-
}
|
|
202
|
-
`.trim();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Extract and normalize content from model response
|
|
207
|
-
* Handles string, array (ContentBlock[]), and other types
|
|
208
|
-
* Following LangChain.js best practices
|
|
209
|
-
*/
|
|
210
|
-
private extractContent(content: unknown): string | Record<string, unknown> {
|
|
211
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
-
const asAny = content as any;
|
|
213
|
-
if (asAny.content) {
|
|
214
|
-
content = asAny.content;
|
|
215
|
-
}
|
|
216
|
-
// If string, return as-is
|
|
217
|
-
if (typeof content === 'string') {
|
|
218
|
-
return content;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// If array (ContentBlock[]), extract text content
|
|
222
|
-
if (Array.isArray(content)) {
|
|
223
|
-
const content0 = content[0];
|
|
224
|
-
if (typeof content0 === 'string') {
|
|
225
|
-
return content0;
|
|
226
|
-
}
|
|
227
|
-
if (typeof content0 === 'object' && content0 !== null) {
|
|
228
|
-
// Handle text blocks
|
|
229
|
-
if ('text' in content0) {
|
|
230
|
-
return content0.text as Record<string, unknown>;
|
|
231
|
-
}
|
|
232
|
-
// Handle other block types by converting to string
|
|
233
|
-
return JSON.stringify(content0);
|
|
234
|
-
}
|
|
235
|
-
return String(content0);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// If object, try to extract text field or convert to string
|
|
239
|
-
if (typeof content === 'object' && content !== null) {
|
|
240
|
-
``
|
|
241
|
-
if ('text' in content) {
|
|
242
|
-
return content as Record<string, unknown>;
|
|
243
|
-
}
|
|
244
|
-
return content as Record<string, unknown>;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Fallback: convert to string
|
|
248
|
-
return String(content);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Playwright Backend 工具适配器 - 新方案
|
|
3
|
-
*
|
|
4
|
-
* 基于 BrowserServerBackend 的伪实例实现
|
|
5
|
-
* 参考:playwright/lib/mcp/test/browserBackend.js
|
|
6
|
-
*
|
|
7
|
-
* 这个方案的优势:
|
|
8
|
-
* 1. 直接使用 Playwright 官方的 BrowserServerBackend 类
|
|
9
|
-
* 2. 直接导入官方的 identityFactory,完全复用
|
|
10
|
-
* 3. 获得完整的官方工具功能,包括 Response、Context 等
|
|
11
|
-
* 4. 无需手动适配 Context 和 Response
|
|
12
|
-
* 5. 自动跟随 Playwright 更新
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
16
|
-
import type { BrowserContext } from 'playwright';
|
|
17
|
-
import * as path from 'path';
|
|
18
|
-
|
|
19
|
-
// 导入 Playwright MCP 模块的类型定义
|
|
20
|
-
import type { PlaywrightMCPModules, BrowserServerBackendInstance } from './playwright-mcp-types';
|
|
21
|
-
|
|
22
|
-
export class PlaywrightBackendAdapter {
|
|
23
|
-
|
|
24
|
-
private backend: BrowserServerBackendInstance | undefined;
|
|
25
|
-
private tools: DynamicStructuredTool[] = [];
|
|
26
|
-
|
|
27
|
-
static async create(browserContext: BrowserContext): Promise<PlaywrightBackendAdapter> {
|
|
28
|
-
const adapter = new PlaywrightBackendAdapter();
|
|
29
|
-
adapter.backend = await adapter.getBackend(browserContext);
|
|
30
|
-
adapter.tools = await adapter.getPlaywrightBackendToolsFromContext();
|
|
31
|
-
return adapter;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
getTools(): DynamicStructuredTool[] {
|
|
35
|
-
return this.tools;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
-
async callTool(name: string, params: any) {
|
|
40
|
-
return await this.backend!.callTool(name, params);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ============================================================================
|
|
44
|
-
// 动态加载 Playwright 官方模块
|
|
45
|
-
// ============================================================================
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* 官方的 identityFactory 函数
|
|
49
|
-
* 参考:playwright/lib/mcp/test/browserBackend.js 第 94-104 行
|
|
50
|
-
*
|
|
51
|
-
* 注意:官方文件中定义了这个函数但没有导出,所以我们在这里复制一份
|
|
52
|
-
*/
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
-
private identityFactory(browserContext: BrowserContext): any {
|
|
55
|
-
return {
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
-
createContext: async (clientInfo: any, abortSignal: any) => {
|
|
58
|
-
void clientInfo;
|
|
59
|
-
void abortSignal;
|
|
60
|
-
return {
|
|
61
|
-
browserContext,
|
|
62
|
-
close: async () => {
|
|
63
|
-
// 不关闭 context,因为它是外部的
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private loadPlaywrightMCPModules(): PlaywrightMCPModules {
|
|
71
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
|
-
const playwrightRoot = path.dirname((require as any).resolve('playwright'));
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
|
-
BrowserServerBackend: (require as any)(path.join(playwrightRoot, 'lib', 'mcp', 'browser', 'browserServerBackend.js')).BrowserServerBackend,
|
|
77
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
-
defaultConfig: (require as any)(path.join(playwrightRoot, 'lib', 'mcp', 'browser', 'config.js')).defaultConfig,
|
|
79
|
-
// 使用本地定义的 identityFactory(因为官方没有导出)
|
|
80
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
-
identityFactory: this.identityFactory as any,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private async getBackend(browserContext: BrowserContext): Promise<BrowserServerBackendInstance> {
|
|
86
|
-
// 加载 Playwright 模块(包括官方的 identityFactory)
|
|
87
|
-
const { BrowserServerBackend, defaultConfig, identityFactory } = this.loadPlaywrightMCPModules();
|
|
88
|
-
|
|
89
|
-
// 使用官方的 identityFactory(参考 browserBackend.js 第 34 行)
|
|
90
|
-
const factory = identityFactory(browserContext);
|
|
91
|
-
|
|
92
|
-
// 创建 Backend 实例
|
|
93
|
-
const backend = new BrowserServerBackend(
|
|
94
|
-
{ ...defaultConfig, capabilities: ['core', 'core-tabs',] },
|
|
95
|
-
factory
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
// 初始化 Backend
|
|
99
|
-
await backend.initialize({
|
|
100
|
-
name: 'ai-runner',
|
|
101
|
-
version: '1.0.0'
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
return backend;
|
|
105
|
-
}
|
|
106
|
-
// ============================================================================
|
|
107
|
-
// 工具转换函数
|
|
108
|
-
// ============================================================================
|
|
109
|
-
|
|
110
|
-
// export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf' | 'testing' | 'tracing';
|
|
111
|
-
|
|
112
|
-
private async getPlaywrightBackendToolsFromContext(): Promise<DynamicStructuredTool[]> {
|
|
113
|
-
// 列出所有工具
|
|
114
|
-
const toolList = await this.backend!.listTools();
|
|
115
|
-
|
|
116
|
-
// 转换为 LangChain 工具
|
|
117
|
-
const tools: DynamicStructuredTool[] = [];
|
|
118
|
-
|
|
119
|
-
for (const mcpTool of toolList) {
|
|
120
|
-
const tool = new DynamicStructuredTool({
|
|
121
|
-
name: mcpTool.name,
|
|
122
|
-
description: mcpTool.description,
|
|
123
|
-
schema: mcpTool.inputSchema,
|
|
124
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
125
|
-
func: async (input: any) => {
|
|
126
|
-
return await this.backend!.callTool(mcpTool.name, input);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
tools.push(tool);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return tools;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Playwright MCP 模块类型定义
|
|
3
|
-
*
|
|
4
|
-
* 为动态加载的 Playwright 内部模块提供 TypeScript 类型支持
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { BrowserContext } from 'playwright';
|
|
8
|
-
|
|
9
|
-
export interface BrowserContextFactory {
|
|
10
|
-
createContext: (
|
|
11
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
-
clientInfo: any,
|
|
13
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
-
abortSignal: any,
|
|
15
|
-
toolName: string
|
|
16
|
-
) => Promise<{
|
|
17
|
-
browserContext: BrowserContext;
|
|
18
|
-
close: () => Promise<void>;
|
|
19
|
-
}>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface BackendConfig {
|
|
23
|
-
capabilities?: string[];
|
|
24
|
-
saveSession?: boolean;
|
|
25
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
-
[key: string]: any;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface BrowserServerBackendInstance {
|
|
30
|
-
initialize(clientInfo: { name: string; version: string }): Promise<void>;
|
|
31
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
-
listTools(): Promise<Array<{ name: string; description: string; inputSchema: any }>>;
|
|
33
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
-
callTool(name: string, params: any): Promise<any>;
|
|
35
|
-
serverClosed(): void;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface BrowserServerBackend {
|
|
39
|
-
new (config: BackendConfig, factory: BrowserContextFactory): BrowserServerBackendInstance;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface DefaultConfig {
|
|
43
|
-
capabilities?: string[];
|
|
44
|
-
saveSession?: boolean;
|
|
45
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
-
[key: string]: any;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 官方的 identityFactory 函数类型
|
|
51
|
-
* 参考:playwright/lib/mcp/test/browserBackend.js 第 94-104 行
|
|
52
|
-
*/
|
|
53
|
-
export type IdentityFactory = (browserContext: BrowserContext) => BrowserContextFactory;
|
|
54
|
-
|
|
55
|
-
export interface MCPTool {
|
|
56
|
-
schema: {
|
|
57
|
-
name: string;
|
|
58
|
-
description: string;
|
|
59
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
-
inputSchema: any;
|
|
61
|
-
};
|
|
62
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
-
handle: (context: any, params: any, response: any) => Promise<void>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 动态加载的模块类型
|
|
67
|
-
export interface PlaywrightMCPModules {
|
|
68
|
-
BrowserServerBackend: BrowserServerBackend;
|
|
69
|
-
defaultConfig: DefaultConfig;
|
|
70
|
-
identityFactory: IdentityFactory;
|
|
71
|
-
}
|
package/src/agents/types.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Playwright Agent Type Definitions
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { ModelConfig } from '../types/external';
|
|
6
|
-
import type { ReActStep } from '../types';
|
|
7
|
-
import { BrowserContext, Page } from '@playwright/test';
|
|
8
|
-
import { Logger } from './logger';
|
|
9
|
-
import { PlaywrightGroup } from '../tools/playwright-groups';
|
|
10
|
-
import { PlaywrightBackendAdapter } from './tools/playwright-backend-adapter';
|
|
11
|
-
import { StorageService } from '@cotestdev/core-infra';
|
|
12
|
-
import { BaseLanguageModel } from '@langchain/core/language_models/base';
|
|
13
|
-
|
|
14
|
-
// ============================================================================
|
|
15
|
-
// Agent Execution Mode
|
|
16
|
-
// ============================================================================
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Agent execution mode
|
|
20
|
-
*/
|
|
21
|
-
export type AgentMode = 'heal' | 'execute';
|
|
22
|
-
|
|
23
|
-
// ============================================================================
|
|
24
|
-
// Agent State
|
|
25
|
-
// ============================================================================
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Playwright Agent State
|
|
29
|
-
*/
|
|
30
|
-
export interface PlaywrightAgentState {
|
|
31
|
-
logger: Logger;
|
|
32
|
-
/** Playwright Page object */
|
|
33
|
-
page: Page;
|
|
34
|
-
/** Playwright BrowserContext object */
|
|
35
|
-
context: BrowserContext;
|
|
36
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
-
variables: Map<string, any>;
|
|
38
|
-
model?: BaseLanguageModel;
|
|
39
|
-
originalScript: string;
|
|
40
|
-
playwrightGroup: PlaywrightGroup;
|
|
41
|
-
adapter: PlaywrightBackendAdapter;
|
|
42
|
-
storageService: StorageService;
|
|
43
|
-
testCaseId: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ============================================================================
|
|
47
|
-
// Agent Configuration
|
|
48
|
-
// ============================================================================
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Playwright Agent Configuration
|
|
52
|
-
*/
|
|
53
|
-
export interface PlaywrightAgentConfig {
|
|
54
|
-
/** Model configuration (for LLM Agent) */
|
|
55
|
-
model: ModelConfig;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ============================================================================
|
|
59
|
-
// Agent Result
|
|
60
|
-
// ============================================================================
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Playwright Agent Execution Result
|
|
64
|
-
*/
|
|
65
|
-
export interface PlaywrightAgentResult {
|
|
66
|
-
/** Whether the goal was successfully completed */
|
|
67
|
-
success: boolean;
|
|
68
|
-
|
|
69
|
-
/** Agent's reasoning process */
|
|
70
|
-
reasoning?: string;
|
|
71
|
-
|
|
72
|
-
/** If failed, the error reason */
|
|
73
|
-
errorMessage?: string;
|
|
74
|
-
|
|
75
|
-
/** How many steps were executed */
|
|
76
|
-
steps?: number;
|
|
77
|
-
|
|
78
|
-
/** Detailed step records of ReAct loop */
|
|
79
|
-
reactSteps?: ReActStep[];
|
|
80
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ai-runner SDK Main Entry Point
|
|
3
|
-
* Provides AI-driven test execution capabilities
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// Types
|
|
7
|
-
export type {
|
|
8
|
-
RunnerConfig,
|
|
9
|
-
ExecutionResult,
|
|
10
|
-
HealContext,
|
|
11
|
-
HealSummary,
|
|
12
|
-
HealingDetail,
|
|
13
|
-
RunStepOptions,
|
|
14
|
-
} from './types';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// Core Classes
|
|
18
|
-
export { Runner } from './runner';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// Agent
|
|
22
|
-
export type {
|
|
23
|
-
PlaywrightAgentState,
|
|
24
|
-
PlaywrightAgentResult,
|
|
25
|
-
PlaywrightAgentConfig,
|
|
26
|
-
} from './agents/types';
|
|
27
|
-
|
package/src/runner.ts
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import type {
|
|
3
|
-
RunnerConfig,
|
|
4
|
-
HealContext,
|
|
5
|
-
} from './types';
|
|
6
|
-
import type { Page, BrowserContext } from 'playwright';
|
|
7
|
-
import { createModel, StorageService } from '@cotestdev/core-infra';
|
|
8
|
-
import { expect } from '@playwright/test';
|
|
9
|
-
import { PlaywrightAgentState } from './agents/types';
|
|
10
|
-
import { Logger } from './agents/logger';
|
|
11
|
-
import { PlaywrightExecutor } from './agents/playwright-executor';
|
|
12
|
-
import { PlaywrightGroup } from './tools/playwright-groups';
|
|
13
|
-
import { PlaywrightBackendAdapter } from './agents/tools/playwright-backend-adapter';
|
|
14
|
-
import { BaseLanguageModel } from '@langchain/core/language_models/base';
|
|
15
|
-
|
|
16
|
-
export class Runner {
|
|
17
|
-
private config: RunnerConfig;
|
|
18
|
-
private model?: BaseLanguageModel;
|
|
19
|
-
private storageService: StorageService;
|
|
20
|
-
private logger: Logger;
|
|
21
|
-
private canHeal: boolean = false;
|
|
22
|
-
private executor: PlaywrightExecutor | undefined;
|
|
23
|
-
private state: PlaywrightAgentState | undefined;
|
|
24
|
-
private script?: string;
|
|
25
|
-
private variables: Map<string, any> = new Map<string, any>();
|
|
26
|
-
|
|
27
|
-
private constructor(config: RunnerConfig) {
|
|
28
|
-
this.config = config;
|
|
29
|
-
this.storageService = StorageService.getInstance(config.projectId);
|
|
30
|
-
this.logger = new Logger();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
static NewInstance(projectId: string, testCaseId: string): Runner {
|
|
34
|
-
const runner = new Runner({ projectId, testCaseId });
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const script = runner.loadOriginalScript();
|
|
38
|
-
if (!script) {
|
|
39
|
-
runner.logger.error('No script found for the specified test case.');
|
|
40
|
-
return runner;
|
|
41
|
-
}
|
|
42
|
-
runner.script = script;
|
|
43
|
-
|
|
44
|
-
// 同步加载模型配置
|
|
45
|
-
const allConfigs = runner.storageService.listModelConfigsSync();
|
|
46
|
-
const config = allConfigs.find(c => c.isDefault === true);
|
|
47
|
-
if (config) {
|
|
48
|
-
runner.model = createModel(config);
|
|
49
|
-
runner.canHeal = true;
|
|
50
|
-
} else {
|
|
51
|
-
runner.logger.error('No default model configuration available in the system.');
|
|
52
|
-
}
|
|
53
|
-
} catch (error) {
|
|
54
|
-
runner.logger.error(`Failed to initialize runner: ${error}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return runner;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async init(page: Page, context: BrowserContext, data?: Record<string, any>): Promise<void> {
|
|
61
|
-
const adapter = await PlaywrightBackendAdapter.create(context);
|
|
62
|
-
this.state = {
|
|
63
|
-
page,
|
|
64
|
-
context,
|
|
65
|
-
variables: this.variables,
|
|
66
|
-
logger: this.logger,
|
|
67
|
-
model: this.model,
|
|
68
|
-
originalScript: this.state?.originalScript || this.script || '',
|
|
69
|
-
adapter: adapter,
|
|
70
|
-
playwrightGroup: new PlaywrightGroup(adapter),
|
|
71
|
-
testCaseId: this.config.testCaseId,
|
|
72
|
-
storageService: this.storageService,
|
|
73
|
-
};
|
|
74
|
-
if (data) {
|
|
75
|
-
for (const [key, value] of Object.entries(data)) {
|
|
76
|
-
this.set(key, value);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
this.executor = new PlaywrightExecutor(this.state);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private loadOriginalScript(): string {
|
|
83
|
-
if (!this.config.testCaseId) {
|
|
84
|
-
throw new Error('Cannot load original script: testCaseId is not provided');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
this.logger.log(`Attempting to load script from database: ${this.config.projectId}/${this.config.testCaseId}`);
|
|
88
|
-
const script = this.storageService.loadTestScript(this.config.testCaseId);
|
|
89
|
-
if (!script) {
|
|
90
|
-
throw new Error(`Cannot load original script: Test case not found in database (ID: ${this.config.testCaseId})`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return script;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
forEach(data: Record<string, any>[], callback: (index: number) => void) {
|
|
97
|
-
for (const [index, item] of data.entries()) {
|
|
98
|
-
for (const [key, value] of Object.entries(item)) {
|
|
99
|
-
this.set(key, value);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
callback(index);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async runStep(description: string, fn: () => Promise<void>) {
|
|
107
|
-
const startTime = Date.now();
|
|
108
|
-
const context: HealContext = {
|
|
109
|
-
stepDescription: description,
|
|
110
|
-
originalCode: fn.toString(),
|
|
111
|
-
success: false
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
await fn();
|
|
116
|
-
context.success = true;
|
|
117
|
-
return;
|
|
118
|
-
} catch (error) {
|
|
119
|
-
if (!this.canHeal || !(error instanceof Error)) {
|
|
120
|
-
throw error;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const healable = this.isHealableError(error);
|
|
124
|
-
if (!healable) {
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
context.error = (error as Error).message;
|
|
129
|
-
context.stackTrace = (error as Error).stack;
|
|
130
|
-
|
|
131
|
-
this.logger.error(`Step failed: ${description}, Error: ${context.error}`);
|
|
132
|
-
this.logger.log(`Agent will attempt to self-heal...`);
|
|
133
|
-
|
|
134
|
-
await this.executor!.execute(context);
|
|
135
|
-
|
|
136
|
-
const duration = (Date.now() - startTime) / 1000;
|
|
137
|
-
if (context.success) {
|
|
138
|
-
this.logger.log(`Test step healed, duration: ${duration}s`);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
this.logger.error(`Failed to fix step: ${context.healError}, duration: ${duration}s`);
|
|
143
|
-
throw new Error(`original error: ${context.error}; heal error: ${context.healError}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
private isHealableError(err: Error): boolean {
|
|
148
|
-
// 方式1:枚举所有 Symbol 属性(最常见尝试)
|
|
149
|
-
const symbols = Object.getOwnPropertySymbols(err);
|
|
150
|
-
|
|
151
|
-
for (const sym of symbols) {
|
|
152
|
-
try {
|
|
153
|
-
const value = (err as any)[sym];
|
|
154
|
-
|
|
155
|
-
// 如果有 category 字段,大概率就是我们要找的 step
|
|
156
|
-
if (value && typeof value === 'object' && 'category' in value) {
|
|
157
|
-
if (value.category === 'expect' || value.category === 'pw:api') {
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} catch (e) {
|
|
162
|
-
// 访问可能会抛错,忽略即可
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
set(key: string, value: any): void {
|
|
170
|
-
this.variables.set(key, value);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
get(key: string): any {
|
|
174
|
-
return this.variables.get(key);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
has(key: string): boolean {
|
|
178
|
-
return this.variables.has(key);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
delete(key: string): boolean {
|
|
182
|
-
return this.variables.delete(key);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
clear(): void {
|
|
186
|
-
this.variables.clear();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
keys(): string[] {
|
|
190
|
-
return Array.from(this.variables.keys());
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
getAll(): Record<string, unknown> {
|
|
194
|
-
return Object.fromEntries(this.variables);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async reuseTest(useTestCaseId: string, description: string): Promise<Map<string, unknown>> {
|
|
198
|
-
const scriptContent = this.storageService.loadTestScript(useTestCaseId);
|
|
199
|
-
if (!scriptContent) {
|
|
200
|
-
throw new Error(`Reusable test script not found: ${useTestCaseId}`);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const reuseRunner = Runner.NewInstance(this.config.projectId, useTestCaseId);
|
|
204
|
-
await reuseRunner.init(this.state!.page, this.state!.context);
|
|
205
|
-
|
|
206
|
-
this.logger.log(`Starting to execute reusable test case: ${description}`);
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const executeScript = new Function('runner', 'page', 'context', 'expect', `
|
|
210
|
-
return (async () => {
|
|
211
|
-
${scriptContent}
|
|
212
|
-
})();
|
|
213
|
-
`);
|
|
214
|
-
|
|
215
|
-
await executeScript(reuseRunner, this.state!.page, this.state!.context, expect);
|
|
216
|
-
|
|
217
|
-
this.logger.log(`Reusable test case executed successfully`);
|
|
218
|
-
|
|
219
|
-
return reuseRunner.state?.variables || new Map<string, unknown>();
|
|
220
|
-
} catch (error) {
|
|
221
|
-
throw new Error(`reuseTest execution failed, error: ${error instanceof Error ? error.message : String(error)}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
package/src/tools/index.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { DynamicStructuredTool } from "langchain";
|
|
2
|
-
|
|
3
|
-
export interface ToolGroup {
|
|
4
|
-
name: string;
|
|
5
|
-
description: string;
|
|
6
|
-
tools?: string[];
|
|
7
|
-
loadByDefault: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export abstract class AbstractGroup {
|
|
11
|
-
groups: ToolGroup[];
|
|
12
|
-
|
|
13
|
-
constructor(groups: ToolGroup[]) {
|
|
14
|
-
this.groups = groups;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
abstract getAllTools(): DynamicStructuredTool[];
|
|
18
|
-
|
|
19
|
-
getDefaultTools(): DynamicStructuredTool[] {
|
|
20
|
-
return this.getAllTools().filter(tool => {
|
|
21
|
-
return this.groups.some(group => group.loadByDefault && (!group.tools || group.tools.includes(tool.name)));
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
getAvailableTools(groupName: string): DynamicStructuredTool[] {
|
|
26
|
-
const group = this.groups.find(g => g.name === groupName);
|
|
27
|
-
if (group) {
|
|
28
|
-
const toolNames = group.tools;
|
|
29
|
-
const all = this.getAllTools();
|
|
30
|
-
if (!toolNames) {
|
|
31
|
-
return all;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return toolNames.filter(name => all.some(tool => tool.name === name)).map(name => all.find(tool => tool.name === name)!);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
throw new Error(`Tool group ${groupName} not found`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
getNonDefaultGroupInfo(): Record<string, string> {
|
|
41
|
-
return this.groups
|
|
42
|
-
.filter(group => !group.loadByDefault)
|
|
43
|
-
.reduce((acc, group) => {
|
|
44
|
-
acc[group.name] = group.description;
|
|
45
|
-
return acc;
|
|
46
|
-
}, {} as Record<string, string>);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
/**
|
|
3
|
-
* MCP (Model Context Protocol) Client for Playwright integration
|
|
4
|
-
* Integrates with https://github.com/microsoft/playwright-mcp
|
|
5
|
-
*/
|
|
6
|
-
import { DynamicStructuredTool } from 'langchain';
|
|
7
|
-
import { AbstractGroup } from '.';
|
|
8
|
-
import { PlaywrightBackendAdapter } from 'src/agents/tools/playwright-backend-adapter';
|
|
9
|
-
export interface PlaywrightMCPOpts {
|
|
10
|
-
language: string,
|
|
11
|
-
headless?: boolean,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class PlaywrightGroup extends AbstractGroup {
|
|
15
|
-
private allTools: DynamicStructuredTool[];
|
|
16
|
-
private adapter: PlaywrightBackendAdapter;
|
|
17
|
-
|
|
18
|
-
constructor(adapter: PlaywrightBackendAdapter) {
|
|
19
|
-
super([{
|
|
20
|
-
name: 'playwright_basic',
|
|
21
|
-
description: 'Basic Playwright tools for browser interactions',
|
|
22
|
-
tools: ['browser_navigate', 'browser_click', 'browser_fill_form', 'browser_press_key', 'browser_run_code', 'browser_select_option', 'browser_snapshot', 'browser_type', 'browser_wait_for'],
|
|
23
|
-
loadByDefault: true,
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
name: 'playwright_browser_manage',
|
|
27
|
-
description: `Playwright tools tab management, including: list, create, close, select a tab`,
|
|
28
|
-
tools: ['browser_tabs'],
|
|
29
|
-
loadByDefault: false,
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: 'playwright_other',
|
|
33
|
-
description: `Other Playwright tools for browser interactions, including: mouse hover on element, drag and drop an element, upload file, handle dialog, evaluate JavaScript on page or element
|
|
34
|
-
1. mouse hover on element
|
|
35
|
-
2. drag and drop an element
|
|
36
|
-
3. upload file
|
|
37
|
-
4. handle dialog
|
|
38
|
-
5. evaluate JavaScript on page or element`,
|
|
39
|
-
tools: ['browser_hover', 'browser_drag', 'browser_file_upload', 'browser_handle_dialog', 'browser_evaluate'],
|
|
40
|
-
loadByDefault: false,
|
|
41
|
-
}
|
|
42
|
-
]);
|
|
43
|
-
this.adapter = adapter;
|
|
44
|
-
this.allTools = this.adapter.getTools();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async callTool(toolName: string, args: any): Promise<any> {
|
|
48
|
-
return this.adapter.callTool(toolName, args);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
getAllTools(): DynamicStructuredTool[] {
|
|
52
|
-
return this.allTools
|
|
53
|
-
}
|
|
54
|
-
}
|
package/src/types/external.ts
DELETED
package/src/types/index.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ai-heal Core Type Definitions (MVP)
|
|
3
|
-
* Maintain simplicity, include only necessary types
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// Playwright types are used in type definitions below
|
|
7
|
-
|
|
8
|
-
export interface RunnerConfig {
|
|
9
|
-
/** Test Case ID */
|
|
10
|
-
testCaseId: string;
|
|
11
|
-
/** Project ID */
|
|
12
|
-
projectId: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// ============================================================================
|
|
16
|
-
// Execution Context
|
|
17
|
-
// ============================================================================
|
|
18
|
-
|
|
19
|
-
export interface RunStepOptions {
|
|
20
|
-
/** Step description for AI to understand */
|
|
21
|
-
description: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface ReActStep {
|
|
25
|
-
/** Step number */
|
|
26
|
-
stepNumber: number;
|
|
27
|
-
/** Step type */
|
|
28
|
-
stepType: 'thought' | 'action' | 'observation' | 'reflection';
|
|
29
|
-
/** Timestamp */
|
|
30
|
-
timestamp: number;
|
|
31
|
-
/** LLM thought process (thought step) */
|
|
32
|
-
thought?: string;
|
|
33
|
-
/** Tool call information (action step) */
|
|
34
|
-
toolCall?: {
|
|
35
|
-
name: string;
|
|
36
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
-
input: any;
|
|
38
|
-
};
|
|
39
|
-
/** Tool execution result (observation step) */
|
|
40
|
-
toolResult?: {
|
|
41
|
-
output: string;
|
|
42
|
-
success: boolean;
|
|
43
|
-
duration?: number;
|
|
44
|
-
};
|
|
45
|
-
/** Reflection content (reflection step) */
|
|
46
|
-
reflection?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Context information for a single self-healing process
|
|
51
|
-
*/
|
|
52
|
-
export interface HealContext {
|
|
53
|
-
/** Step description */
|
|
54
|
-
stepDescription: string;
|
|
55
|
-
/** original error */
|
|
56
|
-
error?: string;
|
|
57
|
-
/** Stack trace */
|
|
58
|
-
stackTrace?: string;
|
|
59
|
-
/** Original code */
|
|
60
|
-
originalCode: string;
|
|
61
|
-
/** Whether successful */
|
|
62
|
-
success: boolean;
|
|
63
|
-
/** Total steps executed by Agent */
|
|
64
|
-
agentSteps?: number;
|
|
65
|
-
healError?: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ============================================================================
|
|
69
|
-
// Execution Result
|
|
70
|
-
// ============================================================================
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Code execution result
|
|
74
|
-
*/
|
|
75
|
-
export interface ExecutionResult {
|
|
76
|
-
/** Whether successful */
|
|
77
|
-
success: boolean;
|
|
78
|
-
/** Return value */
|
|
79
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
|
-
returnValue: any;
|
|
81
|
-
/** Local variables */
|
|
82
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
-
locals: Record<string, any>;
|
|
84
|
-
/** Error message */
|
|
85
|
-
error?: Error;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ============================================================================
|
|
89
|
-
// Self-Healing Summary
|
|
90
|
-
// ============================================================================
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Self-healing summary information
|
|
94
|
-
*/
|
|
95
|
-
export interface HealSummary {
|
|
96
|
-
/** Total number of steps */
|
|
97
|
-
totalSteps: number;
|
|
98
|
-
/** Number of successfully healed steps */
|
|
99
|
-
healedSteps: number;
|
|
100
|
-
/** Fixed script */
|
|
101
|
-
modifiedScript?: string;
|
|
102
|
-
/** List of self-healing details */
|
|
103
|
-
healingDetails: HealingDetail[];
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Single self-healing detail
|
|
108
|
-
*/
|
|
109
|
-
export interface HealingDetail {
|
|
110
|
-
/** Step description */
|
|
111
|
-
step: string;
|
|
112
|
-
/** Error message */
|
|
113
|
-
error: string;
|
|
114
|
-
/** Recovery time */
|
|
115
|
-
recoveryTime: number;
|
|
116
|
-
/** Number of LLM interactions */
|
|
117
|
-
llmInteractions: number;
|
|
118
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { test, expect } from '@playwright/test';
|
|
3
|
-
import { Runner } from '@cotestdev/ai-runner';
|
|
4
|
-
|
|
5
|
-
test.describe(`Register user`, () => {
|
|
6
|
-
const runner = Runner.NewInstance('43af4db4-3249-4135-b279-97b24f27106b', '2e189c6f-6c2b-4086-8fe7-837bf1a6b1df');
|
|
7
|
-
|
|
8
|
-
test.afterEach(async () => {
|
|
9
|
-
runner.clear();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
[
|
|
13
|
-
{
|
|
14
|
-
"password": "abcd1234"
|
|
15
|
-
}
|
|
16
|
-
].forEach((data, index) => {
|
|
17
|
-
test(`#${index + 1}`, async ({ page, context }) => {
|
|
18
|
-
await runner.init(page, context, data);
|
|
19
|
-
|
|
20
|
-
await runner.runStep('进入注册页面', async () => {
|
|
21
|
-
await page.goto('https://practice.expandtesting.com/register');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
await runner.runStep('填写并提交注册信息,用户名使用唯一格式(如未提供则自动生成)', async () => {
|
|
25
|
-
// 获取测试参数中的用户名和密码,如未提供用户名则自动生成
|
|
26
|
-
let username: string;
|
|
27
|
-
if (runner.has('username')) {
|
|
28
|
-
username = runner.get('username');
|
|
29
|
-
} else {
|
|
30
|
-
const timestamp = Date.now();
|
|
31
|
-
username = `user${timestamp}`;
|
|
32
|
-
}
|
|
33
|
-
const password = runner.get('password') || 'abcd1234';
|
|
34
|
-
|
|
35
|
-
// 填写注册表单
|
|
36
|
-
await page.locator('input[type="text"]').first().fill(username);
|
|
37
|
-
await page.locator('input[type="password"]').first().fill(password);
|
|
38
|
-
await page.locator('input[type="password"]').nth(1).fill(password);
|
|
39
|
-
|
|
40
|
-
// 提交注册表单
|
|
41
|
-
await page.getByRole('button', { name: 'Register' }).click();
|
|
42
|
-
|
|
43
|
-
// 设置输出参数
|
|
44
|
-
runner.set('username', username);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
await runner.runStep('确认注册成功', async () => {
|
|
48
|
-
// 验证注册成功后跳转到登录页面或显示成功提示
|
|
49
|
-
await expect(page).toHaveURL(/login/);
|
|
50
|
-
await expect(page.locator('.alert-success')).toContainText('Registration successful');
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "commonjs",
|
|
5
|
-
"lib": ["ES2020"],
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"declarationMap": true,
|
|
8
|
-
"sourceMap": true,
|
|
9
|
-
"outDir": "./dist",
|
|
10
|
-
"strict": true,
|
|
11
|
-
"esModuleInterop": true,
|
|
12
|
-
"skipLibCheck": true,
|
|
13
|
-
"forceConsistentCasingInFileNames": true,
|
|
14
|
-
"resolveJsonModule": true,
|
|
15
|
-
"moduleResolution": "node",
|
|
16
|
-
"allowSyntheticDefaultImports": true,
|
|
17
|
-
"types": ["node", "@playwright/test"],
|
|
18
|
-
"baseUrl": ".",
|
|
19
|
-
"paths": {
|
|
20
|
-
"@cotestdev/core-infra": ["../core-infra/src"],
|
|
21
|
-
"@cotestdev/core-infra/*": ["../core-infra/src/*"]
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
"include": ["src/**/*"],
|
|
25
|
-
"exclude": ["node_modules", "dist", "tests"]
|
|
26
|
-
}
|