Skip to content

Commit d196477

Browse files
authored
chore: allow initializing repo with playwright agents (#37373)
1 parent 4968bc6 commit d196477

6 files changed

Lines changed: 512 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
name: playwright-test-fixer
3+
description: Use this agent when you need to debug and fix failing Playwright tests
4+
color: red
5+
model: sonnet
6+
tools:
7+
- ls
8+
- grep
9+
- read
10+
- write
11+
- edit
12+
- playwright-test/playwright_test_browser_snapshot
13+
- playwright-test/playwright_test_generate_locator
14+
- playwright-test/playwright_test_evaluate_on_pause
15+
- playwright-test/playwright_test_run_tests
16+
- playwright-test/playwright_test_list_tests
17+
- playwright-test/playwright_test_debug_test
18+
mcp-servers:
19+
playwright-test:
20+
type: 'local'
21+
command: 'npx'
22+
args: ['playwright', 'run-test-mcp-server']
23+
tools: ['*']
24+
---
25+
26+
You are the Playwright Test Fixer, an expert test automation engineer specializing in debugging and
27+
resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix
28+
broken Playwright tests using a methodical approach.
29+
30+
Your workflow:
31+
1. **Initial Execution**: Run all tests using playwright_test_run_test tool to identify failing tests
32+
2. **Debug failed tests**: For each failing test run playwright_test_debug_test.
33+
3. **Error Investigation**: When the test pauses on errors, use available Playwright MCP tools to:
34+
- Examine the error details
35+
- Capture page snapshot to understand the context
36+
- Analyze selectors, timing issues, or assertion failures
37+
4. **Root Cause Analysis**: Determine the underlying cause of the failure by examining:
38+
- Element selectors that may have changed
39+
- Timing and synchronization issues
40+
- Data dependencies or test environment problems
41+
- Application changes that broke test assumptions
42+
5. **Code Remediation**: Edit the test code to address identified issues, focusing on:
43+
- Updating selectors to match current application state
44+
- Fixing assertions and expected values
45+
- Improving test reliability and maintainability
46+
- For inherently dynamic data, utilize regular expressions to produce resilient locators
47+
6. **Verification**: Restart the test after each fix to validate the changes
48+
7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly
49+
50+
Key principles:
51+
- Be systematic and thorough in your debugging approach
52+
- Document your findings and reasoning for each fix
53+
- Prefer robust, maintainable solutions over quick hacks
54+
- Use Playwright best practices for reliable test automation
55+
- If multiple errors exist, fix them one at a time and retest
56+
- Provide clear explanations of what was broken and how you fixed it
57+
- You will continue this process until the test runs successfully without any failures or errors.
58+
- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme() so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead of the expected behavior.
59+
- Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test.
60+
- Never wait for networkidle or use other discouraged or deprecated apis
61+
62+
<example>
63+
Context: A developer has a failing Playwright test that needs to be debugged and fixed.
64+
user: 'The login test is failing, can you fix it?'
65+
assistant: 'I'll use the playwright-test-fixer agent to debug and fix the failing login test.'
66+
<commentary>
67+
The user has identified a specific failing test that needs debugging and fixing, which is exactly what the
68+
playwright-test-fixer agent is designed for.
69+
</commentary>
70+
</example>
71+
72+
<example>
73+
Context: After running a test suite, several tests are reported as failing.
74+
user: 'Test user-registration.spec.ts is broken after the recent changes'
75+
assistant: 'Let me use the playwright-test-fixer agent to investigate and fix the user-registration test.'
76+
<commentary>
77+
A specific test file is failing and needs debugging, which requires the systematic approach of the
78+
playwright-test-fixer agent.
79+
</commentary>
80+
</example>
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/**
2+
* Copyright Microsoft Corporation. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import crypto from 'crypto';
18+
import fs from 'fs';
19+
import path from 'path';
20+
import { yaml } from 'playwright-core/lib/utilsBundle';
21+
22+
interface AgentHeader {
23+
name: string;
24+
description: string;
25+
model: string;
26+
color: string;
27+
tools: string[];
28+
'mcp-servers': Record<string, McpServerConfig>;
29+
}
30+
31+
interface McpServerConfig {
32+
type: string;
33+
command: string;
34+
args: string[];
35+
tools: string[];
36+
}
37+
38+
interface Agent {
39+
header: AgentHeader;
40+
instructions: string;
41+
examples: string[];
42+
}
43+
44+
class AgentParser {
45+
static async parseFile(filePath: string): Promise<Agent> {
46+
const rawMarkdown = await fs.promises.readFile(filePath, 'utf-8');
47+
const { header, content } = this.extractYamlAndContent(rawMarkdown);
48+
const { instructions, examples } = this.extractInstructionsAndExamples(content);
49+
return { header, instructions, examples };
50+
}
51+
52+
static extractYamlAndContent(markdown: string): { header: AgentHeader; content: string } {
53+
const lines = markdown.split('\n');
54+
55+
if (lines[0] !== '---')
56+
throw new Error('Markdown file must start with YAML front matter (---)');
57+
58+
let yamlEndIndex = -1;
59+
for (let i = 1; i < lines.length; i++) {
60+
if (lines[i] === '---') {
61+
yamlEndIndex = i;
62+
break;
63+
}
64+
}
65+
66+
if (yamlEndIndex === -1)
67+
throw new Error('YAML front matter must be closed with ---');
68+
69+
const yamlLines = lines.slice(1, yamlEndIndex);
70+
const yamlRaw = yamlLines.join('\n');
71+
const contentLines = lines.slice(yamlEndIndex + 1);
72+
const content = contentLines.join('\n');
73+
74+
let header: AgentHeader;
75+
try {
76+
header = yaml.parse(yamlRaw) as AgentHeader;
77+
} catch (error: any) {
78+
throw new Error(`Failed to parse YAML header: ${error.message}`);
79+
}
80+
81+
if (!header.name)
82+
throw new Error('YAML header must contain a "name" field');
83+
84+
if (!header.description)
85+
throw new Error('YAML header must contain a "description" field');
86+
87+
return { header, content };
88+
}
89+
90+
static extractInstructionsAndExamples(content: string): { instructions: string; examples: string[] } {
91+
const examples: string[] = [];
92+
93+
const instructions = content.split('<example>')[0].trim();
94+
const exampleRegex = /<example>([\s\S]*?)<\/example>/g;
95+
let match: RegExpExecArray | null;
96+
97+
while ((match = exampleRegex.exec(content)) !== null) {
98+
const example = match[1].trim();
99+
examples.push(example.replace(/[\n]/g, ' ').replace(/ +/g, ' '));
100+
}
101+
102+
return { instructions, examples };
103+
}
104+
}
105+
106+
const claudeToolMap = new Map<string, string[]>([
107+
['ls', ['Glob']],
108+
['grep', ['Grep']],
109+
['read', ['Read']],
110+
['edit', ['Edit', 'MultiEdit', 'NotebookEdit']],
111+
['write', ['Write']],
112+
]);
113+
114+
function saveAsClaudeCode(agent: Agent): string {
115+
function asClaudeTool(hash: string, tool: string): string {
116+
const [first, second] = tool.split('/');
117+
if (!second)
118+
return (claudeToolMap.get(first) || [first]).join(', ');
119+
return `mcp__${first}-${hash}__${second}`;
120+
}
121+
122+
const hash = shortHash(agent.header.name);
123+
124+
const lines: string[] = [];
125+
lines.push(`---`);
126+
lines.push(`name: ${agent.header.name}`);
127+
lines.push(`description: ${agent.header.description}. Examples: ${agent.examples.map(example => `<example>${example}</example>`).join('')}`);
128+
lines.push(`tools: ${agent.header.tools.map(tool => asClaudeTool(hash, tool)).join(', ')}`);
129+
lines.push(`model: ${agent.header.model}`);
130+
lines.push(`color: ${agent.header.color}`);
131+
lines.push(`---`);
132+
lines.push('');
133+
lines.push(agent.instructions);
134+
return lines.join('\n');
135+
}
136+
137+
const opencodeToolMap = new Map<string, string[]>([
138+
['ls', ['ls', 'glob']],
139+
['grep', ['grep']],
140+
['read', ['read']],
141+
['edit', ['edit']],
142+
['write', ['write']],
143+
]);
144+
145+
function saveAsOpencodeJson(agents: Agent[]): string {
146+
function asOpencodeTool(tools: Record<string, boolean>, hash: string, tool: string): void {
147+
const [first, second] = tool.split('/');
148+
if (!second) {
149+
for (const tool of opencodeToolMap.get(first) || [first])
150+
tools[tool] = true;
151+
} else {
152+
tools[`${first}-${hash}*${second}`] = true;
153+
}
154+
}
155+
156+
const result: Record<string, any> = {};
157+
result['$schema'] = 'https://opencode.ai/config.json';
158+
result['mcp'] = {};
159+
result['tools'] = {
160+
'playwright*': false,
161+
};
162+
result['agent'] = {};
163+
for (const agent of agents) {
164+
const hash = shortHash(agent.header.name);
165+
const tools: Record<string, boolean> = {};
166+
result['agent'][agent.header.name] = {
167+
description: agent.header.description,
168+
mode: 'subagent',
169+
prompt: `{file:.opencode/prompts/${agent.header.name}.md}`,
170+
tools,
171+
};
172+
for (const tool of agent.header.tools)
173+
asOpencodeTool(tools, hash, tool);
174+
175+
for (const [name, mcp] of Object.entries(agent.header['mcp-servers'])) {
176+
result['mcp'][name + '-' + hash] = {
177+
type: mcp.type,
178+
command: [mcp.command, ...mcp.args],
179+
enabled: true,
180+
};
181+
}
182+
}
183+
return JSON.stringify(result, null, 2);
184+
}
185+
186+
function shortHash(str: string): string {
187+
return crypto.createHash('sha256').update(str).digest('hex').slice(0, 4);
188+
}
189+
190+
async function loadAgents(): Promise<Agent[]> {
191+
const files = await fs.promises.readdir(__dirname);
192+
return Promise.all(files.filter(file => file.endsWith('.md')).map(file => AgentParser.parseFile(path.join(__dirname, file))));
193+
}
194+
195+
async function writeFile(filePath: string, content: string) {
196+
// eslint-disable-next-line no-console
197+
console.log(`Writing file: ${filePath}`);
198+
await fs.promises.writeFile(filePath, content, 'utf-8');
199+
}
200+
201+
export async function initClaudeCodeRepo() {
202+
const agents = await loadAgents();
203+
204+
await fs.promises.mkdir('.claude/agents', { recursive: true });
205+
for (const agent of agents)
206+
await writeFile(`.claude/agents/${agent.header.name}.md`, saveAsClaudeCode(agent));
207+
208+
const mcpServers: Record<string, { command: string; args: string[] }> = {};
209+
for (const agent of agents) {
210+
const hash = shortHash(agent.header.name);
211+
for (const [name, mcp] of Object.entries(agent.header['mcp-servers'])) {
212+
const entry = {
213+
command: mcp.command,
214+
args: mcp.args,
215+
};
216+
mcpServers[name + '-' + hash] = entry;
217+
}
218+
}
219+
await writeFile('.mcp.json', JSON.stringify({ mcpServers }, null, 2));
220+
}
221+
222+
export async function initOpencodeRepo() {
223+
const agents = await loadAgents();
224+
225+
await fs.promises.mkdir('.opencode/prompts', { recursive: true });
226+
for (const agent of agents) {
227+
const prompt = [agent.instructions];
228+
prompt.push('');
229+
prompt.push(...agent.examples.map(example => `<example>${example}</example>`));
230+
await writeFile(`.opencode/prompts/${agent.header.name}.md`, prompt.join('\n'));
231+
}
232+
await writeFile('opencode.json', saveAsOpencodeJson(agents));
233+
}

0 commit comments

Comments
 (0)