@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,65 @@
|
|
|
1
|
+
import { getSkill } from './skill-registry.js';
|
|
2
|
+
|
|
3
|
+
const NODE_DEFAULT_TOOLS = {};
|
|
4
|
+
|
|
5
|
+
export function resolveNodeTools(nodeType, userToolIds) {
|
|
6
|
+
if (Array.isArray(userToolIds)) {
|
|
7
|
+
return getResolvedToolDefinitions(userToolIds);
|
|
8
|
+
}
|
|
9
|
+
const defaults = NODE_DEFAULT_TOOLS[nodeType];
|
|
10
|
+
if (!defaults || defaults.length === 0) return null;
|
|
11
|
+
return getResolvedToolDefinitions(defaults);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getResolvedToolDefinitions(toolIds) {
|
|
15
|
+
if (!Array.isArray(toolIds) || toolIds.length === 0) return null;
|
|
16
|
+
|
|
17
|
+
const claudeTools = [];
|
|
18
|
+
const mcpServers = {};
|
|
19
|
+
const validIds = [];
|
|
20
|
+
|
|
21
|
+
for (const toolId of toolIds) {
|
|
22
|
+
const skill = getSkill(toolId);
|
|
23
|
+
if (!skill) {
|
|
24
|
+
console.warn(`[ToolResolver] Unknown skill "${toolId}" — skipping`);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
validIds.push(toolId);
|
|
29
|
+
|
|
30
|
+
for (const tool of (skill.tools || [])) {
|
|
31
|
+
claudeTools.push({
|
|
32
|
+
name: tool.name,
|
|
33
|
+
description: tool.description,
|
|
34
|
+
input_schema: tool.input_schema || { type: 'object', properties: {} }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!mcpServers[skill.serverName]) {
|
|
39
|
+
if (typeof skill.resolve === 'function') {
|
|
40
|
+
const resolved = skill.resolve();
|
|
41
|
+
if (resolved) {
|
|
42
|
+
mcpServers[skill.serverName] = { ...resolved, toolPrefix: toolId };
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
const env = {};
|
|
46
|
+
for (const key of (skill.envKeys || [])) {
|
|
47
|
+
const value = process.env[key];
|
|
48
|
+
if (value) env[key] = value;
|
|
49
|
+
}
|
|
50
|
+
mcpServers[skill.serverName] = {
|
|
51
|
+
command: skill.command,
|
|
52
|
+
args: [...(skill.args || [])],
|
|
53
|
+
env,
|
|
54
|
+
toolPrefix: toolId,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (validIds.length === 0) return null;
|
|
61
|
+
|
|
62
|
+
return { toolIds: validIds, claudeTools, mcpServers };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { NODE_DEFAULT_TOOLS };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { DEFAULT_OUTPUT_BASE, SESSIONS_DIR, RESULT_FILE } from './framework/constants.js';
|
|
4
|
+
|
|
5
|
+
import { WorkflowAgent, workflow } from './agents/base.js';
|
|
6
|
+
export { WorkflowAgent, workflow };
|
|
7
|
+
export { WorkflowGraph } from './framework/graph.js';
|
|
8
|
+
export { ResultHandler } from './utils/result-handler.js';
|
|
9
|
+
export { z } from 'zod';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
invokeAgent,
|
|
13
|
+
getAgentStrategy,
|
|
14
|
+
CursorAgentStrategy,
|
|
15
|
+
ClaudeAgentStrategy,
|
|
16
|
+
AgentStrategy
|
|
17
|
+
} from './framework/agents/index.js';
|
|
18
|
+
|
|
19
|
+
export { SKILLS } from './framework/constants.js';
|
|
20
|
+
export { registerSkill, getSkill, hasSkill, getAllSkills, listSkillIds } from './framework/skill-registry.js';
|
|
21
|
+
|
|
22
|
+
export { organizeVideos } from './utils/video-organizer.js';
|
|
23
|
+
export { ZibbyUploader, createUploader } from './sync/index.js';
|
|
24
|
+
export { patchCursorAgentForCI, checkCursorAgentPatched, getApprovalKeys, saveApprovalKeys } from './utils/ci-setup.js';
|
|
25
|
+
export { DEFAULT_MODELS, AGENT_TYPES, LOG_LEVELS as CORE_LOG_LEVELS } from './constants.js';
|
|
26
|
+
export {
|
|
27
|
+
DEFAULT_OUTPUT_BASE,
|
|
28
|
+
SESSIONS_DIR,
|
|
29
|
+
SESSION_INFO_FILE,
|
|
30
|
+
RESULT_FILE,
|
|
31
|
+
RAW_OUTPUT_FILE,
|
|
32
|
+
EVENTS_FILE,
|
|
33
|
+
CI_ENV_VARS
|
|
34
|
+
} from './framework/constants.js';
|
|
35
|
+
export { RIPPLE_EFFECT_SCRIPT, injectRippleEffect, generateRippleHelperCode } from './utils/ripple-effect.js';
|
|
36
|
+
export { checkCursorAgentInstalled, getCursorAgentInstallInstructions } from './utils/cursor-utils.js';
|
|
37
|
+
export { SelectorGenerator } from './utils/selector-generator.js';
|
|
38
|
+
export { TestPostProcessor } from './utils/test-post-processor.js';
|
|
39
|
+
export { TraceParser } from './utils/trace-parser.js';
|
|
40
|
+
export { StreamingParser } from './utils/streaming-parser.js';
|
|
41
|
+
export { ZibbyRuntime } from './runtime/zibby-runtime.js';
|
|
42
|
+
export { StableIdRuntime } from './runtime/stable-id-runtime.js';
|
|
43
|
+
export { logger, Logger, LOG_LEVELS } from './utils/logger.js';
|
|
44
|
+
export { timeline } from './utils/timeline.js';
|
|
45
|
+
export { postProcessEvents } from './utils/post-process-events.js';
|
|
46
|
+
export { runPlaywrightTestTool, resetExecutionCount } from './tools/run-playwright-test.js';
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
testGenerationManager,
|
|
50
|
+
TestGenerationStrategy,
|
|
51
|
+
MCPRefStrategy,
|
|
52
|
+
StableIdStrategy
|
|
53
|
+
} from './runtime/generation/index.js';
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
testVerificationManager,
|
|
57
|
+
TestVerificationStrategy,
|
|
58
|
+
PlaywrightJsonVerificationStrategy
|
|
59
|
+
} from './runtime/verification/index.js';
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
EventEnricher,
|
|
63
|
+
EnrichmentPipeline,
|
|
64
|
+
PositionEnricher,
|
|
65
|
+
AccessibilityEnricher,
|
|
66
|
+
PageStateEnricher,
|
|
67
|
+
DOMEnricher,
|
|
68
|
+
createDefaultPipeline,
|
|
69
|
+
createMinimalPipeline,
|
|
70
|
+
createCustomPipeline
|
|
71
|
+
} from './enrichment/index.js';
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
enrichRecordedEvents,
|
|
75
|
+
LiveEnrichmentRecorder
|
|
76
|
+
} from './enrichment/mcp-integration.js';
|
|
77
|
+
|
|
78
|
+
const mcpErrorHandler = (error) => {
|
|
79
|
+
if (error?.message?.includes('Connection closed') ||
|
|
80
|
+
error?.message?.includes('MCP error -32000') ||
|
|
81
|
+
error?.code === -32000) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
console.error('Unhandled rejection:', error);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (!process.listeners('unhandledRejection').includes(mcpErrorHandler)) {
|
|
88
|
+
process.on('unhandledRejection', mcpErrorHandler);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function runTest(specPath, config = {}) {
|
|
92
|
+
const {
|
|
93
|
+
agent: _agent,
|
|
94
|
+
mcp: _mcp,
|
|
95
|
+
headless: _headless,
|
|
96
|
+
cwd = process.cwd(),
|
|
97
|
+
specPath: relativeSpecPath,
|
|
98
|
+
...agentConfig
|
|
99
|
+
} = config;
|
|
100
|
+
|
|
101
|
+
const testSpec = readFileSync(specPath, 'utf-8');
|
|
102
|
+
|
|
103
|
+
const adapter = null;
|
|
104
|
+
let agent = await loadLocalAgent(cwd, agentConfig);
|
|
105
|
+
|
|
106
|
+
if (!agent && config.fallbackAgentModule) {
|
|
107
|
+
const mod = config.fallbackAgentModule;
|
|
108
|
+
const AgentClass = mod.BrowserTestAutomationAgent || mod.default;
|
|
109
|
+
if (AgentClass) {
|
|
110
|
+
agent = new AgentClass(agentConfig);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!agent) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`No agent found. Please run:\n` +
|
|
117
|
+
` zibby init\n\n` +
|
|
118
|
+
`This will create .zibby/graph.js with your workflow definition.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await agent.initialize(adapter);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
if (config.singleNode) {
|
|
126
|
+
console.log(`\n🎯 Running Single Node: ${config.singleNode} (Framework Mode)\n`);
|
|
127
|
+
|
|
128
|
+
const outputPath = agent.calculateOutputPath(relativeSpecPath || specPath);
|
|
129
|
+
|
|
130
|
+
const graph = agent.buildGraph();
|
|
131
|
+
const nodeMap = {};
|
|
132
|
+
for (const [name, node] of graph.nodes.entries()) {
|
|
133
|
+
nodeMap[name] = node.config || node;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let sessionData = {};
|
|
137
|
+
if (config.sessionId) {
|
|
138
|
+
let sessionId = config.sessionId;
|
|
139
|
+
|
|
140
|
+
// Get output base path from config (use new default)
|
|
141
|
+
const outputBase = config.paths?.output || DEFAULT_OUTPUT_BASE;
|
|
142
|
+
|
|
143
|
+
if (sessionId === 'last') {
|
|
144
|
+
const sessionsDir = join(cwd, outputBase, SESSIONS_DIR);
|
|
145
|
+
if (existsSync(sessionsDir)) {
|
|
146
|
+
const { readdirSync, statSync } = await import('fs');
|
|
147
|
+
const sessions = readdirSync(sessionsDir)
|
|
148
|
+
.filter(f => statSync(join(sessionsDir, f)).isDirectory())
|
|
149
|
+
.map(f => ({ name: f, time: statSync(join(sessionsDir, f)).mtimeMs }))
|
|
150
|
+
.sort((a, b) => b.time - a.time);
|
|
151
|
+
|
|
152
|
+
if (sessions.length > 0) {
|
|
153
|
+
sessionId = sessions[0].name;
|
|
154
|
+
console.log(`📂 Using latest session: ${sessionId}`);
|
|
155
|
+
} else {
|
|
156
|
+
console.log(`⚠️ No sessions found in ${sessionsDir}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const sessionPath = join(cwd, outputBase, SESSIONS_DIR, sessionId);
|
|
162
|
+
const executeLiveFolder = join(sessionPath, 'execute_live');
|
|
163
|
+
const resultPath = join(executeLiveFolder, RESULT_FILE);
|
|
164
|
+
|
|
165
|
+
if (existsSync(resultPath)) {
|
|
166
|
+
console.log(`📂 Loading session: ${sessionId}`);
|
|
167
|
+
sessionData = {
|
|
168
|
+
sessionPath,
|
|
169
|
+
execute_live_output: JSON.parse(readFileSync(resultPath, 'utf-8'))
|
|
170
|
+
};
|
|
171
|
+
} else {
|
|
172
|
+
console.log(`⚠️ Session not found: ${sessionPath}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = await agent.runSingleNode(
|
|
177
|
+
config.singleNode,
|
|
178
|
+
nodeMap,
|
|
179
|
+
{
|
|
180
|
+
testSpec,
|
|
181
|
+
outputPath,
|
|
182
|
+
cwd: cwd || process.cwd(),
|
|
183
|
+
contextConfig: config.contextConfig,
|
|
184
|
+
specPath: relativeSpecPath || specPath,
|
|
185
|
+
config,
|
|
186
|
+
...sessionData
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (typeof agent.onComplete === 'function') {
|
|
191
|
+
await agent.onComplete(result);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const result = await agent.run(testSpec, {
|
|
198
|
+
testSpec,
|
|
199
|
+
specPath: relativeSpecPath || specPath,
|
|
200
|
+
cwd: cwd || process.cwd(),
|
|
201
|
+
config
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return result;
|
|
205
|
+
} finally {
|
|
206
|
+
await agent.cleanup();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function kebabToPascal(str) {
|
|
211
|
+
return str
|
|
212
|
+
.split('-')
|
|
213
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
214
|
+
.join('');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function findWorkflow(agentModule, workflowName) {
|
|
218
|
+
if (agentModule[workflowName]) {
|
|
219
|
+
return agentModule[workflowName];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const pascalCase = kebabToPascal(workflowName);
|
|
223
|
+
if (agentModule[pascalCase]) {
|
|
224
|
+
return agentModule[pascalCase];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const withSuffix = `${pascalCase}Workflow`;
|
|
228
|
+
if (agentModule[withSuffix]) {
|
|
229
|
+
return agentModule[withSuffix];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function listWorkflows(cwd = process.cwd()) {
|
|
236
|
+
try {
|
|
237
|
+
const { join: pathJoin } = await import('path');
|
|
238
|
+
const { existsSync: fsExistsSync } = await import('fs');
|
|
239
|
+
const { pathToFileURL } = await import('url');
|
|
240
|
+
|
|
241
|
+
const localAgentPath = pathJoin(cwd, '.zibby/graph.js');
|
|
242
|
+
|
|
243
|
+
if (!fsExistsSync(localAgentPath)) {
|
|
244
|
+
return { available: [], default: null, error: 'No .zibby/graph.js found' };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const agentModule = await import(pathToFileURL(localAgentPath).href);
|
|
248
|
+
const available = Object.keys(agentModule).filter(k =>
|
|
249
|
+
k !== 'default' &&
|
|
250
|
+
typeof agentModule[k] === 'function' &&
|
|
251
|
+
agentModule[k].prototype instanceof WorkflowAgent
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const defaultWorkflow = agentModule.BrowserTestAutomationAgent ? 'BrowserTestAutomationAgent' :
|
|
255
|
+
agentModule.CursorAgent ? 'CursorAgent' :
|
|
256
|
+
agentModule.default ? 'default' :
|
|
257
|
+
available[0] || null;
|
|
258
|
+
|
|
259
|
+
return { available, default: defaultWorkflow, error: null };
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return { available: [], default: null, error: error.message };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function loadLocalAgent(cwd, config) {
|
|
266
|
+
try {
|
|
267
|
+
const { join: pathJoin } = await import('path');
|
|
268
|
+
const { existsSync: fsExistsSync } = await import('fs');
|
|
269
|
+
const { pathToFileURL } = await import('url');
|
|
270
|
+
|
|
271
|
+
const localAgentPath = pathJoin(cwd, '.zibby/graph.js');
|
|
272
|
+
|
|
273
|
+
if (!fsExistsSync(localAgentPath)) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const agentModule = await import(pathToFileURL(localAgentPath).href);
|
|
278
|
+
|
|
279
|
+
const workflowName = config.workflow;
|
|
280
|
+
let AgentClass;
|
|
281
|
+
|
|
282
|
+
if (workflowName) {
|
|
283
|
+
AgentClass = findWorkflow(agentModule, workflowName);
|
|
284
|
+
if (!AgentClass) {
|
|
285
|
+
const available = Object.keys(agentModule).filter(k => k !== 'default' && typeof agentModule[k] === 'function');
|
|
286
|
+
throw new Error(
|
|
287
|
+
`Workflow "${workflowName}" not found.\n` +
|
|
288
|
+
`Available workflows: ${available.join(', ')}\n` +
|
|
289
|
+
`Supported formats: QuickSmokeWorkflow, QuickSmoke, quick-smoke`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const actualName = Object.keys(agentModule).find(k => agentModule[k] === AgentClass);
|
|
293
|
+
console.log(`✓ Using workflow: ${actualName} (from --workflow ${workflowName})`);
|
|
294
|
+
} else {
|
|
295
|
+
// Try multiple fallbacks for default workflow
|
|
296
|
+
AgentClass = agentModule.BrowserTestAutomationAgent ||
|
|
297
|
+
agentModule.CursorAgent ||
|
|
298
|
+
agentModule.default;
|
|
299
|
+
|
|
300
|
+
// If still not found, try to find any workflow class
|
|
301
|
+
if (!AgentClass) {
|
|
302
|
+
const availableClasses = Object.keys(agentModule).filter(k =>
|
|
303
|
+
k !== 'default' &&
|
|
304
|
+
typeof agentModule[k] === 'function' &&
|
|
305
|
+
agentModule[k].prototype instanceof WorkflowAgent
|
|
306
|
+
);
|
|
307
|
+
if (availableClasses.length > 0) {
|
|
308
|
+
AgentClass = agentModule[availableClasses[0]];
|
|
309
|
+
console.log(`✓ Using workflow: ${availableClasses[0]} (auto-detected)`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!AgentClass) {
|
|
314
|
+
console.warn('⚠️ Could not find any WorkflowAgent export in local graph.js');
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!AgentClass.name?.includes('auto-detected')) {
|
|
319
|
+
console.log('✓ Using local agent from .zibby/graph.js');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return new AgentClass(config);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.warn(`⚠️ Failed to load local agent: ${error.message}`);
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export class TestAutomation {
|
|
331
|
+
constructor(config = {}) {
|
|
332
|
+
this.config = config;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async run(options) {
|
|
336
|
+
return runTest(options.spec || options.specPath, {
|
|
337
|
+
...this.config,
|
|
338
|
+
...options,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for test generation strategies
|
|
3
|
+
* Defines the interface that all generation strategies must implement
|
|
4
|
+
*/
|
|
5
|
+
export class TestGenerationStrategy {
|
|
6
|
+
/**
|
|
7
|
+
* Generate a test file from execution data
|
|
8
|
+
* @param {Object} context - Generation context
|
|
9
|
+
* @param {string} context.testFilePath - Where to write the test
|
|
10
|
+
* @param {string} context.sessionPath - Session data path
|
|
11
|
+
* @param {string} context.outputPath - Output file path
|
|
12
|
+
* @param {Object} context.state - Workflow state
|
|
13
|
+
* @param {Object} context.executionData - Data from execute_live node
|
|
14
|
+
* @returns {Promise<Object>} - { success: boolean, testPath: string, method: string }
|
|
15
|
+
*/
|
|
16
|
+
async generate(_context) {
|
|
17
|
+
throw new Error('TestGenerationStrategy.generate() must be implemented');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if this strategy can be used with available data
|
|
22
|
+
* @param {Object} context - Same context as generate()
|
|
23
|
+
* @returns {boolean}
|
|
24
|
+
*/
|
|
25
|
+
canGenerate(_context) {
|
|
26
|
+
throw new Error('TestGenerationStrategy.canGenerate() must be implemented');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get strategy name for logging
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
getName() {
|
|
34
|
+
throw new Error('TestGenerationStrategy.getName() must be implemented');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get strategy priority (higher = try first)
|
|
39
|
+
* @returns {number}
|
|
40
|
+
*/
|
|
41
|
+
getPriority() {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default TestGenerationStrategy;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Generation Strategy Manager
|
|
3
|
+
* Automatically selects and runs the best available generation strategy
|
|
4
|
+
*/
|
|
5
|
+
import { MCPRefStrategy } from './mcp-ref-strategy.js';
|
|
6
|
+
import { StableIdStrategy } from './stable-id-strategy.js';
|
|
7
|
+
|
|
8
|
+
export class TestGenerationStrategyManager {
|
|
9
|
+
constructor() {
|
|
10
|
+
// Register all available strategies (auto-sorted by priority)
|
|
11
|
+
this.strategies = [
|
|
12
|
+
new StableIdStrategy(), // Priority 300 - Highest (stable ID injection)
|
|
13
|
+
new MCPRefStrategy(), // Priority 200 - Fallback (MCP descriptions)
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// Sort by priority (highest first)
|
|
17
|
+
this.strategies.sort((a, b) => b.getPriority() - a.getPriority());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Add a custom strategy
|
|
22
|
+
* @param {TestGenerationStrategy} strategy
|
|
23
|
+
*/
|
|
24
|
+
registerStrategy(strategy) {
|
|
25
|
+
this.strategies.push(strategy);
|
|
26
|
+
this.strategies.sort((a, b) => b.getPriority() - a.getPriority());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generate test using the best available strategy
|
|
31
|
+
* @param {Object} context - Generation context
|
|
32
|
+
* @returns {Promise<Object>} - Generation result
|
|
33
|
+
*/
|
|
34
|
+
async generate(context) {
|
|
35
|
+
console.log(`\n📋 Available generation strategies (${this.strategies.length}):`);
|
|
36
|
+
this.strategies.forEach(s => {
|
|
37
|
+
const canUse = s.canGenerate(context);
|
|
38
|
+
console.log(` ${canUse ? '✓' : '✗'} ${s.getName()} (priority: ${s.getPriority()})`);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Find first strategy that can generate
|
|
42
|
+
for (const strategy of this.strategies) {
|
|
43
|
+
if (strategy.canGenerate(context)) {
|
|
44
|
+
console.log(`\n🎯 Selected: ${strategy.getName()}`);
|
|
45
|
+
return strategy.generate(context);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new Error('No generation strategy available for this context');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get strategy by name
|
|
54
|
+
* @param {string} name - Strategy name
|
|
55
|
+
* @returns {TestGenerationStrategy|null}
|
|
56
|
+
*/
|
|
57
|
+
getStrategy(name) {
|
|
58
|
+
return this.strategies.find(s => s.getName().includes(name)) || null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Export strategy classes for custom implementations
|
|
63
|
+
export { MCPRefStrategy } from './mcp-ref-strategy.js';
|
|
64
|
+
export { StableIdStrategy } from './stable-id-strategy.js';
|
|
65
|
+
export { TestGenerationStrategy } from './base.js';
|
|
66
|
+
|
|
67
|
+
// Export singleton instance
|
|
68
|
+
export const testGenerationManager = new TestGenerationStrategyManager();
|
|
69
|
+
|
|
70
|
+
export default testGenerationManager;
|