@zibby/core 0.1.21 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/base.js +17 -0
- package/dist/backend-client.js +1 -0
- package/dist/constants/tool-names.js +1 -0
- package/dist/constants/zibby-scratch.js +1 -0
- package/dist/constants.js +1 -0
- package/dist/enrichment/base.js +1 -0
- package/dist/enrichment/enrichers/accessibility-enricher.js +1 -0
- package/dist/enrichment/enrichers/dom-enricher.js +1 -0
- package/dist/enrichment/enrichers/page-state-enricher.js +1 -0
- package/dist/enrichment/enrichers/position-enricher.js +1 -0
- package/dist/enrichment/index.js +1 -0
- package/dist/enrichment/mcp-integration.js +1 -0
- package/dist/enrichment/mcp-ref-enricher.js +1 -0
- package/dist/enrichment/pipeline.js +3 -0
- package/dist/enrichment/trace-text-enricher.js +1 -0
- package/dist/framework/agents/assistant-strategy.js +5 -0
- package/dist/framework/agents/base.js +1 -0
- package/dist/framework/agents/claude-strategy.js +4 -0
- package/dist/framework/agents/codex-strategy.js +4 -0
- package/dist/framework/agents/cursor-strategy.js +32 -0
- package/dist/framework/agents/gemini-strategy.js +11 -0
- package/dist/framework/agents/index.js +13 -0
- package/dist/framework/agents/middleware/assistant-round-pipeline.js +3 -0
- package/dist/framework/agents/providers/base.js +1 -0
- package/dist/framework/agents/providers/index.js +1 -0
- package/dist/framework/agents/providers/openai-transport.js +2 -0
- package/dist/framework/agents/providers/openai.js +1 -0
- package/dist/framework/agents/providers/transport-base.js +1 -0
- package/dist/framework/agents/utils/auth-resolver.js +1 -0
- package/dist/framework/agents/utils/cursor-output-formatter.js +1 -0
- package/dist/framework/agents/utils/openai-proxy-formatter.js +9 -0
- package/dist/framework/agents/utils/payload-budget.js +3 -0
- package/dist/framework/agents/utils/structured-output-formatter.js +21 -0
- package/dist/framework/code-generator.js +10 -0
- package/dist/framework/constants.js +1 -0
- package/dist/framework/context-loader.js +5 -0
- package/dist/framework/function-bridge.js +2 -0
- package/dist/framework/function-skill-registry.js +1 -0
- package/dist/framework/graph-compiler.js +1 -0
- package/dist/framework/graph.js +5 -0
- package/dist/framework/index.js +1 -0
- package/dist/framework/mcp-client.js +2 -0
- package/dist/framework/node-registry.js +9 -0
- package/dist/framework/node.js +5 -0
- package/dist/framework/output-parser.js +3 -0
- package/dist/framework/skill-registry.js +1 -0
- package/dist/framework/state-utils.js +1 -0
- package/dist/framework/state.js +1 -0
- package/dist/framework/tool-resolver.js +1 -0
- package/dist/index.js +8 -0
- package/dist/runtime/generation/base.js +1 -0
- package/dist/runtime/generation/index.js +3 -0
- package/dist/runtime/generation/mcp-ref-strategy.js +41 -0
- package/dist/runtime/generation/stable-id-strategy.js +16 -0
- package/dist/runtime/stable-id-runtime.js +1 -0
- package/dist/runtime/verification/base.js +1 -0
- package/dist/runtime/verification/index.js +3 -0
- package/dist/runtime/verification/playwright-json-strategy.js +1 -0
- package/dist/runtime/zibby-runtime.js +1 -0
- package/dist/sync/index.js +1 -0
- package/dist/sync/uploader.js +1 -0
- package/dist/tools/run-playwright-test.js +5 -0
- package/dist/utils/adf-converter.js +7 -0
- package/dist/utils/ast-utils.js +1 -0
- package/dist/utils/ci-setup.js +5 -0
- package/dist/utils/cursor-mcp-isolated-home.js +1 -0
- package/dist/utils/cursor-utils.js +18 -0
- package/dist/utils/live-frame-discovery.js +1 -0
- package/dist/utils/logger.js +1 -0
- package/dist/utils/mcp-config-writer.js +10 -0
- package/dist/utils/mission-control-from-run-states.js +1 -0
- package/dist/utils/node-schema-parser.js +1 -0
- package/dist/utils/parallel-config.js +1 -0
- package/dist/utils/post-process-events.js +1 -0
- package/dist/utils/result-handler.js +1 -0
- package/{src → dist}/utils/ripple-effect.js +3 -12
- package/dist/utils/run-capacity-coordinator.js +1 -0
- package/dist/utils/run-capacity-queue.js +2 -0
- package/dist/utils/run-index-merge.js +1 -0
- package/dist/utils/run-index-post-cli.js +1 -0
- package/dist/utils/run-registry.js +3 -0
- package/dist/utils/run-state-session.js +2 -0
- package/dist/utils/selector-generator.js +4 -0
- package/dist/utils/session-state-constants.js +1 -0
- package/dist/utils/session-state-live-runs.js +1 -0
- package/dist/utils/streaming-parser.js +4 -0
- package/dist/utils/test-post-processor.js +18 -0
- package/dist/utils/timeline.js +14 -0
- package/dist/utils/trace-parser.js +2 -0
- package/dist/utils/video-organizer.js +3 -0
- package/package.json +49 -35
- package/templates/browser-test-automation/README.md +29 -7
- package/templates/browser-test-automation/chat.mjs +36 -0
- package/templates/browser-test-automation/graph.mjs +5 -9
- package/templates/browser-test-automation/nodes/execute-live.mjs +30 -58
- package/templates/browser-test-automation/nodes/generate-script.mjs +32 -12
- package/templates/browser-test-automation/nodes/utils.mjs +153 -10
- package/templates/browser-test-automation/pipeline-ids.js +12 -0
- package/templates/browser-test-automation/result-handler.mjs +78 -2
- package/templates/browser-test-automation/run-index.mjs +418 -0
- package/scripts/export-default-workflows.js +0 -51
- package/scripts/patch-cursor-mcp.js +0 -174
- package/scripts/setup-ci.sh +0 -115
- package/scripts/setup-official-playwright-mcp.sh +0 -226
- package/scripts/test-with-video.sh +0 -49
- package/src/agents/base.js +0 -361
- package/src/constants.js +0 -47
- package/src/enrichment/base.js +0 -49
- package/src/enrichment/enrichers/accessibility-enricher.js +0 -197
- package/src/enrichment/enrichers/dom-enricher.js +0 -171
- package/src/enrichment/enrichers/page-state-enricher.js +0 -129
- package/src/enrichment/enrichers/position-enricher.js +0 -67
- package/src/enrichment/index.js +0 -96
- package/src/enrichment/mcp-integration.js +0 -149
- package/src/enrichment/mcp-ref-enricher.js +0 -78
- package/src/enrichment/pipeline.js +0 -192
- package/src/enrichment/trace-text-enricher.js +0 -115
- package/src/framework/AGENTS.md +0 -98
- package/src/framework/agents/base.js +0 -72
- package/src/framework/agents/claude-strategy.js +0 -278
- package/src/framework/agents/cursor-strategy.js +0 -544
- package/src/framework/agents/index.js +0 -105
- package/src/framework/agents/utils/cursor-output-formatter.js +0 -67
- package/src/framework/agents/utils/openai-proxy-formatter.js +0 -249
- package/src/framework/code-generator.js +0 -301
- package/src/framework/constants.js +0 -33
- package/src/framework/context-loader.js +0 -101
- package/src/framework/function-bridge.js +0 -78
- package/src/framework/function-skill-registry.js +0 -20
- package/src/framework/graph-compiler.js +0 -342
- package/src/framework/graph.js +0 -610
- package/src/framework/index.js +0 -28
- package/src/framework/node-registry.js +0 -163
- package/src/framework/node.js +0 -259
- package/src/framework/output-parser.js +0 -71
- package/src/framework/skill-registry.js +0 -55
- package/src/framework/state-utils.js +0 -52
- package/src/framework/state.js +0 -67
- package/src/framework/tool-resolver.js +0 -65
- package/src/index.js +0 -345
- package/src/runtime/generation/base.js +0 -46
- package/src/runtime/generation/index.js +0 -70
- package/src/runtime/generation/mcp-ref-strategy.js +0 -197
- package/src/runtime/generation/stable-id-strategy.js +0 -170
- package/src/runtime/stable-id-runtime.js +0 -248
- package/src/runtime/verification/base.js +0 -44
- package/src/runtime/verification/index.js +0 -67
- package/src/runtime/verification/playwright-json-strategy.js +0 -119
- package/src/runtime/zibby-runtime.js +0 -299
- package/src/sync/index.js +0 -2
- package/src/sync/uploader.js +0 -29
- package/src/tools/run-playwright-test.js +0 -158
- package/src/utils/adf-converter.js +0 -68
- package/src/utils/ast-utils.js +0 -37
- package/src/utils/ci-setup.js +0 -124
- package/src/utils/cursor-utils.js +0 -71
- package/src/utils/logger.js +0 -144
- package/src/utils/mcp-config-writer.js +0 -115
- package/src/utils/node-schema-parser.js +0 -522
- package/src/utils/post-process-events.js +0 -55
- package/src/utils/result-handler.js +0 -102
- package/src/utils/selector-generator.js +0 -239
- package/src/utils/streaming-parser.js +0 -387
- package/src/utils/test-post-processor.js +0 -211
- package/src/utils/timeline.js +0 -217
- package/src/utils/trace-parser.js +0 -325
- package/src/utils/video-organizer.js +0 -91
|
@@ -2,6 +2,9 @@ import { readFileSync, existsSync } from 'fs';
|
|
|
2
2
|
|
|
3
3
|
const ACTIONABLE_EVENTS = ['navigate', 'type', 'fill', 'click', 'select_option', 'select'];
|
|
4
4
|
|
|
5
|
+
// ARIA roles that accept text input via Playwright .fill()
|
|
6
|
+
const FILLABLE_ROLES = new Set(['textbox', 'combobox', 'spinbutton', 'searchbox']);
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* Load recorded browser events from a session's events.json,
|
|
7
10
|
* filtering to only actionable events (navigate, fill, click, etc.)
|
|
@@ -51,23 +54,63 @@ function tokenOverlapScore(a, b) {
|
|
|
51
54
|
return tokensA.filter(t => tokensB.some(tb => tb.includes(t) || t.includes(tb))).length;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Check whether an ARIA role is compatible with an action type.
|
|
59
|
+
* Prevents click actions from getting textbox fallbacks and vice versa.
|
|
60
|
+
*/
|
|
61
|
+
function isRoleCompatible(role, actionType) {
|
|
62
|
+
if (!role || role === 'generic' || role === 'none') return true;
|
|
63
|
+
if (actionType === 'click' && FILLABLE_ROLES.has(role)) return false;
|
|
64
|
+
if ((actionType === 'fill' || actionType === 'type') && !FILLABLE_ROLES.has(role)) return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatRoleSelector(roleObj) {
|
|
69
|
+
if (!roleObj) return '';
|
|
70
|
+
const { role, name } = roleObj;
|
|
71
|
+
if (role === 'generic' || role === 'none') {
|
|
72
|
+
return name ? ` | fallback: getByText('${name}')` : '';
|
|
73
|
+
}
|
|
74
|
+
return ` | fallback: getByRole('${role}'${name ? `, { name: '${name}' }` : ''})`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Find a fallback selector for an element by matching against the AI selector map.
|
|
79
|
+
* 1. Try exact description match first.
|
|
80
|
+
* 2. Try role.name containment — prefer the longest (most specific) role name found in the element description.
|
|
81
|
+
* 3. Fall back to fuzzy token matching with ≥2 token overlap threshold.
|
|
82
|
+
* 4. Validate ARIA role compatibility with the action type.
|
|
83
|
+
*/
|
|
84
|
+
function findFallbackSelector(elementKey, selectorMap, actionType) {
|
|
85
|
+
if (selectorMap[elementKey]?.role && isRoleCompatible(selectorMap[elementKey].role.role, actionType)) {
|
|
86
|
+
return formatRoleSelector(selectorMap[elementKey].role);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let bestNameMatch = null;
|
|
90
|
+
let bestNameLen = 0;
|
|
91
|
+
for (const [, selectors] of Object.entries(selectorMap)) {
|
|
92
|
+
if (!isRoleCompatible(selectors.role?.role, actionType)) continue;
|
|
93
|
+
const roleName = selectors.role?.name;
|
|
94
|
+
if (!roleName) continue;
|
|
95
|
+
if (elementKey.includes(roleName.toLowerCase()) && roleName.length > bestNameLen) {
|
|
96
|
+
bestNameLen = roleName.length;
|
|
97
|
+
bestNameMatch = selectors;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (bestNameMatch?.role) return formatRoleSelector(bestNameMatch.role);
|
|
101
|
+
|
|
56
102
|
let bestMatch = null;
|
|
57
103
|
let bestScore = 0;
|
|
58
104
|
for (const [k, selectors] of Object.entries(selectorMap)) {
|
|
105
|
+
if (!isRoleCompatible(selectors.role?.role, actionType)) continue;
|
|
59
106
|
const score = tokenOverlapScore(elementKey, k);
|
|
60
107
|
if (score > bestScore) {
|
|
61
108
|
bestScore = score;
|
|
62
109
|
bestMatch = selectors;
|
|
63
110
|
}
|
|
64
111
|
}
|
|
65
|
-
if (!bestMatch?.role || bestScore <
|
|
66
|
-
|
|
67
|
-
if (role === 'generic' || role === 'none') {
|
|
68
|
-
return name ? ` | fallback: getByText('${name}')` : '';
|
|
69
|
-
}
|
|
70
|
-
return ` | fallback: getByRole('${role}'${name ? `, { name: '${name}' }` : ''})`;
|
|
112
|
+
if (!bestMatch?.role || bestScore < 2) return '';
|
|
113
|
+
return formatRoleSelector(bestMatch.role);
|
|
71
114
|
}
|
|
72
115
|
|
|
73
116
|
/** Extract visible text hint from element description for use as a getByText fallback. */
|
|
@@ -80,12 +123,91 @@ function extractTextHint(elementDesc) {
|
|
|
80
123
|
return '';
|
|
81
124
|
}
|
|
82
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Mark duplicate stableIds across actions. When the same stableId appears
|
|
128
|
+
* on different steps, the second+ occurrence gets nullified with a marker
|
|
129
|
+
* so the LLM knows to use the fallback selector instead.
|
|
130
|
+
*/
|
|
131
|
+
export function deduplicateStableIds(actions) {
|
|
132
|
+
const seen = new Map();
|
|
133
|
+
return actions.map((a, i) => {
|
|
134
|
+
if (!a.stableId || a.stableId === 'undefined') return a;
|
|
135
|
+
if (seen.has(a.stableId)) {
|
|
136
|
+
return { ...a, _dupOf: seen.get(a.stableId), stableId: null };
|
|
137
|
+
}
|
|
138
|
+
seen.set(a.stableId, i);
|
|
139
|
+
return a;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Detect a form-submission setup sequence at the start of recorded actions.
|
|
145
|
+
* Purely structural — no hardcoded keywords. Finds a block of fill/type/click
|
|
146
|
+
* actions near the start containing ≥2 fills, ending with a click (submit).
|
|
147
|
+
* The LLM decides what it is (login, registration, etc.) from context.
|
|
148
|
+
*/
|
|
149
|
+
export function detectLoginPattern(actions) {
|
|
150
|
+
if (!actions || actions.length < 3) return null;
|
|
151
|
+
|
|
152
|
+
let start = 0;
|
|
153
|
+
while (start < actions.length && actions[start].type === 'navigate') start++;
|
|
154
|
+
if (start >= actions.length) return null;
|
|
155
|
+
|
|
156
|
+
let lastFillIdx = -1;
|
|
157
|
+
let fillCount = 0;
|
|
158
|
+
let i = start;
|
|
159
|
+
while (i < actions.length) {
|
|
160
|
+
const t = actions[i].type;
|
|
161
|
+
if (t === 'fill' || t === 'type') {
|
|
162
|
+
fillCount++;
|
|
163
|
+
lastFillIdx = i;
|
|
164
|
+
} else if (t !== 'click') {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
i++;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (fillCount < 2 || lastFillIdx < 0) return null;
|
|
171
|
+
|
|
172
|
+
let submitIdx = -1;
|
|
173
|
+
for (let j = lastFillIdx + 1; j < actions.length; j++) {
|
|
174
|
+
if (actions[j].type === 'click') { submitIdx = j; break; }
|
|
175
|
+
if (actions[j].type !== 'fill' && actions[j].type !== 'type') break;
|
|
176
|
+
}
|
|
177
|
+
if (submitIdx < 0) return null;
|
|
178
|
+
|
|
179
|
+
const remaining = actions.length - submitIdx - 1;
|
|
180
|
+
if (remaining < 1) return null;
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
startIndex: 0,
|
|
184
|
+
endIndex: submitIdx,
|
|
185
|
+
fillCount,
|
|
186
|
+
actions: actions.slice(0, submitIdx + 1),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
83
190
|
/** Format a single recorded event into a human-readable action line with stableId and fallback selector. */
|
|
84
191
|
export function formatAction(event, index, selectorMap) {
|
|
85
192
|
const num = index + 1;
|
|
86
193
|
const reason = event.reasoning ? ` | reason: "${event.reasoning}"` : '';
|
|
87
194
|
const hasStableId = event.stableId && event.stableId !== 'undefined';
|
|
88
|
-
|
|
195
|
+
|
|
196
|
+
if (event._dupOf !== undefined) {
|
|
197
|
+
const dupHint = `[DUPLICATE_STABLE_ID — same as step ${event._dupOf + 1}, use fallback]`;
|
|
198
|
+
const fallback = findFallbackSelector((event.element || '').toLowerCase(), selectorMap, event.type);
|
|
199
|
+
const textFb = !fallback ? (() => { const h = extractTextHint(event.element); return h ? ` | fallback: getByText('${h}')` : ''; })() : '';
|
|
200
|
+
switch (event.type) {
|
|
201
|
+
case 'click':
|
|
202
|
+
return `${num}. CLICK ${dupHint} - ${event.element || 'element'}${fallback || textFb}${reason}`;
|
|
203
|
+
case 'fill': case 'type':
|
|
204
|
+
return `${num}. FILL ${dupHint} with "${event.value}" - ${event.element || 'field'}${fallback || textFb}${reason}`;
|
|
205
|
+
default:
|
|
206
|
+
return `${num}. ${event.type} ${dupHint} ${event.value || ''}${fallback || textFb}${reason}`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const fallback = findFallbackSelector((event.element || '').toLowerCase(), selectorMap, event.type);
|
|
89
211
|
const idPart = hasStableId ? `[stableId="${event.stableId}"]` : '[NO_STABLE_ID]';
|
|
90
212
|
|
|
91
213
|
let textFallback = '';
|
|
@@ -111,14 +233,35 @@ export function formatAction(event, index, selectorMap) {
|
|
|
111
233
|
/**
|
|
112
234
|
* Build the full RECORDED ACTIONS prompt block by loading events.json
|
|
113
235
|
* and merging with AI-captured selectors from execution output.
|
|
236
|
+
* Auto-deduplicates stableIds before formatting.
|
|
114
237
|
*/
|
|
115
238
|
export function formatRecordedActions(sessionPath, executionActions) {
|
|
116
|
-
|
|
239
|
+
let recorded = loadRecordedActions(sessionPath);
|
|
117
240
|
if (recorded.length === 0) return '';
|
|
241
|
+
recorded = deduplicateStableIds(recorded);
|
|
118
242
|
const selectorMap = buildSelectorMap(executionActions);
|
|
119
243
|
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
244
|
}
|
|
121
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Format a prompt hint when a setup sequence (form submission) is detected
|
|
248
|
+
* at the start of the recorded actions. Tells the LLM to check for or
|
|
249
|
+
* create a shared fixture — no prescriptive file names or code patterns.
|
|
250
|
+
*/
|
|
251
|
+
export function formatSetupHint(setup) {
|
|
252
|
+
if (!setup) return '';
|
|
253
|
+
return `
|
|
254
|
+
SETUP SEQUENCE DETECTED (steps ${setup.startIndex + 1}–${setup.endIndex + 1}, ${setup.fillCount} form fields):
|
|
255
|
+
The first ${setup.endIndex + 1} actions are a form-submission sequence that will repeat across tests.
|
|
256
|
+
BEFORE writing ANY new files, you MUST search the codebase:
|
|
257
|
+
1. Search tests/ for existing fixture, helper, or setup files (e.g. grep for "export.*function" in tests/)
|
|
258
|
+
2. Read any found files to see what they export
|
|
259
|
+
3. If an existing function already handles this setup → import and reuse it, skip steps ${setup.startIndex + 1}–${setup.endIndex + 1}
|
|
260
|
+
4. ONLY create a new fixture if nothing existing covers these steps
|
|
261
|
+
DO NOT create a duplicate fixture. Reuse what exists.
|
|
262
|
+
`;
|
|
263
|
+
}
|
|
264
|
+
|
|
122
265
|
/** Format preflight assertions into a simple numbered checklist for the execute_live prompt. */
|
|
123
266
|
export function formatAssertionChecklist(assertions) {
|
|
124
267
|
if (!assertions?.length) return '';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-test **template only** — ordered graph node ids used for:
|
|
3
|
+
* - JSONL `recordKind: 'progress'` validation in this template’s `run-index.mjs`
|
|
4
|
+
* - Studio UI that visualizes this workflow (imports from this path, not from `src/`).
|
|
5
|
+
*
|
|
6
|
+
* Other workflows should define their own ordered id list alongside their template.
|
|
7
|
+
*/
|
|
8
|
+
export const BROWSER_TEST_PIPELINE_NODE_IDS = Object.freeze([
|
|
9
|
+
'preflight',
|
|
10
|
+
'execute_live',
|
|
11
|
+
'generate_script',
|
|
12
|
+
]);
|
|
@@ -4,8 +4,17 @@
|
|
|
4
4
|
* and assertion → event ID resolution.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
writeFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
statSync,
|
|
14
|
+
copyFileSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
} from 'fs';
|
|
17
|
+
import { join, isAbsolute, normalize } from 'path';
|
|
9
18
|
import { spawnSync } from 'child_process';
|
|
10
19
|
import { ResultHandler, logger } from '@zibby/core';
|
|
11
20
|
|
|
@@ -283,4 +292,71 @@ export class BrowserTestResultHandler extends ResultHandler {
|
|
|
283
292
|
console.warn('⚠️ Could not enrich events:', err.message);
|
|
284
293
|
}
|
|
285
294
|
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Copy the final Playwright/Selenium file into `session/generate_script/` under stable names
|
|
298
|
+
* so Studio (Electron + web bridge) can load scripts without resolving arbitrary `scriptPath`.
|
|
299
|
+
* Safe to run even when the agent already wrote into that folder.
|
|
300
|
+
*/
|
|
301
|
+
static ensureStudioCodegenMirror(sessionRoot, cwd) {
|
|
302
|
+
if (!sessionRoot || typeof sessionRoot !== 'string') return;
|
|
303
|
+
const gsDir = join(sessionRoot, 'generate_script');
|
|
304
|
+
const resultPath = join(gsDir, 'result.json');
|
|
305
|
+
if (!existsSync(resultPath)) return;
|
|
306
|
+
|
|
307
|
+
let data;
|
|
308
|
+
try {
|
|
309
|
+
data = JSON.parse(readFileSync(resultPath, 'utf-8'));
|
|
310
|
+
} catch {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const raw = data?.scriptPath;
|
|
314
|
+
if (!raw || typeof raw !== 'string' || !raw.trim()) return;
|
|
315
|
+
|
|
316
|
+
const t = raw.trim();
|
|
317
|
+
const candidates = [];
|
|
318
|
+
if (isAbsolute(t)) candidates.push(normalize(t));
|
|
319
|
+
if (cwd && typeof cwd === 'string') candidates.push(join(cwd, t));
|
|
320
|
+
candidates.push(join(sessionRoot, t));
|
|
321
|
+
candidates.push(join(gsDir, t));
|
|
322
|
+
|
|
323
|
+
let src = null;
|
|
324
|
+
for (const c of candidates) {
|
|
325
|
+
try {
|
|
326
|
+
if (existsSync(c) && statSync(c).isFile()) {
|
|
327
|
+
src = c;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
} catch {
|
|
331
|
+
/* ignore */
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!src) {
|
|
335
|
+
logger.debug(`[Studio] Codegen mirror: could not resolve scriptPath "${t}"`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const lower = src.toLowerCase();
|
|
340
|
+
const destName = lower.endsWith('.py')
|
|
341
|
+
? 'test.selenium.py'
|
|
342
|
+
: lower.endsWith('.ts') || lower.endsWith('.tsx')
|
|
343
|
+
? 'playwright.spec.ts'
|
|
344
|
+
: 'generated-test.spec.js';
|
|
345
|
+
const dest = join(gsDir, destName);
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
if (existsSync(dest)) {
|
|
349
|
+
try {
|
|
350
|
+
if (statSync(dest).mtimeMs >= statSync(src).mtimeMs) return;
|
|
351
|
+
} catch {
|
|
352
|
+
/* replace if unsure */
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
mkdirSync(gsDir, { recursive: true });
|
|
356
|
+
copyFileSync(src, dest);
|
|
357
|
+
logger.info(`[Studio] Mirrored script for discovery: ${dest}`);
|
|
358
|
+
} catch (e) {
|
|
359
|
+
logger.debug(`[Studio] Codegen mirror failed: ${e.message}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
286
362
|
}
|