keystone-cli 0.7.2 → 1.0.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 (104) hide show
  1. package/README.md +486 -54
  2. package/package.json +8 -2
  3. package/src/__fixtures__/index.ts +100 -0
  4. package/src/cli.ts +841 -91
  5. package/src/db/memory-db.ts +35 -1
  6. package/src/db/workflow-db.test.ts +24 -0
  7. package/src/db/workflow-db.ts +484 -14
  8. package/src/expression/evaluator.ts +68 -4
  9. package/src/parser/agent-parser.ts +6 -3
  10. package/src/parser/config-schema.ts +38 -2
  11. package/src/parser/schema.ts +192 -7
  12. package/src/parser/test-schema.ts +29 -0
  13. package/src/parser/workflow-parser.test.ts +54 -0
  14. package/src/parser/workflow-parser.ts +153 -7
  15. package/src/runner/aggregate-error.test.ts +57 -0
  16. package/src/runner/aggregate-error.ts +46 -0
  17. package/src/runner/audit-verification.test.ts +2 -2
  18. package/src/runner/auto-heal.test.ts +1 -1
  19. package/src/runner/blueprint-executor.test.ts +63 -0
  20. package/src/runner/blueprint-executor.ts +157 -0
  21. package/src/runner/concurrency-limit.test.ts +82 -0
  22. package/src/runner/debug-repl.ts +18 -3
  23. package/src/runner/durable-timers.test.ts +200 -0
  24. package/src/runner/engine-executor.test.ts +464 -0
  25. package/src/runner/engine-executor.ts +491 -0
  26. package/src/runner/foreach-executor.ts +30 -12
  27. package/src/runner/llm-adapter.test.ts +282 -5
  28. package/src/runner/llm-adapter.ts +581 -8
  29. package/src/runner/llm-clarification.test.ts +79 -21
  30. package/src/runner/llm-errors.ts +83 -0
  31. package/src/runner/llm-executor.test.ts +258 -219
  32. package/src/runner/llm-executor.ts +226 -29
  33. package/src/runner/mcp-client.ts +70 -3
  34. package/src/runner/mcp-manager.test.ts +52 -52
  35. package/src/runner/mcp-manager.ts +12 -5
  36. package/src/runner/mcp-server.test.ts +117 -78
  37. package/src/runner/mcp-server.ts +13 -4
  38. package/src/runner/optimization-runner.ts +48 -31
  39. package/src/runner/reflexion.test.ts +1 -1
  40. package/src/runner/resource-pool.test.ts +113 -0
  41. package/src/runner/resource-pool.ts +164 -0
  42. package/src/runner/shell-executor.ts +130 -32
  43. package/src/runner/standard-tools-execution.test.ts +39 -0
  44. package/src/runner/standard-tools-integration.test.ts +36 -36
  45. package/src/runner/standard-tools.test.ts +18 -0
  46. package/src/runner/standard-tools.ts +174 -93
  47. package/src/runner/step-executor.test.ts +176 -16
  48. package/src/runner/step-executor.ts +534 -83
  49. package/src/runner/stream-utils.test.ts +14 -0
  50. package/src/runner/subflow-outputs.test.ts +103 -0
  51. package/src/runner/test-harness.ts +161 -0
  52. package/src/runner/tool-integration.test.ts +73 -79
  53. package/src/runner/workflow-runner.test.ts +549 -15
  54. package/src/runner/workflow-runner.ts +1448 -79
  55. package/src/runner/workflow-subflows.test.ts +255 -0
  56. package/src/templates/agents/keystone-architect.md +17 -12
  57. package/src/templates/agents/tester.md +21 -0
  58. package/src/templates/child-rollback.yaml +11 -0
  59. package/src/templates/decompose-implement.yaml +53 -0
  60. package/src/templates/decompose-problem.yaml +159 -0
  61. package/src/templates/decompose-research.yaml +52 -0
  62. package/src/templates/decompose-review.yaml +51 -0
  63. package/src/templates/dev.yaml +134 -0
  64. package/src/templates/engine-example.yaml +33 -0
  65. package/src/templates/fan-out-fan-in.yaml +61 -0
  66. package/src/templates/memory-service.yaml +1 -1
  67. package/src/templates/parent-rollback.yaml +16 -0
  68. package/src/templates/robust-automation.yaml +1 -1
  69. package/src/templates/scaffold-feature.yaml +29 -27
  70. package/src/templates/scaffold-generate.yaml +41 -0
  71. package/src/templates/scaffold-plan.yaml +53 -0
  72. package/src/types/status.ts +3 -0
  73. package/src/ui/dashboard.tsx +4 -3
  74. package/src/utils/assets.macro.ts +36 -0
  75. package/src/utils/auth-manager.ts +585 -8
  76. package/src/utils/blueprint-utils.test.ts +49 -0
  77. package/src/utils/blueprint-utils.ts +80 -0
  78. package/src/utils/circuit-breaker.test.ts +177 -0
  79. package/src/utils/circuit-breaker.ts +160 -0
  80. package/src/utils/config-loader.test.ts +100 -13
  81. package/src/utils/config-loader.ts +44 -17
  82. package/src/utils/constants.ts +62 -0
  83. package/src/utils/error-renderer.test.ts +267 -0
  84. package/src/utils/error-renderer.ts +320 -0
  85. package/src/utils/json-parser.test.ts +4 -0
  86. package/src/utils/json-parser.ts +18 -1
  87. package/src/utils/mermaid.ts +4 -0
  88. package/src/utils/paths.test.ts +46 -0
  89. package/src/utils/paths.ts +70 -0
  90. package/src/utils/process-sandbox.test.ts +128 -0
  91. package/src/utils/process-sandbox.ts +293 -0
  92. package/src/utils/rate-limiter.test.ts +143 -0
  93. package/src/utils/rate-limiter.ts +221 -0
  94. package/src/utils/redactor.test.ts +23 -15
  95. package/src/utils/redactor.ts +65 -25
  96. package/src/utils/resource-loader.test.ts +54 -0
  97. package/src/utils/resource-loader.ts +158 -0
  98. package/src/utils/sandbox.test.ts +69 -4
  99. package/src/utils/sandbox.ts +69 -6
  100. package/src/utils/schema-validator.ts +65 -0
  101. package/src/utils/workflow-registry.test.ts +57 -0
  102. package/src/utils/workflow-registry.ts +45 -25
  103. /package/src/expression/{evaluator.audit.test.ts → evaluator-audit.test.ts} +0 -0
  104. /package/src/runner/{mcp-client.audit.test.ts → mcp-client-audit.test.ts} +0 -0
