@zibby/core 0.1.0
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/LICENSE +21 -0
- package/README.md +147 -0
- package/package.json +94 -0
- package/src/agents/base.js +361 -0
- package/src/constants.js +47 -0
- package/src/enrichment/base.js +49 -0
- package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
- package/src/enrichment/enrichers/dom-enricher.js +171 -0
- package/src/enrichment/enrichers/page-state-enricher.js +129 -0
- package/src/enrichment/enrichers/position-enricher.js +67 -0
- package/src/enrichment/index.js +96 -0
- package/src/enrichment/mcp-integration.js +149 -0
- package/src/enrichment/mcp-ref-enricher.js +78 -0
- package/src/enrichment/pipeline.js +192 -0
- package/src/enrichment/trace-text-enricher.js +115 -0
- package/src/framework/AGENTS.md +98 -0
- package/src/framework/agents/base.js +72 -0
- package/src/framework/agents/claude-strategy.js +278 -0
- package/src/framework/agents/cursor-strategy.js +459 -0
- package/src/framework/agents/index.js +105 -0
- package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
- package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
- package/src/framework/code-generator.js +301 -0
- package/src/framework/constants.js +33 -0
- package/src/framework/context-loader.js +101 -0
- package/src/framework/function-bridge.js +78 -0
- package/src/framework/function-skill-registry.js +20 -0
- package/src/framework/graph-compiler.js +342 -0
- package/src/framework/graph.js +610 -0
- package/src/framework/index.js +28 -0
- package/src/framework/node-registry.js +163 -0
- package/src/framework/node.js +259 -0
- package/src/framework/output-parser.js +71 -0
- package/src/framework/skill-registry.js +55 -0
- package/src/framework/state-utils.js +52 -0
- package/src/framework/state.js +67 -0
- package/src/framework/tool-resolver.js +65 -0
- package/src/index.js +342 -0
- package/src/runtime/generation/base.js +46 -0
- package/src/runtime/generation/index.js +70 -0
- package/src/runtime/generation/mcp-ref-strategy.js +197 -0
- package/src/runtime/generation/stable-id-strategy.js +170 -0
- package/src/runtime/stable-id-runtime.js +248 -0
- package/src/runtime/verification/base.js +44 -0
- package/src/runtime/verification/index.js +67 -0
- package/src/runtime/verification/playwright-json-strategy.js +119 -0
- package/src/runtime/zibby-runtime.js +299 -0
- package/src/sync/index.js +2 -0
- package/src/sync/uploader.js +29 -0
- package/src/tools/run-playwright-test.js +158 -0
- package/src/utils/adf-converter.js +68 -0
- package/src/utils/ast-utils.js +37 -0
- package/src/utils/ci-setup.js +124 -0
- package/src/utils/cursor-utils.js +71 -0
- package/src/utils/logger.js +144 -0
- package/src/utils/mcp-config-writer.js +115 -0
- package/src/utils/node-schema-parser.js +522 -0
- package/src/utils/post-process-events.js +55 -0
- package/src/utils/result-handler.js +102 -0
- package/src/utils/ripple-effect.js +84 -0
- package/src/utils/selector-generator.js +239 -0
- package/src/utils/streaming-parser.js +387 -0
- package/src/utils/test-post-processor.js +211 -0
- package/src/utils/timeline.js +217 -0
- package/src/utils/trace-parser.js +325 -0
- package/src/utils/video-organizer.js +91 -0
- package/templates/browser-test-automation/README.md +114 -0
- package/templates/browser-test-automation/graph.js +54 -0
- package/templates/browser-test-automation/nodes/execute-live.js +250 -0
- package/templates/browser-test-automation/nodes/generate-script.js +77 -0
- package/templates/browser-test-automation/nodes/index.js +3 -0
- package/templates/browser-test-automation/nodes/preflight.js +59 -0
- package/templates/browser-test-automation/nodes/utils.js +154 -0
- package/templates/browser-test-automation/result-handler.js +286 -0
- package/templates/code-analysis/graph.js +72 -0
- package/templates/code-analysis/index.js +18 -0
- package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
- package/templates/code-analysis/nodes/create-pr-node.js +175 -0
- package/templates/code-analysis/nodes/finalize-node.js +118 -0
- package/templates/code-analysis/nodes/generate-code-node.js +425 -0
- package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
- package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
- package/templates/code-analysis/nodes/setup-node.js +142 -0
- package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
- package/templates/code-analysis/prompts/generate-code.md +33 -0
- package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
- package/templates/code-analysis/state.js +40 -0
- package/templates/code-implementation/graph.js +35 -0
- package/templates/code-implementation/index.js +7 -0
- package/templates/code-implementation/state.js +14 -0
- package/templates/global-setup.js +56 -0
- package/templates/index.js +94 -0
- package/templates/register-nodes.js +24 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execute Live Node
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Execute test in live browser using MCP Playwright tools
|
|
5
|
+
*
|
|
6
|
+
* Configuration:
|
|
7
|
+
* - capabilities: Declares ['browser'] — framework injects the appropriate MCP server
|
|
8
|
+
* - outputSchema: Structured JSON with execution results, actions, assertions
|
|
9
|
+
* - Model: Configured in .zibby.config.js → agent.claude.model or agent.cursor.model
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z, SKILLS } from '@zibby/core';
|
|
13
|
+
import { formatAssertionChecklist } from './utils.js';
|
|
14
|
+
|
|
15
|
+
export const executeLiveNode = {
|
|
16
|
+
name: 'execute_live',
|
|
17
|
+
skills: [SKILLS.BROWSER, SKILLS.MEMORY],
|
|
18
|
+
timeout: 600000,
|
|
19
|
+
|
|
20
|
+
prompt: (state) => {
|
|
21
|
+
const ctx = state.context;
|
|
22
|
+
const contextInfo = ctx ? `
|
|
23
|
+
Domain Knowledge & Environment:
|
|
24
|
+
${ctx.global || ''}
|
|
25
|
+
${ctx.pathBased ? `Test-Specific Info:\n${ctx.pathBased}\n` : ''}
|
|
26
|
+
${ctx.env ? `Environment Config:\n${JSON.stringify(ctx.env, null, 2)}\n` : ''}
|
|
27
|
+
---
|
|
28
|
+
` : '';
|
|
29
|
+
|
|
30
|
+
const assertionChecklist = formatAssertionChecklist(state.preflight?.assertions);
|
|
31
|
+
|
|
32
|
+
return `⚠️ CRITICAL: At the END, output ONLY the JSON object. NO explanations after the JSON.
|
|
33
|
+
|
|
34
|
+
Execute this test using ONLY mcp_playwright-official_browser_* tools.
|
|
35
|
+
|
|
36
|
+
🚨 HONESTY REQUIREMENT (STRICT):
|
|
37
|
+
- If you DO NOT have access to browser tools → return {"success": false, "steps": [], "browserClosed": false, "notes": "No browser tools available"}
|
|
38
|
+
- DO NOT hallucinate or pretend you executed the test
|
|
39
|
+
- DO NOT return success: true unless you ACTUALLY called browser tools
|
|
40
|
+
- BE HONEST - it's better to admit you can't do it than to lie
|
|
41
|
+
|
|
42
|
+
🎯 YOUR GOAL: Execute the test steps and collect evidence for script generation.
|
|
43
|
+
You don't need perfect verification - just capture the key actions and results.
|
|
44
|
+
The next node will generate the actual test script from your execution.
|
|
45
|
+
|
|
46
|
+
${contextInfo}
|
|
47
|
+
${state.testSpec}
|
|
48
|
+
${assertionChecklist ? `
|
|
49
|
+
═══════════════════════════════════════════════════
|
|
50
|
+
🎯 ASSERTION CHECKLIST (MANDATORY - from test spec)
|
|
51
|
+
You MUST include ALL of these in your 'assertions' array.
|
|
52
|
+
Report each as passed: true or passed: false with evidence.
|
|
53
|
+
DO NOT skip any. DO NOT add extras.
|
|
54
|
+
|
|
55
|
+
${assertionChecklist}
|
|
56
|
+
═══════════════════════════════════════════════════
|
|
57
|
+
` : ''}
|
|
58
|
+
⚠️ CRITICAL RULES (STRICT ENFORCEMENT):
|
|
59
|
+
1. DO NOT get stuck in read loops - if a snapshot is large, move on
|
|
60
|
+
2. DO NOT over-analyze - just execute the steps
|
|
61
|
+
3. **As soon as you complete the test → IMMEDIATELY return JSON**
|
|
62
|
+
4. **NO screenshots required** - just execute and return JSON
|
|
63
|
+
5. **If test is done, STOP - don't try to be perfect**
|
|
64
|
+
6. **USE VALUES FROM THE TEST SPEC** - if the spec provides specific values, use them exactly. Do NOT replace them with random data.
|
|
65
|
+
7. **USE UNIQUE DATA ONLY when CREATING new resources** (e.g., sign-up forms, new accounts) to avoid "already taken" conflicts:
|
|
66
|
+
- For NEW emails (not provided in test spec): use random digits like "test84729@example.com"
|
|
67
|
+
- For NEW names (not provided in test spec): append random digits like "John84729"
|
|
68
|
+
- This does NOT apply to login credentials or test data explicitly provided in the spec
|
|
69
|
+
|
|
70
|
+
WHEN TO STOP (MANDATORY):
|
|
71
|
+
✓ You've completed the test steps
|
|
72
|
+
✓ Test outcome is visible (even briefly)
|
|
73
|
+
→ **RETURN JSON IMMEDIATELY - DO NOT make any more tool calls**
|
|
74
|
+
|
|
75
|
+
DO NOT:
|
|
76
|
+
- Navigate to the same URL multiple times
|
|
77
|
+
- Use browser_run_code or browser_evaluate
|
|
78
|
+
- Read large snapshots repeatedly (max 2 snapshots per page)
|
|
79
|
+
- Take screenshots (optional, skip if slowing you down)
|
|
80
|
+
- Try to verify every single detail - focus on the MAIN outcome
|
|
81
|
+
- Click/scroll/interact after seeing the expected result
|
|
82
|
+
- Spend more than 2 minutes on any single page
|
|
83
|
+
- Try to click elements that aren't immediately visible
|
|
84
|
+
|
|
85
|
+
EXECUTION SEQUENCE (MANDATORY - FOLLOW STRICTLY):
|
|
86
|
+
1. Execute the test steps efficiently (navigate, fill, click)
|
|
87
|
+
- Max 10-15 actions total
|
|
88
|
+
- If stuck, move on to next step
|
|
89
|
+
2. Quick verification - check if main result is visible
|
|
90
|
+
- **If you see expected result → IMMEDIATELY return JSON**
|
|
91
|
+
- Don't try to make it perfect - good enough is enough
|
|
92
|
+
3. **RETURN JSON AND STOP COMPLETELY**
|
|
93
|
+
- Format: { "success": true, "steps": ["step 1", ...], "browserClosed": true, "actions": [...] }
|
|
94
|
+
- MUST include: "success" (boolean), "steps" (array), "browserClosed" (boolean)
|
|
95
|
+
- Keep JSON CONCISE - short descriptions, no excessive detail
|
|
96
|
+
- After the closing brace }, DO NOT write ANYTHING
|
|
97
|
+
- NO commentary, NO explanations, NO additional text
|
|
98
|
+
- NO second JSON object
|
|
99
|
+
- Just the JSON, then STOP
|
|
100
|
+
|
|
101
|
+
IMPORTANT for 'actions' array (STRICT 1:1 MAPPING):
|
|
102
|
+
- Each entry MUST match EXACTLY ONE browser tool call.
|
|
103
|
+
- DO NOT group multiple tool calls into one action.
|
|
104
|
+
- DO NOT combine multiple 'fill' calls into one action.
|
|
105
|
+
- If you call browser_type 3 times for 3 fields, you MUST have 3 actions in the array.
|
|
106
|
+
- Include actual values/URLs in descriptions.
|
|
107
|
+
- Keep descriptions SHORT (5-10 words max).
|
|
108
|
+
|
|
109
|
+
IMPORTANT for 'assertions' array (USE THE CHECKLIST ABOVE):
|
|
110
|
+
- Your assertions array MUST match the ASSERTION CHECKLIST exactly - one entry per item
|
|
111
|
+
- If you verified it and it passed → "passed": true
|
|
112
|
+
- If you could NOT verify it or it wasn't found → "passed": false with evidence of what you saw instead
|
|
113
|
+
- Each assertion MUST include 'verifiedAfterAction' (0-based action index after which you checked)
|
|
114
|
+
- Format: {"description": "...", "passed": true/false, "verifiedAfterAction": N, "evidence": "..."}
|
|
115
|
+
|
|
116
|
+
🔍 CRITICAL: CAPTURE ROBUST SELECTORS (for script generation)
|
|
117
|
+
For EACH action, capture multiple selector strategies in priority order:
|
|
118
|
+
|
|
119
|
+
1. **Role + Name** (Most Robust - Accessibility-first):
|
|
120
|
+
- Role: button/textbox/link/etc
|
|
121
|
+
- Name: visible text or aria-label
|
|
122
|
+
Example: {"role": "button", "name": "Login"}
|
|
123
|
+
|
|
124
|
+
2. **Stable Attributes** (Good Stability):
|
|
125
|
+
- name, type, placeholder, aria-label
|
|
126
|
+
Example: {"attributes": {"name": "username", "type": "text", "placeholder": "Enter username"}}
|
|
127
|
+
|
|
128
|
+
3. **Partial Match** (For Dynamic Elements):
|
|
129
|
+
- Use starts-with for dynamic IDs/classes
|
|
130
|
+
Example: {"partialMatch": {"id": "^user-", "class": "^btn-"}}
|
|
131
|
+
|
|
132
|
+
4. **Structural** (Fallback):
|
|
133
|
+
- Tag + position relative to stable landmark
|
|
134
|
+
Example: {"structure": "form input[type='text']:nth-of-type(1)"}
|
|
135
|
+
|
|
136
|
+
Format for actions with selectors:
|
|
137
|
+
{
|
|
138
|
+
"description": "Fill username field with 'joe'",
|
|
139
|
+
"reasoning": "Need to authenticate user",
|
|
140
|
+
"type": "fill",
|
|
141
|
+
"selectors": {
|
|
142
|
+
"role": {"role": "textbox", "name": "Username"},
|
|
143
|
+
"attributes": {"name": "username", "type": "text", "placeholder": "请输入账号"},
|
|
144
|
+
"structure": "form input[type='text']:first-of-type"
|
|
145
|
+
},
|
|
146
|
+
"value": "joe"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
IMPORTANT for 'evidenceScreenshots' (array) - OPTIONAL:
|
|
150
|
+
- Screenshots are OPTIONAL - only take if helpful
|
|
151
|
+
- If you take screenshots, use descriptive filenames
|
|
152
|
+
- Filename pattern: "{step-number}-{action-or-state}.png"
|
|
153
|
+
- Keep it minimal - test execution is more important than documentation
|
|
154
|
+
|
|
155
|
+
════════════════════════════════════════════════════════════
|
|
156
|
+
🚨 CRITICAL JSON OUTPUT RULES 🚨
|
|
157
|
+
|
|
158
|
+
YOU MUST OUTPUT JSON USING ONE OF THESE TWO FORMATS:
|
|
159
|
+
|
|
160
|
+
✅ FORMAT 1 (BEST - Use This!):
|
|
161
|
+
Think/plan/explain first, THEN output ONLY JSON:
|
|
162
|
+
|
|
163
|
+
I'll navigate to the login page and fill the form...
|
|
164
|
+
[... use browser tools ...]
|
|
165
|
+
[... complete test execution ...]
|
|
166
|
+
|
|
167
|
+
{"success": true, "steps": [...], "browserClosed": true}
|
|
168
|
+
|
|
169
|
+
NO TEXT AFTER THE JSON! Stop immediately after }.
|
|
170
|
+
|
|
171
|
+
✅ FORMAT 2 (If you need to explain after):
|
|
172
|
+
Use delimiters to separate JSON from explanations:
|
|
173
|
+
|
|
174
|
+
I'm executing the test now...
|
|
175
|
+
[... use browser tools ...]
|
|
176
|
+
|
|
177
|
+
===JSON_START===
|
|
178
|
+
{"success": true, "steps": [...], "browserClosed": true}
|
|
179
|
+
===JSON_END===
|
|
180
|
+
|
|
181
|
+
Now let me explain what happened...
|
|
182
|
+
|
|
183
|
+
❌ WRONG - DO NOT DO THIS:
|
|
184
|
+
{"success": true, "steps": [...]} followed by more explanations
|
|
185
|
+
|
|
186
|
+
❌ WRONG - DO NOT STREAM JSON LETTER BY LETTER:
|
|
187
|
+
The test completed successfully, here's the result: { "success": t
|
|
188
|
+
|
|
189
|
+
✅ CORRECT - Output complete JSON in one block:
|
|
190
|
+
{"success": true, "steps": ["step 1", "step 2"], "browserClosed": true}
|
|
191
|
+
|
|
192
|
+
REMEMBER: After the final }, you MUST STOP or use ===JSON_END===
|
|
193
|
+
════════════════════════════════════════════════════════════
|
|
194
|
+
`;
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
outputSchema: z.object({
|
|
198
|
+
success: z.boolean()
|
|
199
|
+
.describe('Whether the test execution completed successfully'),
|
|
200
|
+
|
|
201
|
+
steps: z.array(z.string())
|
|
202
|
+
.describe('Array of test steps executed'),
|
|
203
|
+
|
|
204
|
+
finalUrl: z.string()
|
|
205
|
+
.optional()
|
|
206
|
+
.describe('Final URL after test execution'),
|
|
207
|
+
|
|
208
|
+
actions: z.array(z.any())
|
|
209
|
+
.optional()
|
|
210
|
+
.describe('Detailed array of actions performed with descriptions and reasoning'),
|
|
211
|
+
|
|
212
|
+
assertions: z.array(z.object({
|
|
213
|
+
description: z.string()
|
|
214
|
+
.describe('What was verified'),
|
|
215
|
+
passed: z.boolean()
|
|
216
|
+
.describe('Whether the assertion passed'),
|
|
217
|
+
verifiedAfterAction: z.number()
|
|
218
|
+
.describe('Index of the action after which this was verified (0-based, matches actions array index) - REQUIRED'),
|
|
219
|
+
evidence: z.string()
|
|
220
|
+
.optional()
|
|
221
|
+
.describe('Brief evidence of what was observed')
|
|
222
|
+
}))
|
|
223
|
+
.optional()
|
|
224
|
+
.describe('Array of assertions made during test'),
|
|
225
|
+
|
|
226
|
+
waits: z.array(z.any())
|
|
227
|
+
.optional()
|
|
228
|
+
.describe('Array of waits needed for proper test execution'),
|
|
229
|
+
|
|
230
|
+
evidenceScreenshots: z.array(z.object({
|
|
231
|
+
filename: z.string()
|
|
232
|
+
.describe('Descriptive filename pattern: {step-number}-{action-or-state}.png'),
|
|
233
|
+
|
|
234
|
+
description: z.string()
|
|
235
|
+
.describe('What the screenshot shows and why it is evidence'),
|
|
236
|
+
|
|
237
|
+
verdict: z.enum(['pass', 'fail', 'info'])
|
|
238
|
+
.describe('Test verdict: pass/fail for validation points, info for checkpoints')
|
|
239
|
+
}))
|
|
240
|
+
.optional()
|
|
241
|
+
.describe('Array of screenshots taken at key validation points throughout the test'),
|
|
242
|
+
|
|
243
|
+
browserClosed: z.boolean()
|
|
244
|
+
.describe('Whether the browser was properly closed (should always be true)'),
|
|
245
|
+
|
|
246
|
+
notes: z.string()
|
|
247
|
+
.optional()
|
|
248
|
+
.describe('Additional notes or observations. REQUIRED when success=false to explain why test failed or could not execute')
|
|
249
|
+
})
|
|
250
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from '@zibby/core';
|
|
2
|
+
import { formatRecordedActions, formatAssertionsWithResults } from './utils.js';
|
|
3
|
+
|
|
4
|
+
const GenerateScriptOutputSchema = z.object({
|
|
5
|
+
success: z.boolean(),
|
|
6
|
+
scriptPath: z.string(),
|
|
7
|
+
method: z.string()
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const generateScriptNode = {
|
|
11
|
+
name: 'generate_script',
|
|
12
|
+
outputSchema: GenerateScriptOutputSchema,
|
|
13
|
+
timeout: 360000,
|
|
14
|
+
|
|
15
|
+
prompt: (state) => {
|
|
16
|
+
const exec = state.execute_live || {};
|
|
17
|
+
const preflight = state.preflight || {};
|
|
18
|
+
|
|
19
|
+
const actionsBlock = formatRecordedActions(state.sessionPath, exec.actions);
|
|
20
|
+
const assertionsBlock = formatAssertionsWithResults(
|
|
21
|
+
preflight.assertions,
|
|
22
|
+
exec.assertions,
|
|
23
|
+
exec.notes,
|
|
24
|
+
exec.finalUrl
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return `Generate and verify Playwright test at ${state.outputPath}
|
|
28
|
+
|
|
29
|
+
Test Spec:
|
|
30
|
+
${state.testSpec}
|
|
31
|
+
|
|
32
|
+
Live Execution Summary:
|
|
33
|
+
- Success: ${exec.success}
|
|
34
|
+
- Steps: ${JSON.stringify(exec.steps)}
|
|
35
|
+
- Final URL: ${exec.finalUrl || 'unknown'}
|
|
36
|
+
${actionsBlock}
|
|
37
|
+
${assertionsBlock}
|
|
38
|
+
|
|
39
|
+
IMPORTS AND PATTERN:
|
|
40
|
+
\`\`\`javascript
|
|
41
|
+
import { test, expect } from '@playwright/test';
|
|
42
|
+
import { StableIdRuntime } from '@zibby/core';
|
|
43
|
+
|
|
44
|
+
test('Test Name', async ({ page }) => {
|
|
45
|
+
await page.goto('https://...');
|
|
46
|
+
await StableIdRuntime.injectStableIds(page);
|
|
47
|
+
// Elements WITH stable IDs — use StableIdRuntime:
|
|
48
|
+
await StableIdRuntime.fillWithRetry(page, 'zibby-xxxxx', 'value');
|
|
49
|
+
await StableIdRuntime.clickWithRetry(page, 'zibby-xxxxx');
|
|
50
|
+
// Elements WITHOUT stable IDs (NO_STABLE_ID) — use native Playwright selectors:
|
|
51
|
+
await page.getByText('visible text').click();
|
|
52
|
+
await page.getByRole('button', { name: 'Submit' }).click();
|
|
53
|
+
await page.getByPlaceholder('placeholder text').fill('value');
|
|
54
|
+
await expect(page).toHaveURL(/expected-url/);
|
|
55
|
+
});
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
RULES:
|
|
59
|
+
1. First navigate → page.goto(), skip subsequent navigates
|
|
60
|
+
2. After goto, call StableIdRuntime.injectStableIds(page)
|
|
61
|
+
3. Use EXACT stable IDs from recorded actions when available
|
|
62
|
+
4. For [NO_STABLE_ID] actions, use the fallback selector (getByText, getByRole, getByPlaceholder). These are typically non-semantic elements like spans acting as buttons — use the visible text to target them.
|
|
63
|
+
5. Skip duplicate consecutive clicks on same stableId
|
|
64
|
+
6. No comments in generated code
|
|
65
|
+
7. Implement ALL assertions from the list above
|
|
66
|
+
8. If an assertion fails after retries, comment it out with a TODO (don't delete it)
|
|
67
|
+
|
|
68
|
+
WORKFLOW:
|
|
69
|
+
1. Write test to ${state.outputPath}
|
|
70
|
+
2. Run: PLAYWRIGHT_HEADLESS=1 npx playwright test ${state.outputPath} --reporter=line --timeout=30000
|
|
71
|
+
3. If fails: make ONE targeted fix (longer timeout, different selector, short wait)
|
|
72
|
+
4. MAX 2 ATTEMPTS then STOP
|
|
73
|
+
|
|
74
|
+
The test runs in: ${state.cwd || 'project root'}
|
|
75
|
+
`;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preflight Node
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Prompt-only node (no tools)
|
|
5
|
+
* Purpose: Analyze test spec and extract title + structured assertion checklist
|
|
6
|
+
*
|
|
7
|
+
* This runs before execution to define:
|
|
8
|
+
* - A concise test title
|
|
9
|
+
* - The complete list of assertions that must be verified
|
|
10
|
+
*
|
|
11
|
+
* Downstream nodes receive this as their contract:
|
|
12
|
+
* - execute_live: must report passed/failed for each assertion
|
|
13
|
+
* - generate_script: must implement each assertion in the test
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from '@zibby/core';
|
|
17
|
+
import { writeFileSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
|
|
20
|
+
const AssertionSchema = z.object({
|
|
21
|
+
description: z.string().describe('What to verify (e.g., "User is redirected to dashboard")'),
|
|
22
|
+
expected: z.string().describe('What the expected outcome looks like (e.g., "URL contains /dashboard")')
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const PreflightOutputSchema = z.object({
|
|
26
|
+
title: z.string().describe('Concise test title (5-10 words, action-oriented). Prefix with ticket ID if found.'),
|
|
27
|
+
assertions: z.array(AssertionSchema).describe('Every expected result from the spec as a verifiable assertion')
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const preflightNode = {
|
|
31
|
+
name: 'preflight',
|
|
32
|
+
|
|
33
|
+
async onComplete(state, result) {
|
|
34
|
+
const sessionPath = state.sessionPath || process.env.ZIBBY_SESSION_PATH;
|
|
35
|
+
if (sessionPath && result.title) {
|
|
36
|
+
try {
|
|
37
|
+
writeFileSync(join(sessionPath, 'title.txt'), result.title, 'utf-8');
|
|
38
|
+
console.log(`Saved title: "${result.title}"`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.warn(`⚠️ Could not save title.txt: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
prompt: (state) => `Analyze this test specification and extract:
|
|
47
|
+
1. A concise test title (5-10 words, action-oriented). If you find a ticket ID (e.g., PROJ-123, ACME-456), prefix the title with it.
|
|
48
|
+
2. Every expected result as a verifiable assertion. Each assertion must be something the browser can check after execution.
|
|
49
|
+
|
|
50
|
+
Test Spec:
|
|
51
|
+
${state.testSpec}
|
|
52
|
+
|
|
53
|
+
IMPORTANT: You MUST create ONE assertion for EACH expected result in the spec. Do NOT skip any.
|
|
54
|
+
|
|
55
|
+
Return ONLY this JSON:
|
|
56
|
+
{ "title": "TICKET-ID: Short action title", "assertions": [ { "description": "...", "expected": "..." }, ... ] }`,
|
|
57
|
+
|
|
58
|
+
outputSchema: PreflightOutputSchema
|
|
59
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
const ACTIONABLE_EVENTS = ['navigate', 'type', 'fill', 'click', 'select_option', 'select'];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load recorded browser events from a session's events.json,
|
|
7
|
+
* filtering to only actionable events (navigate, fill, click, etc.)
|
|
8
|
+
*/
|
|
9
|
+
export function loadRecordedActions(sessionPath, nodeName = 'execute_live') {
|
|
10
|
+
const eventsPath = `${sessionPath}/${nodeName}/events.json`;
|
|
11
|
+
if (!existsSync(eventsPath)) return [];
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(eventsPath, 'utf-8'))
|
|
14
|
+
.filter(e => ACTIONABLE_EVENTS.includes(e.type))
|
|
15
|
+
.map(e => ({
|
|
16
|
+
type: e.type,
|
|
17
|
+
stableId: e.stableId || e.data?.stableId,
|
|
18
|
+
value: e.data?.text || e.data?.params?.text || e.data?.url || e.data?.params?.url || e.data?.values?.[0] || e.data?.params?.values?.[0],
|
|
19
|
+
element: e.data?.element || e.data?.params?.element,
|
|
20
|
+
reasoning: e.reasoning || e.description
|
|
21
|
+
}));
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.error('Failed to read events:', e.message);
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Index AI-captured selectors by element description (lowercase)
|
|
30
|
+
* for fuzzy matching when building fallback selectors.
|
|
31
|
+
*/
|
|
32
|
+
export function buildSelectorMap(actions) {
|
|
33
|
+
const map = {};
|
|
34
|
+
for (const a of actions || []) {
|
|
35
|
+
if (a.selectors?.role) {
|
|
36
|
+
map[(a.description || '').toLowerCase()] = a.selectors;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return map;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Extract meaningful tokens from a description string for fuzzy matching. */
|
|
43
|
+
function extractTokens(str) {
|
|
44
|
+
return str.replace(/[-–—]/g, ' ').split(/\s+/).filter(t => t.length > 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Score how well two descriptions match by counting shared tokens. */
|
|
48
|
+
function tokenOverlapScore(a, b) {
|
|
49
|
+
const tokensA = extractTokens(a.toLowerCase());
|
|
50
|
+
const tokensB = extractTokens(b.toLowerCase());
|
|
51
|
+
return tokensA.filter(t => tokensB.some(tb => tb.includes(t) || t.includes(tb))).length;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Find a fallback selector for an element by fuzzy-matching against the selector map. */
|
|
55
|
+
function findFallbackSelector(elementKey, selectorMap) {
|
|
56
|
+
let bestMatch = null;
|
|
57
|
+
let bestScore = 0;
|
|
58
|
+
for (const [k, selectors] of Object.entries(selectorMap)) {
|
|
59
|
+
const score = tokenOverlapScore(elementKey, k);
|
|
60
|
+
if (score > bestScore) {
|
|
61
|
+
bestScore = score;
|
|
62
|
+
bestMatch = selectors;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!bestMatch?.role || bestScore < 1) return '';
|
|
66
|
+
const { role, name } = bestMatch.role;
|
|
67
|
+
if (role === 'generic' || role === 'none') {
|
|
68
|
+
return name ? ` | fallback: getByText('${name}')` : '';
|
|
69
|
+
}
|
|
70
|
+
return ` | fallback: getByRole('${role}'${name ? `, { name: '${name}' }` : ''})`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Extract visible text hint from element description for use as a getByText fallback. */
|
|
74
|
+
function extractTextHint(elementDesc) {
|
|
75
|
+
if (!elementDesc) return '';
|
|
76
|
+
const cnMatch = elementDesc.match(/[\u4e00-\u9fff\u3000-\u303f]+/g);
|
|
77
|
+
if (cnMatch) return cnMatch.join('');
|
|
78
|
+
const afterDash = elementDesc.split(/[-–—]\s*/).pop()?.trim();
|
|
79
|
+
if (afterDash && afterDash !== elementDesc) return afterDash;
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Format a single recorded event into a human-readable action line with stableId and fallback selector. */
|
|
84
|
+
export function formatAction(event, index, selectorMap) {
|
|
85
|
+
const num = index + 1;
|
|
86
|
+
const reason = event.reasoning ? ` | reason: "${event.reasoning}"` : '';
|
|
87
|
+
const hasStableId = event.stableId && event.stableId !== 'undefined';
|
|
88
|
+
const fallback = findFallbackSelector((event.element || '').toLowerCase(), selectorMap);
|
|
89
|
+
const idPart = hasStableId ? `[stableId="${event.stableId}"]` : '[NO_STABLE_ID]';
|
|
90
|
+
|
|
91
|
+
let textFallback = '';
|
|
92
|
+
if (!hasStableId && !fallback) {
|
|
93
|
+
const hint = extractTextHint(event.element);
|
|
94
|
+
if (hint) textFallback = ` | fallback: getByText('${hint}')`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
switch (event.type) {
|
|
98
|
+
case 'navigate':
|
|
99
|
+
return `${num}. NAVIGATE to: ${event.value}${reason}`;
|
|
100
|
+
case 'click':
|
|
101
|
+
return `${num}. CLICK ${idPart} - ${event.element || 'element'}${fallback || textFallback}${reason}`;
|
|
102
|
+
case 'fill': case 'type':
|
|
103
|
+
return `${num}. FILL ${idPart} with "${event.value}" - ${event.element || 'field'}${fallback || textFallback}${reason}`;
|
|
104
|
+
case 'select': case 'select_option':
|
|
105
|
+
return `${num}. SELECT ${idPart} option "${event.value}" - ${event.element || 'dropdown'}${fallback || textFallback}${reason}`;
|
|
106
|
+
default:
|
|
107
|
+
return `${num}. ${event.type} ${hasStableId ? idPart : ''} ${event.value || ''}${fallback || textFallback}${reason}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build the full RECORDED ACTIONS prompt block by loading events.json
|
|
113
|
+
* and merging with AI-captured selectors from execution output.
|
|
114
|
+
*/
|
|
115
|
+
export function formatRecordedActions(sessionPath, executionActions) {
|
|
116
|
+
const recorded = loadRecordedActions(sessionPath);
|
|
117
|
+
if (recorded.length === 0) return '';
|
|
118
|
+
const selectorMap = buildSelectorMap(executionActions);
|
|
119
|
+
return `RECORDED ACTIONS:\n${recorded.map((e, i) => formatAction(e, i, selectorMap)).join('\n')}\n\nGenerate a CLEAN script from these — not a 1:1 replay. If stableId fails, use the fallback selector.`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Format preflight assertions into a simple numbered checklist for the execute_live prompt. */
|
|
123
|
+
export function formatAssertionChecklist(assertions) {
|
|
124
|
+
if (!assertions?.length) return '';
|
|
125
|
+
return assertions.map((a, i) =>
|
|
126
|
+
`${i + 1}. ${a.description} → expected: ${a.expected}`
|
|
127
|
+
).join('\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Merge preflight assertion definitions with execution results (pass/fail/evidence)
|
|
132
|
+
* into an ASSERTIONS TO IMPLEMENT block for the generate_script prompt.
|
|
133
|
+
*/
|
|
134
|
+
export function formatAssertionsWithResults(preflightAssertions, executionAssertions, notes, finalUrl) {
|
|
135
|
+
if (!preflightAssertions?.length && !executionAssertions?.length) return '';
|
|
136
|
+
|
|
137
|
+
const lines = preflightAssertions?.length > 0
|
|
138
|
+
? preflightAssertions.map((a, i) => {
|
|
139
|
+
const exec = executionAssertions?.[i];
|
|
140
|
+
const status = exec ? (exec.passed ? '✅ PASSED' : '❌ FAILED') : '⚠️ NOT CHECKED';
|
|
141
|
+
const evidence = exec?.evidence ? ` | evidence: "${exec.evidence}"` : '';
|
|
142
|
+
return `${i + 1}. [${status}] ${a.description} → expected: ${a.expected}${evidence}`;
|
|
143
|
+
})
|
|
144
|
+
: executionAssertions.map((a, i) =>
|
|
145
|
+
typeof a === 'object'
|
|
146
|
+
? `${i + 1}. ${a.description || a.type}: expected "${a.expected}", actual "${a.actual}"`
|
|
147
|
+
: `${i + 1}. ${a}`
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return `ASSERTIONS TO IMPLEMENT:
|
|
151
|
+
${lines.join('\n')}
|
|
152
|
+
|
|
153
|
+
Final URL: ${finalUrl || 'unknown'}${notes ? `\nAI OBSERVATION: ${notes}` : ''}`;
|
|
154
|
+
}
|