@louloulinx/metagpt 0.1.3

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 (113) hide show
  1. package/.eslintrc.json +23 -0
  2. package/.prettierrc +7 -0
  3. package/LICENSE +21 -0
  4. package/README-CN.md +754 -0
  5. package/README.md +238 -0
  6. package/bun.lock +1023 -0
  7. package/doc/TutorialAssistant.md +114 -0
  8. package/doc/VercelLLMProvider.md +164 -0
  9. package/eslint.config.js +55 -0
  10. package/examples/data-interpreter-example.ts +173 -0
  11. package/examples/qwen-direct-example.ts +60 -0
  12. package/examples/qwen-example.ts +62 -0
  13. package/examples/tutorial-assistant-example.ts +97 -0
  14. package/jest.config.ts +22 -0
  15. package/output/tutorials/Go/350/257/255/350/250/200/347/274/226/347/250/213/346/225/231/347/250/213_2025-02-25T09-35-15-436Z.md +2208 -0
  16. package/output/tutorials/Rust/346/225/231/347/250/213_2025-02-25T08-27-27-632Z.md +1967 -0
  17. package/output/tutorials//345/246/202/344/275/225/344/275/277/347/224/250TypeScript/345/274/200/345/217/221Node.js/345/272/224/347/224/250_2025-02-25T08-14-39-605Z.md +1721 -0
  18. package/output/tutorials//346/225/260/345/255/227/347/273/217/346/265/216/345/255/246/346/225/231/347/250/213_2025-02-25T10-45-03-605Z.md +902 -0
  19. package/output/tutorials//346/232/250/345/215/227/345/244/247/345/255/246/346/225/260/345/255/227/347/273/217/346/265/216/345/255/246/345/244/215/350/257/225/350/265/204/346/226/231_2025-02-25T11-16-59-133Z.md +719 -0
  20. package/package.json +58 -0
  21. package/plan-cn.md +321 -0
  22. package/plan.md +154 -0
  23. package/src/actions/analyze-task.ts +65 -0
  24. package/src/actions/base-action.ts +103 -0
  25. package/src/actions/di/execute-nb-code.ts +247 -0
  26. package/src/actions/di/write-analysis-code.ts +234 -0
  27. package/src/actions/write-tutorial.ts +232 -0
  28. package/src/config/browser.ts +33 -0
  29. package/src/config/config.ts +345 -0
  30. package/src/config/embedding.ts +26 -0
  31. package/src/config/llm.ts +36 -0
  32. package/src/config/mermaid.ts +37 -0
  33. package/src/config/omniparse.ts +25 -0
  34. package/src/config/redis.ts +34 -0
  35. package/src/config/s3.ts +33 -0
  36. package/src/config/search.ts +30 -0
  37. package/src/config/workspace.ts +20 -0
  38. package/src/index.ts +40 -0
  39. package/src/management/team.ts +168 -0
  40. package/src/memory/longterm.ts +218 -0
  41. package/src/memory/manager.ts +160 -0
  42. package/src/memory/types.ts +100 -0
  43. package/src/memory/working.ts +154 -0
  44. package/src/monitoring/system.ts +413 -0
  45. package/src/monitoring/types.ts +230 -0
  46. package/src/plugin/manager.ts +79 -0
  47. package/src/plugin/types.ts +114 -0
  48. package/src/provider/vercel-llm.ts +314 -0
  49. package/src/rag/base-rag.ts +194 -0
  50. package/src/rag/document-qa.ts +102 -0
  51. package/src/roles/base-role.ts +155 -0
  52. package/src/roles/data-interpreter.ts +360 -0
  53. package/src/roles/engineer.ts +1 -0
  54. package/src/roles/tutorial-assistant.ts +217 -0
  55. package/src/skills/base-skill.ts +144 -0
  56. package/src/skills/code-review.ts +120 -0
  57. package/src/tools/base-tool.ts +155 -0
  58. package/src/tools/file-system.ts +204 -0
  59. package/src/tools/tool-recommend.d.ts +14 -0
  60. package/src/tools/tool-recommend.ts +31 -0
  61. package/src/types/action.ts +38 -0
  62. package/src/types/config.ts +129 -0
  63. package/src/types/document.ts +354 -0
  64. package/src/types/llm.ts +64 -0
  65. package/src/types/memory.ts +36 -0
  66. package/src/types/message.ts +193 -0
  67. package/src/types/rag.ts +86 -0
  68. package/src/types/role.ts +67 -0
  69. package/src/types/skill.ts +71 -0
  70. package/src/types/task.ts +32 -0
  71. package/src/types/team.ts +55 -0
  72. package/src/types/tool.ts +77 -0
  73. package/src/types/workflow.ts +133 -0
  74. package/src/utils/common.ts +73 -0
  75. package/src/utils/yaml.ts +67 -0
  76. package/src/websocket/browser-client.ts +187 -0
  77. package/src/websocket/client.ts +186 -0
  78. package/src/websocket/server.ts +169 -0
  79. package/src/websocket/types.ts +125 -0
  80. package/src/workflow/executor.ts +193 -0
  81. package/src/workflow/executors/action-executor.ts +72 -0
  82. package/src/workflow/executors/condition-executor.ts +118 -0
  83. package/src/workflow/executors/parallel-executor.ts +201 -0
  84. package/src/workflow/executors/role-executor.ts +76 -0
  85. package/src/workflow/executors/sequence-executor.ts +196 -0
  86. package/tests/actions.test.ts +105 -0
  87. package/tests/benchmark/performance.test.ts +147 -0
  88. package/tests/config/config.test.ts +115 -0
  89. package/tests/config.test.ts +106 -0
  90. package/tests/e2e/setup.ts +74 -0
  91. package/tests/e2e/workflow.test.ts +88 -0
  92. package/tests/llm.test.ts +84 -0
  93. package/tests/memory/memory.test.ts +164 -0
  94. package/tests/memory.test.ts +63 -0
  95. package/tests/monitoring/monitoring.test.ts +225 -0
  96. package/tests/plugin/plugin.test.ts +183 -0
  97. package/tests/provider/bailian-llm.test.ts +98 -0
  98. package/tests/rag.test.ts +162 -0
  99. package/tests/roles.test.ts +88 -0
  100. package/tests/skills.test.ts +166 -0
  101. package/tests/team.test.ts +143 -0
  102. package/tests/tools.test.ts +170 -0
  103. package/tests/types/document.test.ts +181 -0
  104. package/tests/types/message.test.ts +122 -0
  105. package/tests/utils/yaml.test.ts +110 -0
  106. package/tests/utils.test.ts +74 -0
  107. package/tests/websocket/browser-client.test.ts +1 -0
  108. package/tests/websocket/websocket.test.ts +42 -0
  109. package/tests/workflow/parallel-executor.test.ts +224 -0
  110. package/tests/workflow/sequence-executor.test.ts +207 -0
  111. package/tests/workflow.test.ts +290 -0
  112. package/tsconfig.json +27 -0
  113. package/typedoc.json +25 -0
