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.
- package/README.md +486 -54
- package/package.json +8 -2
- package/src/__fixtures__/index.ts +100 -0
- package/src/cli.ts +841 -91
- package/src/db/memory-db.ts +35 -1
- package/src/db/workflow-db.test.ts +24 -0
- package/src/db/workflow-db.ts +484 -14
- package/src/expression/evaluator.ts +68 -4
- package/src/parser/agent-parser.ts +6 -3
- package/src/parser/config-schema.ts +38 -2
- package/src/parser/schema.ts +192 -7
- package/src/parser/test-schema.ts +29 -0
- package/src/parser/workflow-parser.test.ts +54 -0
- package/src/parser/workflow-parser.ts +153 -7
- package/src/runner/aggregate-error.test.ts +57 -0
- package/src/runner/aggregate-error.ts +46 -0
- package/src/runner/audit-verification.test.ts +2 -2
- package/src/runner/auto-heal.test.ts +1 -1
- package/src/runner/blueprint-executor.test.ts +63 -0
- package/src/runner/blueprint-executor.ts +157 -0
- package/src/runner/concurrency-limit.test.ts +82 -0
- package/src/runner/debug-repl.ts +18 -3
- package/src/runner/durable-timers.test.ts +200 -0
- package/src/runner/engine-executor.test.ts +464 -0
- package/src/runner/engine-executor.ts +491 -0
- package/src/runner/foreach-executor.ts +30 -12
- package/src/runner/llm-adapter.test.ts +282 -5
- package/src/runner/llm-adapter.ts +581 -8
- package/src/runner/llm-clarification.test.ts +79 -21
- package/src/runner/llm-errors.ts +83 -0
- package/src/runner/llm-executor.test.ts +258 -219
- package/src/runner/llm-executor.ts +226 -29
- package/src/runner/mcp-client.ts +70 -3
- package/src/runner/mcp-manager.test.ts +52 -52
- package/src/runner/mcp-manager.ts +12 -5
- package/src/runner/mcp-server.test.ts +117 -78
- package/src/runner/mcp-server.ts +13 -4
- package/src/runner/optimization-runner.ts +48 -31
- package/src/runner/reflexion.test.ts +1 -1
- package/src/runner/resource-pool.test.ts +113 -0
- package/src/runner/resource-pool.ts +164 -0
- package/src/runner/shell-executor.ts +130 -32
- package/src/runner/standard-tools-execution.test.ts +39 -0
- package/src/runner/standard-tools-integration.test.ts +36 -36
- package/src/runner/standard-tools.test.ts +18 -0
- package/src/runner/standard-tools.ts +174 -93
- package/src/runner/step-executor.test.ts +176 -16
- package/src/runner/step-executor.ts +534 -83
- package/src/runner/stream-utils.test.ts +14 -0
- package/src/runner/subflow-outputs.test.ts +103 -0
- package/src/runner/test-harness.ts +161 -0
- package/src/runner/tool-integration.test.ts +73 -79
- package/src/runner/workflow-runner.test.ts +549 -15
- package/src/runner/workflow-runner.ts +1448 -79
- package/src/runner/workflow-subflows.test.ts +255 -0
- package/src/templates/agents/keystone-architect.md +17 -12
- package/src/templates/agents/tester.md +21 -0
- package/src/templates/child-rollback.yaml +11 -0
- package/src/templates/decompose-implement.yaml +53 -0
- package/src/templates/decompose-problem.yaml +159 -0
- package/src/templates/decompose-research.yaml +52 -0
- package/src/templates/decompose-review.yaml +51 -0
- package/src/templates/dev.yaml +134 -0
- package/src/templates/engine-example.yaml +33 -0
- package/src/templates/fan-out-fan-in.yaml +61 -0
- package/src/templates/memory-service.yaml +1 -1
- package/src/templates/parent-rollback.yaml +16 -0
- package/src/templates/robust-automation.yaml +1 -1
- package/src/templates/scaffold-feature.yaml +29 -27
- package/src/templates/scaffold-generate.yaml +41 -0
- package/src/templates/scaffold-plan.yaml +53 -0
- package/src/types/status.ts +3 -0
- package/src/ui/dashboard.tsx +4 -3
- package/src/utils/assets.macro.ts +36 -0
- package/src/utils/auth-manager.ts +585 -8
- package/src/utils/blueprint-utils.test.ts +49 -0
- package/src/utils/blueprint-utils.ts +80 -0
- package/src/utils/circuit-breaker.test.ts +177 -0
- package/src/utils/circuit-breaker.ts +160 -0
- package/src/utils/config-loader.test.ts +100 -13
- package/src/utils/config-loader.ts +44 -17
- package/src/utils/constants.ts +62 -0
- package/src/utils/error-renderer.test.ts +267 -0
- package/src/utils/error-renderer.ts +320 -0
- package/src/utils/json-parser.test.ts +4 -0
- package/src/utils/json-parser.ts +18 -1
- package/src/utils/mermaid.ts +4 -0
- package/src/utils/paths.test.ts +46 -0
- package/src/utils/paths.ts +70 -0
- package/src/utils/process-sandbox.test.ts +128 -0
- package/src/utils/process-sandbox.ts +293 -0
- package/src/utils/rate-limiter.test.ts +143 -0
- package/src/utils/rate-limiter.ts +221 -0
- package/src/utils/redactor.test.ts +23 -15
- package/src/utils/redactor.ts +65 -25
- package/src/utils/resource-loader.test.ts +54 -0
- package/src/utils/resource-loader.ts +158 -0
- package/src/utils/sandbox.test.ts +69 -4
- package/src/utils/sandbox.ts +69 -6
- package/src/utils/schema-validator.ts +65 -0
- package/src/utils/workflow-registry.test.ts +57 -0
- package/src/utils/workflow-registry.ts +45 -25
- /package/src/expression/{evaluator.audit.test.ts → evaluator-audit.test.ts} +0 -0
- /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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
});
|