@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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/package.json +94 -0
  4. package/src/agents/base.js +361 -0
  5. package/src/constants.js +47 -0
  6. package/src/enrichment/base.js +49 -0
  7. package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
  8. package/src/enrichment/enrichers/dom-enricher.js +171 -0
  9. package/src/enrichment/enrichers/page-state-enricher.js +129 -0
  10. package/src/enrichment/enrichers/position-enricher.js +67 -0
  11. package/src/enrichment/index.js +96 -0
  12. package/src/enrichment/mcp-integration.js +149 -0
  13. package/src/enrichment/mcp-ref-enricher.js +78 -0
  14. package/src/enrichment/pipeline.js +192 -0
  15. package/src/enrichment/trace-text-enricher.js +115 -0
  16. package/src/framework/AGENTS.md +98 -0
  17. package/src/framework/agents/base.js +72 -0
  18. package/src/framework/agents/claude-strategy.js +278 -0
  19. package/src/framework/agents/cursor-strategy.js +459 -0
  20. package/src/framework/agents/index.js +105 -0
  21. package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
  22. package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
  23. package/src/framework/code-generator.js +301 -0
  24. package/src/framework/constants.js +33 -0
  25. package/src/framework/context-loader.js +101 -0
  26. package/src/framework/function-bridge.js +78 -0
  27. package/src/framework/function-skill-registry.js +20 -0
  28. package/src/framework/graph-compiler.js +342 -0
  29. package/src/framework/graph.js +610 -0
  30. package/src/framework/index.js +28 -0
  31. package/src/framework/node-registry.js +163 -0
  32. package/src/framework/node.js +259 -0
  33. package/src/framework/output-parser.js +71 -0
  34. package/src/framework/skill-registry.js +55 -0
  35. package/src/framework/state-utils.js +52 -0
  36. package/src/framework/state.js +67 -0
  37. package/src/framework/tool-resolver.js +65 -0
  38. package/src/index.js +342 -0
  39. package/src/runtime/generation/base.js +46 -0
  40. package/src/runtime/generation/index.js +70 -0
  41. package/src/runtime/generation/mcp-ref-strategy.js +197 -0
  42. package/src/runtime/generation/stable-id-strategy.js +170 -0
  43. package/src/runtime/stable-id-runtime.js +248 -0
  44. package/src/runtime/verification/base.js +44 -0
  45. package/src/runtime/verification/index.js +67 -0
  46. package/src/runtime/verification/playwright-json-strategy.js +119 -0
  47. package/src/runtime/zibby-runtime.js +299 -0
  48. package/src/sync/index.js +2 -0
  49. package/src/sync/uploader.js +29 -0
  50. package/src/tools/run-playwright-test.js +158 -0
  51. package/src/utils/adf-converter.js +68 -0
  52. package/src/utils/ast-utils.js +37 -0
  53. package/src/utils/ci-setup.js +124 -0
  54. package/src/utils/cursor-utils.js +71 -0
  55. package/src/utils/logger.js +144 -0
  56. package/src/utils/mcp-config-writer.js +115 -0
  57. package/src/utils/node-schema-parser.js +522 -0
  58. package/src/utils/post-process-events.js +55 -0
  59. package/src/utils/result-handler.js +102 -0
  60. package/src/utils/ripple-effect.js +84 -0
  61. package/src/utils/selector-generator.js +239 -0
  62. package/src/utils/streaming-parser.js +387 -0
  63. package/src/utils/test-post-processor.js +211 -0
  64. package/src/utils/timeline.js +217 -0
  65. package/src/utils/trace-parser.js +325 -0
  66. package/src/utils/video-organizer.js +91 -0
  67. package/templates/browser-test-automation/README.md +114 -0
  68. package/templates/browser-test-automation/graph.js +54 -0
  69. package/templates/browser-test-automation/nodes/execute-live.js +250 -0
  70. package/templates/browser-test-automation/nodes/generate-script.js +77 -0
  71. package/templates/browser-test-automation/nodes/index.js +3 -0
  72. package/templates/browser-test-automation/nodes/preflight.js +59 -0
  73. package/templates/browser-test-automation/nodes/utils.js +154 -0
  74. package/templates/browser-test-automation/result-handler.js +286 -0
  75. package/templates/code-analysis/graph.js +72 -0
  76. package/templates/code-analysis/index.js +18 -0
  77. package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
  78. package/templates/code-analysis/nodes/create-pr-node.js +175 -0
  79. package/templates/code-analysis/nodes/finalize-node.js +118 -0
  80. package/templates/code-analysis/nodes/generate-code-node.js +425 -0
  81. package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
  82. package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
  83. package/templates/code-analysis/nodes/setup-node.js +142 -0
  84. package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
  85. package/templates/code-analysis/prompts/generate-code.md +33 -0
  86. package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
  87. package/templates/code-analysis/state.js +40 -0
  88. package/templates/code-implementation/graph.js +35 -0
  89. package/templates/code-implementation/index.js +7 -0
  90. package/templates/code-implementation/state.js +14 -0
  91. package/templates/global-setup.js +56 -0
  92. package/templates/index.js +94 -0
  93. 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;