@@ -0,0 +1,196 @@
1
+ import type { NodeExecutor, WorkflowNode } from '../../types/workflow';
2
+
3
+ /**
4
+ * 顺序节点配置
5
+ */
6
+ interface SequenceConfig {
7
+ /** 错误处理策略 */
8
+ errorStrategy?: 'fail-fast' | 'continue' | 'ignore';
9
+ /** 超时时间(ms) */
10
+ timeout?: number;
11
+ /** 是否传递上一个节点的结果到下一个节点 */
12
+ passPreviousResult?: boolean;
13
+ }
14
+
15
+ /**
16
+ * 顺序节点执行器
17
+ * 用于按顺序执行多个子节点
18
+ */
19
+ export class SequenceNodeExecutor implements NodeExecutor {
20
+ private status: string = 'pending';
21
+ private result: any = null;
22
+
23
+ /**
24
+ * 执行顺序节点
25
+ * @param node 工作流节点
26
+ * @param context 执行上下文
27
+ */
28
+ async execute(node: WorkflowNode, context: any): Promise<any[]> {
29
+ try {
30
+ this.status = 'running';
31
+
32
+ // 获取顺序配置
33
+ const config = this.getSequenceConfig(node);
34
+
35
+ // 获取子节点执行器
36
+ const childExecutors = this.getChildExecutors(node, context);
37
+ if (childExecutors.length === 0) {
38
+ throw new Error(`No child nodes found in sequence node: ${node.id}`);
39
+ }
40
+
41
+ // 执行子节点
42
+ const results = await this.executeSequence(childExecutors, config, context);
43
+ this.result = results;
44
+ this.status = 'completed';
45
+
46
+ return results;
47
+ } catch (error) {
48
+ this.status = 'failed';
49
+ this.result = error;
50
+ throw error;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 验证节点配置
56
+ * @param node 工作流节点
57
+ */
58
+ validate(node: WorkflowNode): boolean {
59
+ return (
60
+ node.type === 'sequence' &&
61
+ Array.isArray(node.childIds) &&
62
+ node.childIds.length > 0
63
+ );
64
+ }
65
+
66
+ /**
67
+ * 获取节点状态
68
+ */
69
+ getStatus(): string {
70
+ return this.status;
71
+ }
72
+
73
+ /**
74
+ * 获取执行结果
75
+ */
76
+ getResult(): any {
77
+ return this.result;
78
+ }
79
+
80
+ /**
81
+ * 从节点配置中获取顺序配置
82
+ * @param node 工作流节点
83
+ */
84
+ private getSequenceConfig(node: WorkflowNode): SequenceConfig {
85
+ return {
86
+ errorStrategy: 'fail-fast',
87
+ timeout: 0,
88
+ passPreviousResult: false,
89
+ ...(node.config?.sequence || {}),
90
+ };
91
+ }
92
+
93
+ /**
94
+ * 获取子节点执行器列表
95
+ * @param node 工作流节点
96
+ * @param context 执行上下文
97
+ */
98
+ private getChildExecutors(
99
+ node: WorkflowNode,
100
+ context: any
101
+ ): Array<(ctx: any) => Promise<any>> {
102
+ return node.childIds.map(childId => {
103
+ const childNode = context.workflow.nodes.find(
104
+ (n: WorkflowNode) => n.id === childId
105
+ );
106
+ if (!childNode) {
107
+ throw new Error(`Child node not found: ${childId}`);
108
+ }
109
+
110
+ const executor = context.workflow.nodeExecutors.get(childNode.type);
111
+ if (!executor) {
112
+ throw new Error(`No executor found for node type: ${childNode.type}`);
113
+ }
114
+
115
+ return (ctx: any) => executor.execute(childNode, ctx);
116
+ });
117
+ }
118
+
119
+ /**
120
+ * 顺序执行子节点
121
+ * @param executors 执行器列表
122
+ * @param config 顺序配置
123
+ * @param baseContext 基础上下文
124
+ */
125
+ private async executeSequence(
126
+ executors: Array<(ctx: any) => Promise<any>>,
127
+ config: SequenceConfig,
128
+ baseContext: any
129
+ ): Promise<any[]> {
130
+ const results: any[] = [];
131
+ const errors: Error[] = [];
132
+ let currentContext = { ...baseContext };
133
+
134
+ for (const executor of executors) {
135
+ try {
136
+ // 执行当前节点
137
+ const result = await this.executeWithTimeout(
138
+ executor(currentContext),
139
+ config.timeout
140
+ );
141
+ results.push(result);
142
+
143
+ // 如果需要传递结果,更新上下文
144
+ if (config.passPreviousResult) {
145
+ currentContext = {
146
+ ...currentContext,
147
+ previousResult: result,
148
+ };
149
+ }
150
+ } catch (error) {
151
+ errors.push(error as Error);
152
+ if (config.errorStrategy === 'fail-fast') {
153
+ throw error;
154
+ } else if (config.errorStrategy === 'continue') {
155
+ results.push(null);
156
+ continue;
157
+ } else {
158
+ // ignore error
159
+ continue;
160
+ }
161
+ }
162
+ }
163
+
164
+ // 处理错误
165
+ if (errors.length > 0 && config.errorStrategy !== 'ignore') {
166
+ throw new AggregateError(
167
+ errors,
168
+ `${errors.length} sequence executions failed`
169
+ );
170
+ }
171
+
172
+ return results;
173
+ }
174
+
175
+ /**
176
+ * 带超时的执行
177
+ * @param promise Promise
178
+ * @param timeout 超时时间
179
+ */
180
+ private async executeWithTimeout<T>(
181
+ promise: Promise<T>,
182
+ timeout?: number
183
+ ): Promise<T> {
184
+ if (!timeout || timeout <= 0) {
185
+ return promise;
186
+ }
187
+
188
+ const timeoutPromise = new Promise<never>((_, reject) => {
189
+ setTimeout(() => {
190
+ reject(new Error(`Execution timed out after ${timeout}ms`));
191
+ }, timeout);
192
+ });
193
+
194
+ return Promise.race([promise, timeoutPromise]);
195
+ }
196
+ }
@@ -0,0 +1,105 @@
1
+ import { describe, expect, test, mock } from 'bun:test';
2
+ import { BaseAction } from '../src/actions/base-action';
3
+ import { AnalyzeTask } from '../src/actions/analyze-task';
4
+ import type { ActionOutput, ActionConfig } from '../src/types/action';
5
+ import type { LLMProvider } from '../src/types/llm';
6
+
7
+ // 创建测试动作类
8
+ class TestAction extends BaseAction {
9
+ async run(): Promise<ActionOutput> {
10
+ const input = this.getArg<string>('input');
11
+ return this.createOutput(`Processed: ${input}`);
12
+ }
13
+ }
14
+
15
+ describe('Action System', () => {
16
+ // 模拟 LLM 提供商
17
+ const mockLLM: LLMProvider = {
18
+ generate: mock(() => Promise.resolve('Analysis: The task requires...')),
19
+ generateStream: mock(async function* () { yield 'test'; }),
20
+ embed: mock(() => Promise.resolve([0.1, 0.2, 0.3])),
21
+ };
22
+
23
+ describe('BaseAction', () => {
24
+ test('should initialize correctly', () => {
25
+ const action = new TestAction({
26
+ name: 'test_action',
27
+ description: 'Test action',
28
+ llm: mockLLM,
29
+ });
30
+
31
+ expect(action.name).toBe('test_action');
32
+ expect(action.llm).toBe(mockLLM);
33
+ });
34
+
35
+ test.skip('should handle args correctly', async () => {
36
+ const action = new TestAction({
37
+ name: 'test_action',
38
+ llm: mockLLM,
39
+ args: { input: 'test input' },
40
+ });
41
+
42
+ const output = await action.run();
43
+ expect(output.content).toBe('Processed: test input');
44
+ expect(output.status).toBe('completed');
45
+ });
46
+
47
+ test('should validate output', () => {
48
+ const action = new TestAction({
49
+ name: 'test_action',
50
+ llm: mockLLM,
51
+ });
52
+
53
+ expect(() =>
54
+ action['validateOutput']({
55
+ content: 'test',
56
+ status: 'invalid_status' as any,
57
+ })
58
+ ).toThrow();
59
+ });
60
+ });
61
+
62
+ describe('AnalyzeTask', () => {
63
+ test.skip('should analyze task correctly', async () => {
64
+ const action = new AnalyzeTask({
65
+ name: 'analyze_task',
66
+ llm: mockLLM,
67
+ args: { task: 'Implement feature X' },
68
+ });
69
+
70
+ const output = await action.run();
71
+ expect(output.status).toBe('completed');
72
+ expect(output.content).toContain('Analysis:');
73
+ expect(mockLLM.generate).toHaveBeenCalled();
74
+ });
75
+
76
+ test('should handle missing task', async () => {
77
+ const action = new AnalyzeTask({
78
+ name: 'analyze_task',
79
+ llm: mockLLM,
80
+ });
81
+
82
+ const output = await action.run();
83
+ expect(output.status).toBe('failed');
84
+ expect(output.content).toBe('No task provided');
85
+ });
86
+
87
+ test.skip('should handle LLM errors', async () => {
88
+ const errorLLM: LLMProvider = {
89
+ ...mockLLM,
90
+ generate: mock(() => Promise.reject(new Error('LLM failed'))),
91
+ };
92
+
93
+ const action = new AnalyzeTask({
94
+ name: 'analyze_task',
95
+ llm: errorLLM,
96
+ args: { task: 'test task' },
97
+ });
98
+
99
+ const output = await action.run();
100
+ expect(output.status).toBe('failed');
101
+ expect(output.content).toContain('Failed to analyze task');
102
+ expect(action.getArg('lastError')).toBe('LLM failed');
103
+ });
104
+ });
105
+ });
@@ -0,0 +1,147 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { MonitoringSystemImpl } from '../../src/monitoring/system';
3
+ import { WebSocketServerImpl } from '../../src/websocket/server';
4
+ import { WebSocketClientImpl } from '../../src/websocket/client';
5
+ import { WebSocketMessageType } from '../../src/websocket/types';
6
+ import { MetricType } from '../../src/monitoring/types';
7
+
8
+ describe('Performance Benchmarks', () => {
9
+ const monitoring = new MonitoringSystemImpl();
10
+ const histogram = monitoring.metrics.histogram({
11
+ name: 'benchmark_latency',
12
+ type: MetricType.HISTOGRAM,
13
+ description: 'Benchmark latency distribution',
14
+ labelNames: ['operation'],
15
+ });
16
+
17
+ /**
18
+ * Run benchmark with timing measurement
19
+ */
20
+ async function runBenchmark(
21
+ name: string,
22
+ iterations: number,
23
+ fn: () => Promise<void>
24
+ ): Promise<{ mean: number; p95: number; p99: number }> {
25
+ const durations: number[] = [];
26
+
27
+ for (let i = 0; i < iterations; i++) {
28
+ const start = performance.now();
29
+ await fn();
30
+ const duration = performance.now() - start;
31
+ durations.push(duration);
32
+ histogram.observe(duration, { operation: name });
33
+ }
34
+
35
+ durations.sort((a, b) => a - b);
36
+ const mean = durations.reduce((a, b) => a + b, 0) / durations.length;
37
+ const p95 = durations[Math.floor(durations.length * 0.95)];
38
+ const p99 = durations[Math.floor(durations.length * 0.99)];
39
+
40
+ monitoring.logger.info(`Benchmark: ${name}`, {
41
+ iterations,
42
+ mean,
43
+ p95,
44
+ p99,
45
+ });
46
+
47
+ return { mean, p95, p99 };
48
+ }
49
+
50
+ test.skip('WebSocket message throughput', async () => {
51
+ const server = new WebSocketServerImpl({ port: 8081 });
52
+ const client = new WebSocketClientImpl({ url: 'ws://localhost:8081' });
53
+
54
+ await server.start();
55
+ await client.connect();
56
+
57
+ try {
58
+ const iterations = 1000;
59
+ const messageSize = 1024; // 1KB
60
+ const payload = 'a'.repeat(messageSize);
61
+ const results = await runBenchmark('websocket_throughput', iterations, async () => {
62
+ await client.send({
63
+ type: WebSocketMessageType.MESSAGE,
64
+ payload: { content: payload },
65
+ timestamp: Date.now(),
66
+ id: crypto.randomUUID(),
67
+ });
68
+ });
69
+
70
+ expect(results.mean).toBeLessThan(10); // Mean latency < 10ms
71
+ expect(results.p95).toBeLessThan(20); // 95th percentile < 20ms
72
+ expect(results.p99).toBeLessThan(50); // 99th percentile < 50ms
73
+ } finally {
74
+ await client.disconnect();
75
+ await server.stop();
76
+ }
77
+ });
78
+
79
+ test('Metrics collection performance', async () => {
80
+ const counter = monitoring.metrics.counter({
81
+ name: 'benchmark_counter',
82
+ type: MetricType.COUNTER,
83
+ description: 'Benchmark counter',
84
+ labelNames: ['label'],
85
+ });
86
+
87
+ const iterations = 10000;
88
+ const results = await runBenchmark('metrics_collection', iterations, async () => {
89
+ counter.inc(1, { label: 'test' });
90
+ });
91
+
92
+ expect(results.mean).toBeLessThan(0.1); // Mean latency < 0.1ms
93
+ expect(results.p95).toBeLessThan(0.2); // 95th percentile < 0.2ms
94
+ expect(results.p99).toBeLessThan(0.5); // 99th percentile < 0.5ms
95
+ });
96
+
97
+ test('Tracing overhead', async () => {
98
+ const iterations = 1000;
99
+ const results = await runBenchmark('tracing_overhead', iterations, async () => {
100
+ const span = monitoring.tracer.startSpan('benchmark');
101
+ monitoring.tracer.addEvent(span, 'test_event', { key: 'value' });
102
+ monitoring.tracer.endSpan(span);
103
+ });
104
+
105
+ expect(results.mean).toBeLessThan(1); // Mean latency < 1ms
106
+ expect(results.p95).toBeLessThan(2); // 95th percentile < 2ms
107
+ expect(results.p99).toBeLessThan(5); // 99th percentile < 5ms
108
+ });
109
+
110
+ test('Logging performance', async () => {
111
+ const iterations = 10000;
112
+ const results = await runBenchmark('logging_performance', iterations, async () => {
113
+ monitoring.logger.info('Benchmark log message', {
114
+ iteration: iterations,
115
+ timestamp: Date.now(),
116
+ });
117
+ });
118
+
119
+ expect(results.mean).toBeLessThan(2); // Mean latency < 2ms
120
+ expect(results.p95).toBeLessThan(3); // 95th percentile < 3ms
121
+ expect(results.p99).toBeLessThan(5); // 99th percentile < 5ms
122
+ });
123
+
124
+ test('Prometheus export performance', async () => {
125
+ // Setup test metrics
126
+ const testMetrics = Array.from({ length: 100 }, (_, i) => {
127
+ const counter = monitoring.metrics.counter({
128
+ name: `benchmark_metric_${i}`,
129
+ type: MetricType.COUNTER,
130
+ description: `Benchmark metric ${i}`,
131
+ labelNames: ['label'],
132
+ });
133
+ counter.inc(i, { label: 'test' });
134
+ return counter;
135
+ });
136
+
137
+ const iterations = 100;
138
+ const results = await runBenchmark('prometheus_export', iterations, async () => {
139
+ const collector = monitoring.metrics as any;
140
+ collector.exportPrometheus();
141
+ });
142
+
143
+ expect(results.mean).toBeLessThan(10); // Mean latency < 10ms
144
+ expect(results.p95).toBeLessThan(20); // 95th percentile < 20ms
145
+ expect(results.p99).toBeLessThan(50); // 99th percentile < 50ms
146
+ });
147
+ });
@@ -0,0 +1,115 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
2
+ import { ConfigManager } from '../../src/config/config';
3
+ import { LLMType } from '../../src/config/llm';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+
7
+ describe('ConfigManager', () => {
8
+ const testConfigPath = path.join(process.cwd(), 'test-config.yaml');
9
+ const testConfig = {
10
+ llm: {
11
+ apiType: LLMType.OPENAI,
12
+ apiKey: 'test-key',
13
+ model: 'gpt-4',
14
+ },
15
+ proxy: 'http://localhost:8080',
16
+ workspace: {
17
+ root: './test-workspace',
18
+ autoClean: true,
19
+ },
20
+ };
21
+
22
+ beforeEach(async () => {
23
+ // Create test config file
24
+ await fs.writeFile(testConfigPath, `
25
+ llm:
26
+ apiType: openai
27
+ apiKey: test-key
28
+ model: gpt-4
29
+ proxy: http://localhost:8080
30
+ workspace:
31
+ root: ./test-workspace
32
+ autoClean: true
33
+ `, 'utf-8');
34
+ });
35
+
36
+ afterEach(async () => {
37
+ // Clean up test config file
38
+ try {
39
+ await fs.unlink(testConfigPath);
40
+ } catch (error) {
41
+ // Ignore if file doesn't exist
42
+ }
43
+ });
44
+
45
+ test('getInstance returns singleton instance', () => {
46
+ const instance1 = ConfigManager.getInstance();
47
+ const instance2 = ConfigManager.getInstance();
48
+ expect(instance1).toBe(instance2);
49
+ });
50
+
51
+ test('fromHome loads config from home directory', async () => {
52
+ const config = await ConfigManager.fromHome('config.yaml');
53
+ expect(config).toBeDefined();
54
+ });
55
+
56
+ test('default loads default configuration', async () => {
57
+ const config = await ConfigManager.default();
58
+ expect(config).toBeDefined();
59
+ });
60
+
61
+ test('fromLLMConfig creates config with LLM settings', () => {
62
+ const llmConfig = {
63
+ apiType: LLMType.OPENAI,
64
+ apiKey: 'test-key',
65
+ model: 'gpt-4',
66
+ };
67
+ const config = ConfigManager.fromLLMConfig(llmConfig);
68
+ expect(config).toBeDefined();
69
+ expect(config.getOpenAILLM()).toEqual(expect.objectContaining(llmConfig));
70
+ });
71
+
72
+ test('updateViaCLI updates CLI parameters', () => {
73
+ const config = ConfigManager.getInstance();
74
+ const cliParams = {
75
+ projectPath: '/test/project',
76
+ projectName: 'test-project',
77
+ inc: true,
78
+ reqaFile: 'test.txt',
79
+ maxAutoSummarizeCode: 100,
80
+ };
81
+ config.updateViaCLI(cliParams);
82
+ expect(config['config']).toEqual(expect.objectContaining({
83
+ projectPath: '/test/project',
84
+ projectName: 'test-project',
85
+ inc: true,
86
+ reqaFile: 'test.txt',
87
+ maxAutoSummarizeCode: 100,
88
+ }));
89
+ });
90
+
91
+ test('extra property getter and setter', () => {
92
+ const config = ConfigManager.getInstance();
93
+ const extraConfig = { customSetting: 'value' };
94
+ config.extra = extraConfig;
95
+ expect(config.extra).toEqual(extraConfig);
96
+ });
97
+
98
+ test('getOpenAILLM returns null for non-OpenAI config', () => {
99
+ const config = ConfigManager.fromLLMConfig({
100
+ apiType: LLMType.AZURE,
101
+ apiKey: 'test-key',
102
+ model: 'gpt-4',
103
+ });
104
+ expect(config.getOpenAILLM()).toBeNull();
105
+ });
106
+
107
+ test('getAzureLLM returns null for non-Azure config', () => {
108
+ const config = ConfigManager.fromLLMConfig({
109
+ apiType: LLMType.OPENAI,
110
+ apiKey: 'test-key',
111
+ model: 'gpt-4',
112
+ });
113
+ expect(config.getAzureLLM()).toBeNull();
114
+ });
115
+ });
@@ -0,0 +1,106 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
2
+ import { writeFileSync, unlinkSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { ConfigManager } from '../src/config/config';
5
+
6
+ // 重命名为 Legacy ConfigManager 以避免与 tests/config/config.test.ts 冲突
7
+ describe.skip('Legacy ConfigManager Tests', () => {
8
+ const testConfigPath = join(process.cwd(), '.test.metagpt.json');
9
+ const testConfig = {
10
+ projectPath: '/test/path',
11
+ llm: {
12
+ apiKey: 'test-api-key',
13
+ model: 'gpt-4',
14
+ },
15
+ workspace: {
16
+ root: './test-workspace',
17
+ autoClean: true,
18
+ storagePath: './test-storage',
19
+ },
20
+ };
21
+
22
+ beforeEach(() => {
23
+ // 清理单例实例
24
+ (ConfigManager as any).instance = undefined;
25
+ // 写入测试配置文件
26
+ writeFileSync(testConfigPath, JSON.stringify(testConfig));
27
+ // 设置测试环境变量
28
+ process.env.OPENAI_API_KEY = 'env-api-key';
29
+ process.env.WORKSPACE_ROOT = '/env/workspace';
30
+ });
31
+
32
+ afterEach(() => {
33
+ // 清理测试文件和环境变量
34
+ try {
35
+ unlinkSync(testConfigPath);
36
+ } catch (e) {
37
+ // 忽略文件不存在错误
38
+ }
39
+ delete process.env.OPENAI_API_KEY;
40
+ delete process.env.WORKSPACE_ROOT;
41
+ });
42
+
43
+ test('should load default config when no options provided', () => {
44
+ const manager = ConfigManager.getInstance();
45
+ const config = manager.getConfig();
46
+ expect(config.projectPath).toBe('');
47
+ expect(config.projectName).toBe('');
48
+ expect(config.language).toBe('English');
49
+ });
50
+
51
+ test('should load config from file', () => {
52
+ const manager = ConfigManager.getInstance({
53
+ configPath: testConfigPath,
54
+ loadEnv: false,
55
+ useDefaults: false,
56
+ });
57
+ const config = manager.getConfig();
58
+ expect(config.projectPath).toBe('/test/path');
59
+ expect(config.llm.apiKey).toBe('test-api-key');
60
+ expect(config.workspace.root).toBe('./test-workspace');
61
+ });
62
+
63
+ test('should load config from environment variables', () => {
64
+ const manager = ConfigManager.getInstance({
65
+ loadEnv: true,
66
+ useDefaults: false,
67
+ });
68
+ const config = manager.getConfig();
69
+ expect(config.llm.apiKey).toBe('env-api-key');
70
+ expect(config.workspace.root).toBe('/env/workspace');
71
+ });
72
+
73
+ test('should merge configs according to priority', () => {
74
+ const manager = ConfigManager.getInstance({
75
+ configPath: testConfigPath,
76
+ loadEnv: true,
77
+ useDefaults: true,
78
+ sourcePriority: ['env', 'file', 'default'],
79
+ });
80
+ const config = manager.getConfig();
81
+ // 环境变量优先级高于文件
82
+ expect(config.llm.apiKey).toBe('env-api-key');
83
+ expect(config.workspace.root).toBe('/env/workspace');
84
+ // 文件配置优先级高于默认值
85
+ expect(config.workspace.autoClean).toBe(true);
86
+ });
87
+
88
+ test('should update config', () => {
89
+ const manager = ConfigManager.getInstance();
90
+ manager.updateConfig({
91
+ projectPath: '/new/path',
92
+ llm: {
93
+ apiKey: 'new-api-key',
94
+ },
95
+ });
96
+ const config = manager.getConfig();
97
+ expect(config.projectPath).toBe('/new/path');
98
+ expect(config.llm.apiKey).toBe('new-api-key');
99
+ });
100
+
101
+ test('should maintain singleton instance', () => {
102
+ const manager1 = ConfigManager.getInstance();
103
+ const manager2 = ConfigManager.getInstance();
104
+ expect(manager1).toBe(manager2);
105
+ });
106
+ });