@@ -52,6 +52,20 @@ describe('processOpenAIStream', () => {
52
52
  expect(onStream).toHaveBeenCalledTimes(1);
53
53
  });
54
54
 
55
+ it('handles tool calls in a final line without a newline', async () => {
56
+ const response = responseFromChunks([
57
+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"final_tool","arguments":"{\\"x\\":1}"}}]}}]}',
58
+ ]);
59
+
60
+ const result = await processOpenAIStream(response);
61
+
62
+ expect(result.message.tool_calls?.[0]).toEqual({
63
+ id: 'call_1',
64
+ type: 'function',
65
+ function: { name: 'final_tool', arguments: '{"x":1}' },
66
+ });
67
+ });
68
+
55
69
  it('logs malformed JSON and continues processing', async () => {
56
70
  const logger = {
57
71
  log: mock(() => {}),
@@ -0,0 +1,103 @@
1
+ import { describe, expect, it, mock, spyOn } from 'bun:test';
2
+ import type { Workflow } from '../parser/schema';
3
+ import { WorkflowParser } from '../parser/workflow-parser';
4
+ import { WorkflowRegistry } from '../utils/workflow-registry';
5
+ import { WorkflowRunner } from './workflow-runner';
6
+
7
+ describe('Sub-workflow Output Mapping and Contracts', () => {
8
+ const dbPath = ':memory:';
9
+
10
+ it('should support workflow output schema validation', async () => {
11
+ const workflow: Workflow = {
12
+ name: 'contract-wf',
13
+ steps: [{ id: 's1', type: 'shell', run: 'echo "hello"', needs: [] }],
14
+ outputs: {
15
+ val: '${{ steps.s1.output.stdout.trim() }}',
16
+ },
17
+ outputSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ val: { type: 'number' }, // Should fail because it's a string
21
+ },
22
+ required: ['val'],
23
+ },
24
+ } as unknown as Workflow;
25
+
26
+ const runner = new WorkflowRunner(workflow, { dbPath });
27
+ await expect(runner.run()).rejects.toThrow(/Workflow output validation failed/);
28
+ });
29
+
30
+ it('should support namespacing and explicit mapping for sub-workflows', async () => {
31
+ const childWorkflow: Workflow = {
32
+ name: 'child-wf',
33
+ steps: [
34
+ { id: 'cs1', type: 'shell', run: 'echo "v1"', needs: [] },
35
+ { id: 'cs2', type: 'shell', run: 'echo "v2"', needs: [] },
36
+ ],
37
+ outputs: {
38
+ foo: '${{ steps.cs1.output.stdout.trim() }}',
39
+ bar: '${{ steps.cs2.output.stdout.trim() }}',
40
+ },
41
+ } as unknown as Workflow;
42
+
43
+ const parentWorkflow: Workflow = {
44
+ name: 'parent-wf',
45
+ steps: [
46
+ {
47
+ id: 'sub',
48
+ type: 'workflow',
49
+ path: 'child.yaml',
50
+ needs: [],
51
+ outputMapping: {
52
+ mappedFoo: 'foo',
53
+ withDefault: { from: 'missing', default: 'fallback' },
54
+ },
55
+ },
56
+ ],
57
+ outputs: {
58
+ foo: '${{ steps.sub.output.mappedFoo }}',
59
+ rawFoo: '${{ steps.sub.output.outputs.foo }}',
60
+ def: '${{ steps.sub.output.withDefault }}',
61
+ },
62
+ } as unknown as Workflow;
63
+
64
+ spyOn(WorkflowRegistry, 'resolvePath').mockReturnValue('child.yaml');
65
+ spyOn(WorkflowParser, 'loadWorkflow').mockReturnValue(childWorkflow);
66
+
67
+ const runner = new WorkflowRunner(parentWorkflow, { dbPath });
68
+ const outputs = await runner.run();
69
+
70
+ expect(outputs.foo).toBe('v1');
71
+ expect(outputs.rawFoo).toBe('v1');
72
+ expect(outputs.def).toBe('fallback');
73
+ });
74
+
75
+ it('should fail if mapped output is missing and no default is provided', async () => {
76
+ const childWorkflow: Workflow = {
77
+ name: 'child-wf',
78
+ steps: [{ id: 'cs1', type: 'shell', run: 'echo "ok"', needs: [] }],
79
+ outputs: { ok: 'true' },
80
+ } as unknown as Workflow;
81
+
82
+ const parentWorkflow: Workflow = {
83
+ name: 'parent-wf',
84
+ steps: [
85
+ {
86
+ id: 'sub',
87
+ type: 'workflow',
88
+ path: 'child.yaml',
89
+ needs: [],
90
+ outputMapping: {
91
+ missing: 'nonexistent',
92
+ },
93
+ },
94
+ ],
95
+ } as unknown as Workflow;
96
+
97
+ spyOn(WorkflowRegistry, 'resolvePath').mockReturnValue('child.yaml');
98
+ spyOn(WorkflowParser, 'loadWorkflow').mockReturnValue(childWorkflow);
99
+
100
+ const runner = new WorkflowRunner(parentWorkflow, { dbPath });
101
+ await expect(runner.run()).rejects.toThrow(/Sub-workflow output "nonexistent" not found/);
102
+ });
103
+ });
@@ -0,0 +1,161 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { type ExpressionContext, ExpressionEvaluator } from '../expression/evaluator';
5
+ import type { Step, Workflow } from '../parser/schema';
6
+ import { ConsoleLogger, type Logger } from '../utils/logger';
7
+ import type { LLMAdapter, LLMMessage, LLMResponse } from './llm-adapter';
8
+ import { type StepExecutorOptions, type StepResult, executeStep } from './step-executor';
9
+ import { WorkflowRunner } from './workflow-runner';
10
+
11
+ export interface TestFixture {
12
+ inputs?: Record<string, unknown>;
13
+ env?: Record<string, string>;
14
+ secrets?: Record<string, string>;
15
+ mocks?: Array<{
16
+ step?: string;
17
+ type?: string;
18
+ prompt?: string;
19
+ response: unknown;
20
+ }>;
21
+ }
22
+
23
+ export interface TestSnapshot {
24
+ steps: Record<
25
+ string,
26
+ {
27
+ status: string;
28
+ output: unknown;
29
+ error?: string;
30
+ }
31
+ >;
32
+ outputs: Record<string, unknown>;
33
+ }
34
+
35
+ export class TestHarness {
36
+ private stepResults: Map<string, { status: string; output: unknown; error?: string }> = new Map();
37
+ private mockResponses: Map<string, unknown> = new Map();
38
+ private llmMocks: Array<{ prompt: string; response: unknown }> = [];
39
+
40
+ constructor(
41
+ private workflow: Workflow,
42
+ private fixture: TestFixture = {}
43
+ ) {
44
+ if (fixture.mocks) {
45
+ for (const mock of fixture.mocks) {
46
+ if (mock.step) {
47
+ this.mockResponses.set(mock.step, mock.response);
48
+ } else if (mock.prompt) {
49
+ this.llmMocks.push({ prompt: mock.prompt, response: mock.response });
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ async run(): Promise<TestSnapshot> {
56
+ // Capture original environment for cleanup
57
+ const originalEnv = { ...process.env };
58
+ const addedKeys: string[] = [];
59
+
60
+ const runner = new WorkflowRunner(this.workflow, {
61
+ inputs: this.fixture.inputs,
62
+ secrets: this.fixture.secrets,
63
+ executeStep: this.mockExecuteStep.bind(this),
64
+ getAdapter: this.getMockAdapter.bind(this),
65
+ // Use memory DB for tests
66
+ dbPath: ':memory:',
67
+ });
68
+
69
+ try {
70
+ // Inject env
71
+ if (this.fixture.env) {
72
+ for (const [key, value] of Object.entries(this.fixture.env)) {
73
+ if (!(key in originalEnv)) {
74
+ addedKeys.push(key);
75
+ }
76
+ process.env[key] = value;
77
+ }
78
+ }
79
+
80
+ const outputs = await runner.run();
81
+
82
+ return {
83
+ steps: Object.fromEntries(this.stepResults.entries()),
84
+ outputs,
85
+ };
86
+ } finally {
87
+ // Restore original environment
88
+ for (const key of addedKeys) {
89
+ delete process.env[key];
90
+ }
91
+ for (const [key, value] of Object.entries(originalEnv)) {
92
+ if (value !== undefined) {
93
+ process.env[key] = value;
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ private async mockExecuteStep(
100
+ step: Step,
101
+ context: ExpressionContext,
102
+ logger: Logger,
103
+ options: StepExecutorOptions
104
+ ): Promise<StepResult> {
105
+ const mockResponse = this.mockResponses.get(step.id);
106
+ if (mockResponse !== undefined) {
107
+ const result: StepResult = {
108
+ output: mockResponse,
109
+ status: 'success',
110
+ };
111
+ this.stepResults.set(step.id, {
112
+ status: result.status,
113
+ output: result.output,
114
+ error: result.error,
115
+ });
116
+ return result;
117
+ }
118
+
119
+ // Default to real execution but capture snapshot
120
+ const result = await executeStep(step, context, logger, {
121
+ ...options,
122
+ executeStep: this.mockExecuteStep.bind(this),
123
+ getAdapter: this.getMockAdapter.bind(this),
124
+ });
125
+
126
+ this.stepResults.set(step.id, {
127
+ status: result.status,
128
+ output: result.output,
129
+ error: result.error,
130
+ });
131
+
132
+ return result;
133
+ }
134
+
135
+ private getMockAdapter(model: string): { adapter: LLMAdapter; resolvedModel: string } {
136
+ return {
137
+ resolvedModel: model,
138
+ adapter: {
139
+ chat: async (messages: LLMMessage[]) => {
140
+ const userMessage = messages.find((m) => m.role === 'user')?.content || '';
141
+
142
+ for (const mock of this.llmMocks) {
143
+ if (userMessage.includes(mock.prompt)) {
144
+ return {
145
+ message: {
146
+ role: 'assistant',
147
+ content:
148
+ typeof mock.response === 'string'
149
+ ? mock.response
150
+ : JSON.stringify(mock.response),
151
+ },
152
+ };
153
+ }
154
+ }
155
+
156
+ throw new Error(`No LLM mock found for prompt: ${userMessage.substring(0, 100)}...`);
157
+ },
158
+ },
159
+ };
160
+ }
161
+ }
@@ -3,16 +3,8 @@ import { mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import type { ExpressionContext } from '../expression/evaluator';
5
5
  import type { LlmStep, Step } from '../parser/schema';
6
- import {
7
- AnthropicAdapter,
8
- CopilotAdapter,
9
- type LLMMessage,
10
- type LLMResponse,
11
- type LLMTool,
12
- OpenAIAdapter,
13
- } from './llm-adapter';
6
+ import type { LLMAdapter } from './llm-adapter';
14
7
  import { executeLlmStep } from './llm-executor';
15
- import { MCPClient, type MCPResponse } from './mcp-client';
16
8
  import type { StepResult } from './step-executor';
17
9
 
18
10
  interface MockToolCall {
@@ -24,6 +16,37 @@ interface MockToolCall {
24
16
  describe('llm-executor with tools and MCP', () => {
25
17
  const agentsDir = join(process.cwd(), '.keystone', 'workflows', 'agents');
26
18
  const agentPath = join(agentsDir, 'tool-test-agent.md');
19
+ const createMockGetAdapter = (chatFn: LLMAdapter['chat']) => {
20
+ return (_modelString: string) => ({
21
+ adapter: { chat: chatFn } as LLMAdapter,
22
+ resolvedModel: 'gpt-4',
23
+ });
24
+ };
25
+ const createMockMcpClient = (
26
+ options: {
27
+ tools?: { name: string; description?: string; inputSchema: Record<string, unknown> }[];
28
+ callTool?: (name: string, args: Record<string, unknown>) => Promise<unknown>;
29
+ } = {}
30
+ ) => {
31
+ const listTools = mock(async () => options.tools ?? []);
32
+ const callTool =
33
+ options.callTool || (mock(async () => ({})) as unknown as typeof options.callTool);
34
+ return {
35
+ listTools,
36
+ callTool,
37
+ };
38
+ };
39
+ const createMockMcpManager = (
40
+ options: {
41
+ clients?: Record<string, ReturnType<typeof createMockMcpClient> | undefined>;
42
+ } = {}
43
+ ) => {
44
+ const getClient = mock(async (serverRef: string | { name: string }) => {
45
+ const name = typeof serverRef === 'string' ? serverRef : serverRef.name;
46
+ return options.clients?.[name];
47
+ });
48
+ return { getClient };
49
+ };
27
50
 
28
51
  beforeAll(() => {
29
52
  try {
@@ -53,9 +76,6 @@ Test system prompt`;
53
76
  });
54
77
 
55
78
  it('should merge tools from agent, step and MCP', async () => {
56
- const originalOpenAIChat = OpenAIAdapter.prototype.chat;
57
- const originalCopilotChat = CopilotAdapter.prototype.chat;
58
- const originalAnthropicChat = AnthropicAdapter.prototype.chat;
59
79
  let capturedTools: MockToolCall[] = [];
60
80
 
61
81
  const mockChat = mock(async (_messages: unknown, options: unknown) => {
@@ -63,30 +83,21 @@ Test system prompt`;
63
83
  return {
64
84
  message: { role: 'assistant', content: 'Final response' },
65
85
  };
66
- });
86
+ }) as unknown as LLMAdapter['chat'];
87
+ const getAdapter = createMockGetAdapter(mockChat);
67
88
 
68
- OpenAIAdapter.prototype.chat = mockChat as unknown as typeof originalOpenAIChat;
69
- CopilotAdapter.prototype.chat = mockChat as unknown as typeof originalCopilotChat;
70
- AnthropicAdapter.prototype.chat = mockChat as unknown as typeof originalAnthropicChat;
71
-
72
- // Use mock.module for MCPClient
73
- const originalInitialize = MCPClient.prototype.initialize;
74
- const originalListTools = MCPClient.prototype.listTools;
75
- const originalStop = MCPClient.prototype.stop;
76
-
77
- const mockInitialize = mock(async () => ({}) as MCPResponse);
78
- const mockListTools = mock(async () => [
79
- {
80
- name: 'mcp-tool',
81
- description: 'MCP tool',
82
- inputSchema: { type: 'object', properties: {} },
83
- },
84
- ]);
85
- const mockStop = mock(() => {});
86
-
87
- MCPClient.prototype.initialize = mockInitialize;
88
- MCPClient.prototype.listTools = mockListTools;
89
- MCPClient.prototype.stop = mockStop;
89
+ const mockClient = createMockMcpClient({
90
+ tools: [
91
+ {
92
+ name: 'mcp-tool',
93
+ description: 'MCP tool',
94
+ inputSchema: { type: 'object', properties: {} },
95
+ },
96
+ ],
97
+ });
98
+ const mcpManager = createMockMcpManager({
99
+ clients: { 'test-mcp': mockClient },
100
+ });
90
101
 
91
102
  const step: LlmStep = {
92
103
  id: 'l1',
@@ -110,26 +121,21 @@ Test system prompt`;
110
121
  await executeLlmStep(
111
122
  step,
112
123
  context,
113
- executeStepFn as unknown as (step: Step, context: ExpressionContext) => Promise<StepResult>
124
+ executeStepFn as unknown as (step: Step, context: ExpressionContext) => Promise<StepResult>,
125
+ undefined,
126
+ mcpManager as unknown as { getClient: () => Promise<unknown> },
127
+ undefined,
128
+ undefined,
129
+ getAdapter
114
130
  );
115
131
 
116
132
  const toolNames = capturedTools.map((t) => t.function.name);
117
133
  expect(toolNames).toContain('agent-tool');
118
134
  expect(toolNames).toContain('step-tool');
119
135
  expect(toolNames).toContain('mcp-tool');
120
-
121
- OpenAIAdapter.prototype.chat = originalOpenAIChat;
122
- CopilotAdapter.prototype.chat = originalCopilotChat;
123
- AnthropicAdapter.prototype.chat = originalAnthropicChat;
124
- MCPClient.prototype.initialize = originalInitialize;
125
- MCPClient.prototype.listTools = originalListTools;
126
- MCPClient.prototype.stop = originalStop;
127
136
  });
128
137
 
129
138
  it('should execute MCP tool when called', async () => {
130
- const originalOpenAIChat = OpenAIAdapter.prototype.chat;
131
- const originalCopilotChat = CopilotAdapter.prototype.chat;
132
- const originalAnthropicChat = AnthropicAdapter.prototype.chat;
133
139
  let chatCount = 0;
134
140
 
135
141
  const mockChat = mock(async () => {
@@ -151,32 +157,23 @@ Test system prompt`;
151
157
  return {
152
158
  message: { role: 'assistant', content: 'Done' },
153
159
  };
154
- });
160
+ }) as unknown as LLMAdapter['chat'];
161
+ const getAdapter = createMockGetAdapter(mockChat);
155
162
 
156
- OpenAIAdapter.prototype.chat = mockChat as unknown as typeof originalOpenAIChat;
157
- CopilotAdapter.prototype.chat = mockChat as unknown as typeof originalCopilotChat;
158
- AnthropicAdapter.prototype.chat = mockChat as unknown as typeof originalAnthropicChat;
159
-
160
- const originalInitialize = MCPClient.prototype.initialize;
161
- const originalListTools = MCPClient.prototype.listTools;
162
- const originalCallTool = MCPClient.prototype.callTool;
163
- const originalStop = MCPClient.prototype.stop;
164
-
165
- const mockInitialize = mock(async () => ({}) as MCPResponse);
166
- const mockListTools = mock(async () => [
167
- {
168
- name: 'mcp-tool',
169
- description: 'MCP tool',
170
- inputSchema: { type: 'object', properties: {} },
171
- },
172
- ]);
173
163
  const mockCallTool = mock(async () => ({ result: 'mcp success' }));
174
- const mockStop = mock(() => {});
175
-
176
- MCPClient.prototype.initialize = mockInitialize;
177
- MCPClient.prototype.listTools = mockListTools;
178
- MCPClient.prototype.callTool = mockCallTool;
179
- MCPClient.prototype.stop = mockStop;
164
+ const mockClient = createMockMcpClient({
165
+ tools: [
166
+ {
167
+ name: 'mcp-tool',
168
+ description: 'MCP tool',
169
+ inputSchema: { type: 'object', properties: {} },
170
+ },
171
+ ],
172
+ callTool: mockCallTool,
173
+ });
174
+ const mcpManager = createMockMcpManager({
175
+ clients: { 'test-mcp': mockClient },
176
+ });
180
177
 
181
178
  const step: LlmStep = {
182
179
  id: 'l1',
@@ -194,18 +191,15 @@ Test system prompt`;
194
191
  await executeLlmStep(
195
192
  step,
196
193
  context,
197
- executeStepFn as unknown as (step: Step, context: ExpressionContext) => Promise<StepResult>
194
+ executeStepFn as unknown as (step: Step, context: ExpressionContext) => Promise<StepResult>,
195
+ undefined,
196
+ mcpManager as unknown as { getClient: () => Promise<unknown> },
197
+ undefined,
198
+ undefined,
199
+ getAdapter
198
200
  );
199
201
 
200
202
  expect(mockCallTool).toHaveBeenCalledWith('mcp-tool', {});
201
203
  expect(chatCount).toBe(2);
202
-
203
- OpenAIAdapter.prototype.chat = originalOpenAIChat;
204
- CopilotAdapter.prototype.chat = originalCopilotChat;
205
- AnthropicAdapter.prototype.chat = originalAnthropicChat;
206
- MCPClient.prototype.initialize = originalInitialize;
207
- MCPClient.prototype.listTools = originalListTools;
208
- MCPClient.prototype.callTool = originalCallTool;
209
- MCPClient.prototype.stop = originalStop;
210
204
  });
211
205
  });