@f2a/network 0.1.2
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/.github/workflows/ci.yml +113 -0
- package/.github/workflows/publish.yml +60 -0
- package/LICENSE +21 -0
- package/MONOREPO.md +58 -0
- package/README.md +280 -0
- package/SKILL.md +137 -0
- package/dist/adapters/openclaw.d.ts +103 -0
- package/dist/adapters/openclaw.d.ts.map +1 -0
- package/dist/adapters/openclaw.js +297 -0
- package/dist/adapters/openclaw.js.map +1 -0
- package/dist/cli/commands.d.ts +17 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +107 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +203 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/autonomous-economy.d.ts +136 -0
- package/dist/core/autonomous-economy.d.ts.map +1 -0
- package/dist/core/autonomous-economy.js +255 -0
- package/dist/core/autonomous-economy.js.map +1 -0
- package/dist/core/connection-manager.d.ts +80 -0
- package/dist/core/connection-manager.d.ts.map +1 -0
- package/dist/core/connection-manager.js +235 -0
- package/dist/core/connection-manager.js.map +1 -0
- package/dist/core/connection-manager.test.d.ts +2 -0
- package/dist/core/connection-manager.test.d.ts.map +1 -0
- package/dist/core/connection-manager.test.js +52 -0
- package/dist/core/connection-manager.test.js.map +1 -0
- package/dist/core/e2ee-crypto.d.ts +90 -0
- package/dist/core/e2ee-crypto.d.ts.map +1 -0
- package/dist/core/e2ee-crypto.js +190 -0
- package/dist/core/e2ee-crypto.js.map +1 -0
- package/dist/core/f2a.d.ts +126 -0
- package/dist/core/f2a.d.ts.map +1 -0
- package/dist/core/f2a.js +425 -0
- package/dist/core/f2a.js.map +1 -0
- package/dist/core/identity.d.ts +47 -0
- package/dist/core/identity.d.ts.map +1 -0
- package/dist/core/identity.js +130 -0
- package/dist/core/identity.js.map +1 -0
- package/dist/core/identity.test.d.ts +2 -0
- package/dist/core/identity.test.d.ts.map +1 -0
- package/dist/core/identity.test.js +43 -0
- package/dist/core/identity.test.js.map +1 -0
- package/dist/core/p2p-network.d.ts +242 -0
- package/dist/core/p2p-network.d.ts.map +1 -0
- package/dist/core/p2p-network.js +1182 -0
- package/dist/core/p2p-network.js.map +1 -0
- package/dist/core/reputation-security.d.ts +168 -0
- package/dist/core/reputation-security.d.ts.map +1 -0
- package/dist/core/reputation-security.js +369 -0
- package/dist/core/reputation-security.js.map +1 -0
- package/dist/core/reputation.d.ts +179 -0
- package/dist/core/reputation.d.ts.map +1 -0
- package/dist/core/reputation.js +472 -0
- package/dist/core/reputation.js.map +1 -0
- package/dist/core/review-committee.d.ts +130 -0
- package/dist/core/review-committee.d.ts.map +1 -0
- package/dist/core/review-committee.js +251 -0
- package/dist/core/review-committee.js.map +1 -0
- package/dist/core/serverless.d.ts +155 -0
- package/dist/core/serverless.d.ts.map +1 -0
- package/dist/core/serverless.js +615 -0
- package/dist/core/serverless.js.map +1 -0
- package/dist/core/token-manager.d.ts +42 -0
- package/dist/core/token-manager.d.ts.map +1 -0
- package/dist/core/token-manager.js +122 -0
- package/dist/core/token-manager.js.map +1 -0
- package/dist/daemon/control-server.d.ts +55 -0
- package/dist/daemon/control-server.d.ts.map +1 -0
- package/dist/daemon/control-server.js +262 -0
- package/dist/daemon/control-server.js.map +1 -0
- package/dist/daemon/index.d.ts +35 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +69 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/main.d.ts +6 -0
- package/dist/daemon/main.d.ts.map +1 -0
- package/dist/daemon/main.js +38 -0
- package/dist/daemon/main.js.map +1 -0
- package/dist/daemon/start.d.ts +6 -0
- package/dist/daemon/start.d.ts.map +1 -0
- package/dist/daemon/start.js +25 -0
- package/dist/daemon/start.js.map +1 -0
- package/dist/daemon/webhook.d.ts +30 -0
- package/dist/daemon/webhook.d.ts.map +1 -0
- package/dist/daemon/webhook.js +86 -0
- package/dist/daemon/webhook.js.map +1 -0
- package/dist/daemon/webhook.test.d.ts +2 -0
- package/dist/daemon/webhook.test.d.ts.map +1 -0
- package/dist/daemon/webhook.test.js +24 -0
- package/dist/daemon/webhook.test.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/messages.d.ts +739 -0
- package/dist/protocol/messages.d.ts.map +1 -0
- package/dist/protocol/messages.js +188 -0
- package/dist/protocol/messages.js.map +1 -0
- package/dist/protocol/messages.test.d.ts +2 -0
- package/dist/protocol/messages.test.d.ts.map +1 -0
- package/dist/protocol/messages.test.js +55 -0
- package/dist/protocol/messages.test.js.map +1 -0
- package/dist/types/index.d.ts +247 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/result.d.ts +28 -0
- package/dist/types/result.d.ts.map +1 -0
- package/dist/types/result.js +16 -0
- package/dist/types/result.js.map +1 -0
- package/dist/utils/benchmark.d.ts +67 -0
- package/dist/utils/benchmark.d.ts.map +1 -0
- package/dist/utils/benchmark.js +179 -0
- package/dist/utils/benchmark.js.map +1 -0
- package/dist/utils/logger.d.ts +105 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +275 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/middleware.d.ts +85 -0
- package/dist/utils/middleware.d.ts.map +1 -0
- package/dist/utils/middleware.js +173 -0
- package/dist/utils/middleware.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +71 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +160 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/signature.d.ts +57 -0
- package/dist/utils/signature.d.ts.map +1 -0
- package/dist/utils/signature.js +102 -0
- package/dist/utils/signature.js.map +1 -0
- package/dist/utils/validation.d.ts +504 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +159 -0
- package/dist/utils/validation.js.map +1 -0
- package/docs/F2A-PROTOCOL.md +61 -0
- package/docs/MOBILE_BOOTSTRAP_DESIGN.md +126 -0
- package/docs/a2a-lessons.md +316 -0
- package/docs/middleware-guide.md +448 -0
- package/docs/readme-update-checklist.md +90 -0
- package/docs/reputation-guide.md +396 -0
- package/docs/rfcs/001-reputation-system.md +712 -0
- package/docs/security-design.md +247 -0
- package/install.sh +231 -0
- package/package.json +64 -0
- package/packages/openclaw-adapter/README.md +510 -0
- package/packages/openclaw-adapter/openclaw.plugin.json +106 -0
- package/packages/openclaw-adapter/package.json +40 -0
- package/packages/openclaw-adapter/src/announcement-queue.test.ts +449 -0
- package/packages/openclaw-adapter/src/announcement-queue.ts +403 -0
- package/packages/openclaw-adapter/src/capability-detector.test.ts +99 -0
- package/packages/openclaw-adapter/src/capability-detector.ts +183 -0
- package/packages/openclaw-adapter/src/claim-handlers.test.ts +974 -0
- package/packages/openclaw-adapter/src/claim-handlers.ts +482 -0
- package/packages/openclaw-adapter/src/connector.business.test.ts +583 -0
- package/packages/openclaw-adapter/src/connector.ts +795 -0
- package/packages/openclaw-adapter/src/index.test.ts +82 -0
- package/packages/openclaw-adapter/src/index.ts +18 -0
- package/packages/openclaw-adapter/src/integration.e2e.test.ts +829 -0
- package/packages/openclaw-adapter/src/logger.ts +51 -0
- package/packages/openclaw-adapter/src/network-client.test.ts +266 -0
- package/packages/openclaw-adapter/src/network-client.ts +251 -0
- package/packages/openclaw-adapter/src/network-recovery.test.ts +465 -0
- package/packages/openclaw-adapter/src/node-manager.test.ts +136 -0
- package/packages/openclaw-adapter/src/node-manager.ts +429 -0
- package/packages/openclaw-adapter/src/plugin.test.ts +439 -0
- package/packages/openclaw-adapter/src/plugin.ts +104 -0
- package/packages/openclaw-adapter/src/reputation.test.ts +221 -0
- package/packages/openclaw-adapter/src/reputation.ts +368 -0
- package/packages/openclaw-adapter/src/task-guard.test.ts +502 -0
- package/packages/openclaw-adapter/src/task-guard.ts +860 -0
- package/packages/openclaw-adapter/src/task-queue.concurrency.test.ts +462 -0
- package/packages/openclaw-adapter/src/task-queue.edge-cases.test.ts +284 -0
- package/packages/openclaw-adapter/src/task-queue.persistence.test.ts +408 -0
- package/packages/openclaw-adapter/src/task-queue.ts +668 -0
- package/packages/openclaw-adapter/src/tool-handlers.test.ts +906 -0
- package/packages/openclaw-adapter/src/tool-handlers.ts +574 -0
- package/packages/openclaw-adapter/src/types.ts +361 -0
- package/packages/openclaw-adapter/src/webhook-pusher.test.ts +188 -0
- package/packages/openclaw-adapter/src/webhook-pusher.ts +220 -0
- package/packages/openclaw-adapter/src/webhook-server.test.ts +580 -0
- package/packages/openclaw-adapter/src/webhook-server.ts +202 -0
- package/packages/openclaw-adapter/tsconfig.json +20 -0
- package/src/cli/commands.test.ts +157 -0
- package/src/cli/commands.ts +129 -0
- package/src/cli/index.test.ts +77 -0
- package/src/cli/index.ts +234 -0
- package/src/core/autonomous-economy.test.ts +291 -0
- package/src/core/autonomous-economy.ts +428 -0
- package/src/core/e2ee-crypto.test.ts +125 -0
- package/src/core/e2ee-crypto.ts +246 -0
- package/src/core/f2a.test.ts +269 -0
- package/src/core/f2a.ts +618 -0
- package/src/core/p2p-network.test.ts +199 -0
- package/src/core/p2p-network.ts +1432 -0
- package/src/core/reputation-security.test.ts +403 -0
- package/src/core/reputation-security.ts +562 -0
- package/src/core/reputation.test.ts +260 -0
- package/src/core/reputation.ts +576 -0
- package/src/core/review-committee.test.ts +380 -0
- package/src/core/review-committee.ts +401 -0
- package/src/core/token-manager.test.ts +133 -0
- package/src/core/token-manager.ts +140 -0
- package/src/daemon/control-server.test.ts +216 -0
- package/src/daemon/control-server.ts +292 -0
- package/src/daemon/index.test.ts +85 -0
- package/src/daemon/index.ts +89 -0
- package/src/daemon/main.ts +44 -0
- package/src/daemon/start.ts +29 -0
- package/src/daemon/webhook.test.ts +68 -0
- package/src/daemon/webhook.ts +105 -0
- package/src/index.test.ts +436 -0
- package/src/index.ts +72 -0
- package/src/types/index.test.ts +87 -0
- package/src/types/index.ts +341 -0
- package/src/types/result.ts +68 -0
- package/src/utils/benchmark.ts +237 -0
- package/src/utils/logger.ts +331 -0
- package/src/utils/middleware.ts +229 -0
- package/src/utils/rate-limiter.ts +207 -0
- package/src/utils/signature.ts +136 -0
- package/src/utils/validation.ts +186 -0
- package/tests/docker/Dockerfile.node +23 -0
- package/tests/docker/Dockerfile.runner +18 -0
- package/tests/docker/docker-compose.test.yml +73 -0
- package/tests/integration/message-passing.test.ts +109 -0
- package/tests/integration/multi-node.test.ts +92 -0
- package/tests/integration/p2p-connection.test.ts +83 -0
- package/tests/integration/test-config.ts +32 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +26 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F2A OpenClaw Connector - 业务逻辑测试
|
|
3
|
+
* 测试核心功能,使用真实业务逻辑而非无意义的 mock
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import { mkdirSync, rmSync, existsSync } from 'fs';
|
|
8
|
+
import { F2AOpenClawAdapter } from './connector.js';
|
|
9
|
+
import { ReputationSystem } from './reputation.js';
|
|
10
|
+
import { CapabilityDetector } from './capability-detector.js';
|
|
11
|
+
import { TaskQueue } from './task-queue.js';
|
|
12
|
+
import type { F2APluginConfig, AgentCapability, AgentInfo, TaskRequest } from './types.js';
|
|
13
|
+
|
|
14
|
+
describe('F2AOpenClawAdapter 业务逻辑', () => {
|
|
15
|
+
describe('配置合并', () => {
|
|
16
|
+
it('应该使用默认配置当未提供可选配置时', () => {
|
|
17
|
+
const connector = new F2AOpenClawAdapter();
|
|
18
|
+
const minimalConfig = {};
|
|
19
|
+
|
|
20
|
+
// 验证默认配置值
|
|
21
|
+
const mergedConfig = (connector as any).mergeConfig(minimalConfig);
|
|
22
|
+
|
|
23
|
+
expect(mergedConfig.autoStart).toBe(true);
|
|
24
|
+
expect(mergedConfig.webhookPort).toBe(9002);
|
|
25
|
+
expect(mergedConfig.agentName).toBe('OpenClaw Agent');
|
|
26
|
+
expect(mergedConfig.dataDir).toBe('./f2a-data');
|
|
27
|
+
expect(mergedConfig.maxQueuedTasks).toBe(100);
|
|
28
|
+
expect(mergedConfig.reputation.enabled).toBe(true);
|
|
29
|
+
expect(mergedConfig.reputation.initialScore).toBe(50);
|
|
30
|
+
expect(mergedConfig.security.requireConfirmation).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('应该覆盖默认配置当提供自定义值时', () => {
|
|
34
|
+
const connector = new F2AOpenClawAdapter();
|
|
35
|
+
const customConfig = {
|
|
36
|
+
autoStart: false,
|
|
37
|
+
webhookPort: 9999,
|
|
38
|
+
agentName: 'Custom Agent',
|
|
39
|
+
maxQueuedTasks: 50,
|
|
40
|
+
reputation: {
|
|
41
|
+
enabled: false,
|
|
42
|
+
initialScore: 100
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const mergedConfig = (connector as any).mergeConfig(customConfig);
|
|
47
|
+
|
|
48
|
+
expect(mergedConfig.autoStart).toBe(false);
|
|
49
|
+
expect(mergedConfig.webhookPort).toBe(9999);
|
|
50
|
+
expect(mergedConfig.agentName).toBe('Custom Agent');
|
|
51
|
+
expect(mergedConfig.maxQueuedTasks).toBe(50);
|
|
52
|
+
expect(mergedConfig.reputation.enabled).toBe(false);
|
|
53
|
+
expect(mergedConfig.reputation.initialScore).toBe(100);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('应该正确处理 bootstrapPeers 配置', () => {
|
|
57
|
+
const connector = new F2AOpenClawAdapter();
|
|
58
|
+
const configWithPeers = {
|
|
59
|
+
bootstrapPeers: ['/ip4/1.2.3.4/tcp/9000/p2p/peer-1', '/ip4/5.6.7.8/tcp/9000/p2p/peer-2']
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const mergedConfig = (connector as any).mergeConfig(configWithPeers);
|
|
63
|
+
|
|
64
|
+
expect(mergedConfig.bootstrapPeers).toHaveLength(2);
|
|
65
|
+
expect(mergedConfig.bootstrapPeers[0]).toBe('/ip4/1.2.3.4/tcp/9000/p2p/peer-1');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('Agent 解析', () => {
|
|
70
|
+
it('应该通过 #索引 格式解析 Agent', async () => {
|
|
71
|
+
const connector = new F2AOpenClawAdapter();
|
|
72
|
+
const mockAgents: AgentInfo[] = [
|
|
73
|
+
{ peerId: 'peer-1', displayName: 'Agent A', agentType: 'test', version: '1.0', capabilities: [], multiaddrs: [], lastSeen: Date.now() },
|
|
74
|
+
{ peerId: 'peer-2', displayName: 'Agent B', agentType: 'test', version: '1.0', capabilities: [], multiaddrs: [], lastSeen: Date.now() }
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// 模拟 discoverAgents 返回数据
|
|
78
|
+
(connector as any).networkClient = {
|
|
79
|
+
discoverAgents: async () => ({ success: true, data: mockAgents })
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const result = await (connector as any).resolveAgent('#1');
|
|
83
|
+
expect(result?.displayName).toBe('Agent A');
|
|
84
|
+
|
|
85
|
+
const result2 = await (connector as any).resolveAgent('#2');
|
|
86
|
+
expect(result2?.displayName).toBe('Agent B');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('应该通过 displayName 精确匹配解析 Agent', async () => {
|
|
90
|
+
const connector = new F2AOpenClawAdapter();
|
|
91
|
+
const mockAgents: AgentInfo[] = [
|
|
92
|
+
{ peerId: 'peer-1', displayName: 'MacBook-Pro', agentType: 'test', version: '1.0', capabilities: [], multiaddrs: [], lastSeen: Date.now() }
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
(connector as any).networkClient = {
|
|
96
|
+
discoverAgents: async () => ({ success: true, data: mockAgents })
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const result = await (connector as any).resolveAgent('MacBook-Pro');
|
|
100
|
+
expect(result?.peerId).toBe('peer-1');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('应该通过 peerId 前缀模糊匹配解析 Agent', async () => {
|
|
104
|
+
const connector = new F2AOpenClawAdapter();
|
|
105
|
+
const mockAgents: AgentInfo[] = [
|
|
106
|
+
{ peerId: 'f2a-abc123', displayName: 'Test Agent', agentType: 'test', version: '1.0', capabilities: [], multiaddrs: [], lastSeen: Date.now() }
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
(connector as any).networkClient = {
|
|
110
|
+
discoverAgents: async () => ({ success: true, data: mockAgents })
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const result = await (connector as any).resolveAgent('f2a-ab');
|
|
114
|
+
expect(result?.peerId).toBe('f2a-abc123');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('应该返回 null 当找不到匹配的 Agent', async () => {
|
|
118
|
+
const connector = new F2AOpenClawAdapter();
|
|
119
|
+
|
|
120
|
+
(connector as any).networkClient = {
|
|
121
|
+
discoverAgents: async () => ({ success: true, data: [] })
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = await (connector as any).resolveAgent('NonExistent');
|
|
125
|
+
expect(result).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('应该通过 displayName 模糊匹配解析 Agent', async () => {
|
|
129
|
+
const connector = new F2AOpenClawAdapter();
|
|
130
|
+
const mockAgents: AgentInfo[] = [
|
|
131
|
+
{ peerId: 'peer-1', displayName: 'My Special Agent', agentType: 'test', version: '1.0', capabilities: [], multiaddrs: [], lastSeen: Date.now() }
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
(connector as any).networkClient = {
|
|
135
|
+
discoverAgents: async () => ({ success: true, data: mockAgents })
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const result = await (connector as any).resolveAgent('special');
|
|
139
|
+
expect(result?.peerId).toBe('peer-1');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('广播结果格式化', () => {
|
|
144
|
+
it('应该正确格式化成功的广播结果', () => {
|
|
145
|
+
const connector = new F2AOpenClawAdapter();
|
|
146
|
+
const results = [
|
|
147
|
+
{ agent: 'Agent A', success: true, latency: 100 },
|
|
148
|
+
{ agent: 'Agent B', success: true, latency: 200 }
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const formatted = (connector as any).formatBroadcastResults(results);
|
|
152
|
+
|
|
153
|
+
expect(formatted).toContain('✅ Agent A (100ms)');
|
|
154
|
+
expect(formatted).toContain('✅ Agent B (200ms)');
|
|
155
|
+
expect(formatted).toContain('完成');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('应该正确格式化失败的广播结果', () => {
|
|
159
|
+
const connector = new F2AOpenClawAdapter();
|
|
160
|
+
const results = [
|
|
161
|
+
{ agent: 'Agent A', success: false, error: 'Timeout' }
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const formatted = (connector as any).formatBroadcastResults(results);
|
|
165
|
+
|
|
166
|
+
expect(formatted).toContain('❌ Agent A');
|
|
167
|
+
expect(formatted).toContain('失败: Timeout');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('应该处理没有延迟信息的结果', () => {
|
|
171
|
+
const connector = new F2AOpenClawAdapter();
|
|
172
|
+
const results = [
|
|
173
|
+
{ agent: 'Agent A', success: true }
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const formatted = (connector as any).formatBroadcastResults(results);
|
|
177
|
+
|
|
178
|
+
expect(formatted).toContain('✅ Agent A');
|
|
179
|
+
expect(formatted).not.toContain('undefined');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('Token 生成', () => {
|
|
184
|
+
it('应该生成符合格式的 token', () => {
|
|
185
|
+
const connector = new F2AOpenClawAdapter();
|
|
186
|
+
const token = (connector as any).generateToken();
|
|
187
|
+
|
|
188
|
+
expect(token).toMatch(/^f2a-[A-Za-z0-9]{32}$/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('应该生成不同的 token 每次调用', () => {
|
|
192
|
+
const connector = new F2AOpenClawAdapter();
|
|
193
|
+
const token1 = (connector as any).generateToken();
|
|
194
|
+
const token2 = (connector as any).generateToken();
|
|
195
|
+
|
|
196
|
+
expect(token1).not.toBe(token2);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('ReputationSystem 业务逻辑', () => {
|
|
202
|
+
const testDir = '/tmp/f2a-test-reputation-' + Date.now();
|
|
203
|
+
|
|
204
|
+
beforeEach(() => {
|
|
205
|
+
// 创建测试目录
|
|
206
|
+
try {
|
|
207
|
+
mkdirSync(testDir, { recursive: true });
|
|
208
|
+
} catch (e) {
|
|
209
|
+
// 目录可能已存在
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
afterEach(() => {
|
|
214
|
+
// 清理测试目录
|
|
215
|
+
try {
|
|
216
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
217
|
+
} catch (e) {
|
|
218
|
+
// 忽略清理错误
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('信誉计算', () => {
|
|
223
|
+
it('新 peer 应该获得初始信誉分', () => {
|
|
224
|
+
const config = { enabled: true, initialScore: 50, minScoreForService: 20, decayRate: 0.01 };
|
|
225
|
+
const reputation = new ReputationSystem(config, testDir);
|
|
226
|
+
|
|
227
|
+
const rep = reputation.getReputation('new-peer');
|
|
228
|
+
expect(rep.score).toBe(50);
|
|
229
|
+
expect(rep.successfulTasks).toBe(0);
|
|
230
|
+
expect(rep.failedTasks).toBe(0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('成功任务应该增加信誉分', () => {
|
|
234
|
+
const config = { enabled: true, initialScore: 50, minScoreForService: 20, decayRate: 0.01 };
|
|
235
|
+
const reputation = new ReputationSystem(config, testDir);
|
|
236
|
+
|
|
237
|
+
reputation.recordSuccess('peer-1', 'task-1', 100);
|
|
238
|
+
const rep = reputation.getReputation('peer-1');
|
|
239
|
+
|
|
240
|
+
expect(rep.score).toBeGreaterThan(50);
|
|
241
|
+
expect(rep.successfulTasks).toBe(1);
|
|
242
|
+
expect(rep.avgResponseTime).toBe(100);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('多次成功任务应该累计', () => {
|
|
246
|
+
const config = { enabled: true, initialScore: 50, minScoreForService: 20, decayRate: 0.01 };
|
|
247
|
+
const reputation = new ReputationSystem(config, testDir);
|
|
248
|
+
|
|
249
|
+
reputation.recordSuccess('peer-1', 'task-1', 100);
|
|
250
|
+
reputation.recordSuccess('peer-1', 'task-2', 200);
|
|
251
|
+
const rep = reputation.getReputation('peer-1');
|
|
252
|
+
|
|
253
|
+
expect(rep.successfulTasks).toBe(2);
|
|
254
|
+
// 平均响应时间使用加权移动平均: 100 * 0.7 + 200 * 0.3 = 130
|
|
255
|
+
expect(rep.avgResponseTime).toBe(130);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('失败任务应该降低信誉分', () => {
|
|
259
|
+
const config = { enabled: true, initialScore: 50, minScoreForService: 20, decayRate: 0.01 };
|
|
260
|
+
const reputation = new ReputationSystem(config, testDir);
|
|
261
|
+
|
|
262
|
+
reputation.recordFailure('peer-1', 'task-1', 'Error');
|
|
263
|
+
const rep = reputation.getReputation('peer-1');
|
|
264
|
+
|
|
265
|
+
expect(rep.score).toBeLessThan(50);
|
|
266
|
+
expect(rep.failedTasks).toBe(1);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('信誉低于阈值应该被拒绝服务', () => {
|
|
270
|
+
const config = { enabled: true, initialScore: 50, minScoreForService: 20, decayRate: 0.01 };
|
|
271
|
+
const reputation = new ReputationSystem(config, testDir);
|
|
272
|
+
|
|
273
|
+
// 多次失败降低信誉
|
|
274
|
+
for (let i = 0; i < 10; i++) {
|
|
275
|
+
reputation.recordFailure('bad-peer', `task-${i}`, 'Error');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
expect(reputation.isAllowed('bad-peer')).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('信誉高于阈值应该被允许服务', () => {
|
|
282
|
+
const config = { enabled: true, initialScore: 50, minScoreForService: 20, decayRate: 0.01 };
|
|
283
|
+
const reputation = new ReputationSystem(config, testDir);
|
|
284
|
+
|
|
285
|
+
reputation.recordSuccess('good-peer', 'task-1', 100);
|
|
286
|
+
|
|
287
|
+
expect(reputation.isAllowed('good-peer')).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('禁用信誉系统时应该允许所有 peer', () => {
|
|
291
|
+
const config = { enabled: false, initialScore: 50, minScoreForService: 20, decayRate: 0.01 };
|
|
292
|
+
const reputation = new ReputationSystem(config, testDir);
|
|
293
|
+
|
|
294
|
+
// 即使多次失败也应该允许
|
|
295
|
+
for (let i = 0; i < 20; i++) {
|
|
296
|
+
reputation.recordFailure('any-peer', `task-${i}`, 'Error');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
expect(reputation.isAllowed('any-peer')).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('历史记录', () => {
|
|
304
|
+
it('应该记录成功任务历史', () => {
|
|
305
|
+
const config = { enabled: true, initialScore: 50, minScoreForService: 20, decayRate: 0.01 };
|
|
306
|
+
const reputation = new ReputationSystem(config, testDir);
|
|
307
|
+
|
|
308
|
+
reputation.recordSuccess('peer-1', 'task-1', 100);
|
|
309
|
+
const rep = reputation.getReputation('peer-1');
|
|
310
|
+
|
|
311
|
+
expect(rep.history.length).toBe(1);
|
|
312
|
+
expect(rep.history[0].type).toBe('task_success');
|
|
313
|
+
expect(rep.history[0].taskId).toBe('task-1');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('应该记录失败任务历史', () => {
|
|
317
|
+
const config = { enabled: true, initialScore: 50, minScoreForService: 20, decayRate: 0.01 };
|
|
318
|
+
const reputation = new ReputationSystem(config, testDir);
|
|
319
|
+
|
|
320
|
+
reputation.recordFailure('peer-1', 'task-1', 'Timeout');
|
|
321
|
+
const rep = reputation.getReputation('peer-1');
|
|
322
|
+
|
|
323
|
+
expect(rep.history.length).toBe(1);
|
|
324
|
+
expect(rep.history[0].type).toBe('task_failure');
|
|
325
|
+
expect(rep.history[0].reason).toBe('Timeout');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('应该限制历史记录数量', () => {
|
|
329
|
+
const config = { enabled: true, initialScore: 50, minScoreForService: 20, decayRate: 0.01 };
|
|
330
|
+
const reputation = new ReputationSystem(config, testDir);
|
|
331
|
+
|
|
332
|
+
// 添加超过 100 条记录
|
|
333
|
+
for (let i = 0; i < 110; i++) {
|
|
334
|
+
reputation.recordSuccess('peer-1', `task-${i}`, 100);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const rep = reputation.getReputation('peer-1');
|
|
338
|
+
expect(rep.history.length).toBeLessThanOrEqual(100);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe('高信誉节点', () => {
|
|
343
|
+
it('应该返回信誉高于阈值的节点', () => {
|
|
344
|
+
const config = { enabled: true, initialScore: 50, minScoreForService: 20, decayRate: 0.01 };
|
|
345
|
+
const reputation = new ReputationSystem(config, testDir);
|
|
346
|
+
|
|
347
|
+
// peer-1: 高信誉
|
|
348
|
+
reputation.recordSuccess('peer-1', 'task-1', 100);
|
|
349
|
+
reputation.recordSuccess('peer-1', 'task-2', 100);
|
|
350
|
+
|
|
351
|
+
// peer-2: 低信誉
|
|
352
|
+
reputation.recordFailure('peer-2', 'task-1', 'Error');
|
|
353
|
+
|
|
354
|
+
const highRepNodes = reputation.getHighReputationNodes(55);
|
|
355
|
+
const peerIds = highRepNodes.map(n => n.peerId);
|
|
356
|
+
|
|
357
|
+
expect(peerIds).toContain('peer-1');
|
|
358
|
+
expect(peerIds).not.toContain('peer-2');
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('CapabilityDetector 业务逻辑', () => {
|
|
364
|
+
describe('getDefaultCapabilities', () => {
|
|
365
|
+
it('应该返回默认能力列表', () => {
|
|
366
|
+
const detector = new CapabilityDetector();
|
|
367
|
+
const capabilities = detector.getDefaultCapabilities();
|
|
368
|
+
|
|
369
|
+
expect(capabilities.length).toBeGreaterThan(0);
|
|
370
|
+
expect(capabilities.some(c => c.name === 'file-operation')).toBe(true);
|
|
371
|
+
expect(capabilities.some(c => c.name === 'command-execution')).toBe(true);
|
|
372
|
+
expect(capabilities.some(c => c.name === 'web-browsing')).toBe(true);
|
|
373
|
+
expect(capabilities.some(c => c.name === 'code-generation')).toBe(true);
|
|
374
|
+
expect(capabilities.some(c => c.name === 'task-delegation')).toBe(true);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('每个能力应该有完整的定义', () => {
|
|
378
|
+
const detector = new CapabilityDetector();
|
|
379
|
+
const capabilities = detector.getDefaultCapabilities();
|
|
380
|
+
|
|
381
|
+
for (const cap of capabilities) {
|
|
382
|
+
expect(cap.name).toBeDefined();
|
|
383
|
+
expect(cap.description).toBeDefined();
|
|
384
|
+
expect(cap.parameters).toBeDefined();
|
|
385
|
+
expect(Object.keys(cap.parameters).length).toBeGreaterThan(0);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe('mergeCustomCapabilities', () => {
|
|
391
|
+
it('应该添加自定义能力', () => {
|
|
392
|
+
const detector = new CapabilityDetector();
|
|
393
|
+
const defaults = detector.getDefaultCapabilities();
|
|
394
|
+
const custom = ['custom-ml', 'custom-analysis'];
|
|
395
|
+
|
|
396
|
+
const merged = detector.mergeCustomCapabilities(defaults, custom);
|
|
397
|
+
|
|
398
|
+
expect(merged.length).toBe(defaults.length + 2);
|
|
399
|
+
expect(merged.some(c => c.name === 'custom-ml')).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('不应该重复已存在的能力', () => {
|
|
403
|
+
const detector = new CapabilityDetector();
|
|
404
|
+
const defaults = detector.getDefaultCapabilities();
|
|
405
|
+
const custom = ['code-generation']; // 已存在
|
|
406
|
+
|
|
407
|
+
const merged = detector.mergeCustomCapabilities(defaults, custom);
|
|
408
|
+
|
|
409
|
+
const count = merged.filter(c => c.name === 'code-generation').length;
|
|
410
|
+
expect(count).toBe(1);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe('TaskQueue 业务逻辑', () => {
|
|
416
|
+
let taskQueue: TaskQueue;
|
|
417
|
+
|
|
418
|
+
beforeEach(() => {
|
|
419
|
+
taskQueue = new TaskQueue({ maxSize: 10, maxAgeMs: 60000 });
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('任务管理', () => {
|
|
423
|
+
it('应该添加任务到队列', () => {
|
|
424
|
+
const task: TaskRequest = {
|
|
425
|
+
taskId: 'task-1',
|
|
426
|
+
taskType: 'test',
|
|
427
|
+
description: 'Test task',
|
|
428
|
+
from: 'peer-1',
|
|
429
|
+
timestamp: Date.now(),
|
|
430
|
+
timeout: 60000
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const queued = taskQueue.add(task);
|
|
434
|
+
|
|
435
|
+
expect(queued.taskId).toBe('task-1');
|
|
436
|
+
expect(queued.status).toBe('pending');
|
|
437
|
+
expect(queued.createdAt).toBeDefined();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('应该获取待处理任务', () => {
|
|
441
|
+
const task1 = { taskId: 'task-1', taskType: 'test', description: 'Task 1', from: 'peer-1', timestamp: Date.now(), timeout: 60000 };
|
|
442
|
+
const task2 = { taskId: 'task-2', taskType: 'test', description: 'Task 2', from: 'peer-2', timestamp: Date.now(), timeout: 60000 };
|
|
443
|
+
|
|
444
|
+
taskQueue.add(task1 as TaskRequest);
|
|
445
|
+
taskQueue.add(task2 as TaskRequest);
|
|
446
|
+
|
|
447
|
+
const pending = taskQueue.getPending();
|
|
448
|
+
|
|
449
|
+
expect(pending.length).toBe(2);
|
|
450
|
+
expect(pending[0].taskId).toBe('task-1'); // FIFO 顺序
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('应该限制返回的待处理任务数量', () => {
|
|
454
|
+
for (let i = 0; i < 5; i++) {
|
|
455
|
+
taskQueue.add({
|
|
456
|
+
taskId: `task-${i}`,
|
|
457
|
+
taskType: 'test',
|
|
458
|
+
description: `Task ${i}`,
|
|
459
|
+
from: 'peer-1',
|
|
460
|
+
timestamp: Date.now(),
|
|
461
|
+
timeout: 60000
|
|
462
|
+
} as TaskRequest);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const pending = taskQueue.getPending(3);
|
|
466
|
+
expect(pending.length).toBe(3);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('应该标记任务为处理中', () => {
|
|
470
|
+
const task = {
|
|
471
|
+
taskId: 'task-1',
|
|
472
|
+
taskType: 'test',
|
|
473
|
+
description: 'Test',
|
|
474
|
+
from: 'peer-1',
|
|
475
|
+
timestamp: Date.now(),
|
|
476
|
+
timeout: 60000
|
|
477
|
+
} as TaskRequest;
|
|
478
|
+
|
|
479
|
+
taskQueue.add(task);
|
|
480
|
+
const processing = taskQueue.markProcessing('task-1');
|
|
481
|
+
|
|
482
|
+
expect(processing?.status).toBe('processing');
|
|
483
|
+
expect(processing?.updatedAt).toBeDefined();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('应该完成任务并记录结果', () => {
|
|
487
|
+
const task = {
|
|
488
|
+
taskId: 'task-1',
|
|
489
|
+
taskType: 'test',
|
|
490
|
+
description: 'Test',
|
|
491
|
+
from: 'peer-1',
|
|
492
|
+
timestamp: Date.now(),
|
|
493
|
+
timeout: 60000
|
|
494
|
+
} as TaskRequest;
|
|
495
|
+
|
|
496
|
+
taskQueue.add(task);
|
|
497
|
+
const completed = taskQueue.complete('task-1', {
|
|
498
|
+
taskId: 'task-1',
|
|
499
|
+
status: 'success',
|
|
500
|
+
result: 'Done',
|
|
501
|
+
latency: 1000
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
expect(completed?.status).toBe('completed');
|
|
505
|
+
expect(completed?.result).toBe('Done');
|
|
506
|
+
expect(completed?.latency).toBe(1000);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('应该标记失败任务', () => {
|
|
510
|
+
const task = {
|
|
511
|
+
taskId: 'task-1',
|
|
512
|
+
taskType: 'test',
|
|
513
|
+
description: 'Test',
|
|
514
|
+
from: 'peer-1',
|
|
515
|
+
timestamp: Date.now(),
|
|
516
|
+
timeout: 60000
|
|
517
|
+
} as TaskRequest;
|
|
518
|
+
|
|
519
|
+
taskQueue.add(task);
|
|
520
|
+
const failed = taskQueue.complete('task-1', {
|
|
521
|
+
taskId: 'task-1',
|
|
522
|
+
status: 'error',
|
|
523
|
+
error: 'Something went wrong',
|
|
524
|
+
latency: 500
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
expect(failed?.status).toBe('failed');
|
|
528
|
+
expect(failed?.error).toBe('Something went wrong');
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
describe('队列统计', () => {
|
|
533
|
+
it('应该返回正确的统计信息', () => {
|
|
534
|
+
// 添加不同状态的任务
|
|
535
|
+
taskQueue.add({ taskId: 'p1', taskType: 'test', description: 'P1', from: 'peer-1', timestamp: Date.now(), timeout: 60000 } as TaskRequest);
|
|
536
|
+
taskQueue.add({ taskId: 'p2', taskType: 'test', description: 'P2', from: 'peer-1', timestamp: Date.now(), timeout: 60000 } as TaskRequest);
|
|
537
|
+
|
|
538
|
+
taskQueue.add({ taskId: 'proc1', taskType: 'test', description: 'Proc1', from: 'peer-1', timestamp: Date.now(), timeout: 60000 } as TaskRequest);
|
|
539
|
+
taskQueue.markProcessing('proc1');
|
|
540
|
+
|
|
541
|
+
taskQueue.add({ taskId: 'comp1', taskType: 'test', description: 'Comp1', from: 'peer-1', timestamp: Date.now(), timeout: 60000 } as TaskRequest);
|
|
542
|
+
taskQueue.complete('comp1', { taskId: 'comp1', status: 'success', latency: 100 });
|
|
543
|
+
|
|
544
|
+
taskQueue.add({ taskId: 'fail1', taskType: 'test', description: 'Fail1', from: 'peer-1', timestamp: Date.now(), timeout: 60000 } as TaskRequest);
|
|
545
|
+
taskQueue.complete('fail1', { taskId: 'fail1', status: 'error', error: 'Failed', latency: 100 });
|
|
546
|
+
|
|
547
|
+
const stats = taskQueue.getStats();
|
|
548
|
+
|
|
549
|
+
expect(stats.pending).toBe(2);
|
|
550
|
+
expect(stats.processing).toBe(1);
|
|
551
|
+
expect(stats.completed).toBe(1);
|
|
552
|
+
expect(stats.failed).toBe(1);
|
|
553
|
+
expect(stats.total).toBe(5); // 所有任务都计入 total
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
describe('队列限制', () => {
|
|
558
|
+
it('应该限制队列大小', () => {
|
|
559
|
+
const smallQueue = new TaskQueue({ maxSize: 3 });
|
|
560
|
+
|
|
561
|
+
smallQueue.add({ taskId: 't1', taskType: 'test', description: 'T1', from: 'peer-1', timestamp: Date.now(), timeout: 60000 } as TaskRequest);
|
|
562
|
+
smallQueue.add({ taskId: 't2', taskType: 'test', description: 'T2', from: 'peer-1', timestamp: Date.now(), timeout: 60000 } as TaskRequest);
|
|
563
|
+
smallQueue.add({ taskId: 't3', taskType: 'test', description: 'T3', from: 'peer-1', timestamp: Date.now(), timeout: 60000 } as TaskRequest);
|
|
564
|
+
|
|
565
|
+
expect(() => {
|
|
566
|
+
smallQueue.add({ taskId: 't4', taskType: 'test', description: 'T4', from: 'peer-1', timestamp: Date.now(), timeout: 60000 } as TaskRequest);
|
|
567
|
+
}).toThrow('Task queue is full');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('应该清理过期任务', () => {
|
|
571
|
+
const queue = new TaskQueue({ maxAgeMs: 100 }); // 100ms 过期
|
|
572
|
+
|
|
573
|
+
queue.add({ taskId: 'old', taskType: 'test', description: 'Old', from: 'peer-1', timestamp: Date.now(), timeout: 60000 } as TaskRequest);
|
|
574
|
+
|
|
575
|
+
// 等待过期
|
|
576
|
+
setTimeout(() => {
|
|
577
|
+
queue.cleanup();
|
|
578
|
+
const stats = queue.getStats();
|
|
579
|
+
expect(stats.total).toBe(0);
|
|
580
|
+
}, 150);
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
});
|