@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.
Files changed (234) hide show
  1. package/.github/workflows/ci.yml +113 -0
  2. package/.github/workflows/publish.yml +60 -0
  3. package/LICENSE +21 -0
  4. package/MONOREPO.md +58 -0
  5. package/README.md +280 -0
  6. package/SKILL.md +137 -0
  7. package/dist/adapters/openclaw.d.ts +103 -0
  8. package/dist/adapters/openclaw.d.ts.map +1 -0
  9. package/dist/adapters/openclaw.js +297 -0
  10. package/dist/adapters/openclaw.js.map +1 -0
  11. package/dist/cli/commands.d.ts +17 -0
  12. package/dist/cli/commands.d.ts.map +1 -0
  13. package/dist/cli/commands.js +107 -0
  14. package/dist/cli/commands.js.map +1 -0
  15. package/dist/cli/index.d.ts +6 -0
  16. package/dist/cli/index.d.ts.map +1 -0
  17. package/dist/cli/index.js +203 -0
  18. package/dist/cli/index.js.map +1 -0
  19. package/dist/core/autonomous-economy.d.ts +136 -0
  20. package/dist/core/autonomous-economy.d.ts.map +1 -0
  21. package/dist/core/autonomous-economy.js +255 -0
  22. package/dist/core/autonomous-economy.js.map +1 -0
  23. package/dist/core/connection-manager.d.ts +80 -0
  24. package/dist/core/connection-manager.d.ts.map +1 -0
  25. package/dist/core/connection-manager.js +235 -0
  26. package/dist/core/connection-manager.js.map +1 -0
  27. package/dist/core/connection-manager.test.d.ts +2 -0
  28. package/dist/core/connection-manager.test.d.ts.map +1 -0
  29. package/dist/core/connection-manager.test.js +52 -0
  30. package/dist/core/connection-manager.test.js.map +1 -0
  31. package/dist/core/e2ee-crypto.d.ts +90 -0
  32. package/dist/core/e2ee-crypto.d.ts.map +1 -0
  33. package/dist/core/e2ee-crypto.js +190 -0
  34. package/dist/core/e2ee-crypto.js.map +1 -0
  35. package/dist/core/f2a.d.ts +126 -0
  36. package/dist/core/f2a.d.ts.map +1 -0
  37. package/dist/core/f2a.js +425 -0
  38. package/dist/core/f2a.js.map +1 -0
  39. package/dist/core/identity.d.ts +47 -0
  40. package/dist/core/identity.d.ts.map +1 -0
  41. package/dist/core/identity.js +130 -0
  42. package/dist/core/identity.js.map +1 -0
  43. package/dist/core/identity.test.d.ts +2 -0
  44. package/dist/core/identity.test.d.ts.map +1 -0
  45. package/dist/core/identity.test.js +43 -0
  46. package/dist/core/identity.test.js.map +1 -0
  47. package/dist/core/p2p-network.d.ts +242 -0
  48. package/dist/core/p2p-network.d.ts.map +1 -0
  49. package/dist/core/p2p-network.js +1182 -0
  50. package/dist/core/p2p-network.js.map +1 -0
  51. package/dist/core/reputation-security.d.ts +168 -0
  52. package/dist/core/reputation-security.d.ts.map +1 -0
  53. package/dist/core/reputation-security.js +369 -0
  54. package/dist/core/reputation-security.js.map +1 -0
  55. package/dist/core/reputation.d.ts +179 -0
  56. package/dist/core/reputation.d.ts.map +1 -0
  57. package/dist/core/reputation.js +472 -0
  58. package/dist/core/reputation.js.map +1 -0
  59. package/dist/core/review-committee.d.ts +130 -0
  60. package/dist/core/review-committee.d.ts.map +1 -0
  61. package/dist/core/review-committee.js +251 -0
  62. package/dist/core/review-committee.js.map +1 -0
  63. package/dist/core/serverless.d.ts +155 -0
  64. package/dist/core/serverless.d.ts.map +1 -0
  65. package/dist/core/serverless.js +615 -0
  66. package/dist/core/serverless.js.map +1 -0
  67. package/dist/core/token-manager.d.ts +42 -0
  68. package/dist/core/token-manager.d.ts.map +1 -0
  69. package/dist/core/token-manager.js +122 -0
  70. package/dist/core/token-manager.js.map +1 -0
  71. package/dist/daemon/control-server.d.ts +55 -0
  72. package/dist/daemon/control-server.d.ts.map +1 -0
  73. package/dist/daemon/control-server.js +262 -0
  74. package/dist/daemon/control-server.js.map +1 -0
  75. package/dist/daemon/index.d.ts +35 -0
  76. package/dist/daemon/index.d.ts.map +1 -0
  77. package/dist/daemon/index.js +69 -0
  78. package/dist/daemon/index.js.map +1 -0
  79. package/dist/daemon/main.d.ts +6 -0
  80. package/dist/daemon/main.d.ts.map +1 -0
  81. package/dist/daemon/main.js +38 -0
  82. package/dist/daemon/main.js.map +1 -0
  83. package/dist/daemon/start.d.ts +6 -0
  84. package/dist/daemon/start.d.ts.map +1 -0
  85. package/dist/daemon/start.js +25 -0
  86. package/dist/daemon/start.js.map +1 -0
  87. package/dist/daemon/webhook.d.ts +30 -0
  88. package/dist/daemon/webhook.d.ts.map +1 -0
  89. package/dist/daemon/webhook.js +86 -0
  90. package/dist/daemon/webhook.js.map +1 -0
  91. package/dist/daemon/webhook.test.d.ts +2 -0
  92. package/dist/daemon/webhook.test.d.ts.map +1 -0
  93. package/dist/daemon/webhook.test.js +24 -0
  94. package/dist/daemon/webhook.test.js.map +1 -0
  95. package/dist/index.d.ts +24 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +25 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/protocol/messages.d.ts +739 -0
  100. package/dist/protocol/messages.d.ts.map +1 -0
  101. package/dist/protocol/messages.js +188 -0
  102. package/dist/protocol/messages.js.map +1 -0
  103. package/dist/protocol/messages.test.d.ts +2 -0
  104. package/dist/protocol/messages.test.d.ts.map +1 -0
  105. package/dist/protocol/messages.test.js +55 -0
  106. package/dist/protocol/messages.test.js.map +1 -0
  107. package/dist/types/index.d.ts +247 -0
  108. package/dist/types/index.d.ts.map +1 -0
  109. package/dist/types/index.js +10 -0
  110. package/dist/types/index.js.map +1 -0
  111. package/dist/types/result.d.ts +28 -0
  112. package/dist/types/result.d.ts.map +1 -0
  113. package/dist/types/result.js +16 -0
  114. package/dist/types/result.js.map +1 -0
  115. package/dist/utils/benchmark.d.ts +67 -0
  116. package/dist/utils/benchmark.d.ts.map +1 -0
  117. package/dist/utils/benchmark.js +179 -0
  118. package/dist/utils/benchmark.js.map +1 -0
  119. package/dist/utils/logger.d.ts +105 -0
  120. package/dist/utils/logger.d.ts.map +1 -0
  121. package/dist/utils/logger.js +275 -0
  122. package/dist/utils/logger.js.map +1 -0
  123. package/dist/utils/middleware.d.ts +85 -0
  124. package/dist/utils/middleware.d.ts.map +1 -0
  125. package/dist/utils/middleware.js +173 -0
  126. package/dist/utils/middleware.js.map +1 -0
  127. package/dist/utils/rate-limiter.d.ts +71 -0
  128. package/dist/utils/rate-limiter.d.ts.map +1 -0
  129. package/dist/utils/rate-limiter.js +160 -0
  130. package/dist/utils/rate-limiter.js.map +1 -0
  131. package/dist/utils/signature.d.ts +57 -0
  132. package/dist/utils/signature.d.ts.map +1 -0
  133. package/dist/utils/signature.js +102 -0
  134. package/dist/utils/signature.js.map +1 -0
  135. package/dist/utils/validation.d.ts +504 -0
  136. package/dist/utils/validation.d.ts.map +1 -0
  137. package/dist/utils/validation.js +159 -0
  138. package/dist/utils/validation.js.map +1 -0
  139. package/docs/F2A-PROTOCOL.md +61 -0
  140. package/docs/MOBILE_BOOTSTRAP_DESIGN.md +126 -0
  141. package/docs/a2a-lessons.md +316 -0
  142. package/docs/middleware-guide.md +448 -0
  143. package/docs/readme-update-checklist.md +90 -0
  144. package/docs/reputation-guide.md +396 -0
  145. package/docs/rfcs/001-reputation-system.md +712 -0
  146. package/docs/security-design.md +247 -0
  147. package/install.sh +231 -0
  148. package/package.json +64 -0
  149. package/packages/openclaw-adapter/README.md +510 -0
  150. package/packages/openclaw-adapter/openclaw.plugin.json +106 -0
  151. package/packages/openclaw-adapter/package.json +40 -0
  152. package/packages/openclaw-adapter/src/announcement-queue.test.ts +449 -0
  153. package/packages/openclaw-adapter/src/announcement-queue.ts +403 -0
  154. package/packages/openclaw-adapter/src/capability-detector.test.ts +99 -0
  155. package/packages/openclaw-adapter/src/capability-detector.ts +183 -0
  156. package/packages/openclaw-adapter/src/claim-handlers.test.ts +974 -0
  157. package/packages/openclaw-adapter/src/claim-handlers.ts +482 -0
  158. package/packages/openclaw-adapter/src/connector.business.test.ts +583 -0
  159. package/packages/openclaw-adapter/src/connector.ts +795 -0
  160. package/packages/openclaw-adapter/src/index.test.ts +82 -0
  161. package/packages/openclaw-adapter/src/index.ts +18 -0
  162. package/packages/openclaw-adapter/src/integration.e2e.test.ts +829 -0
  163. package/packages/openclaw-adapter/src/logger.ts +51 -0
  164. package/packages/openclaw-adapter/src/network-client.test.ts +266 -0
  165. package/packages/openclaw-adapter/src/network-client.ts +251 -0
  166. package/packages/openclaw-adapter/src/network-recovery.test.ts +465 -0
  167. package/packages/openclaw-adapter/src/node-manager.test.ts +136 -0
  168. package/packages/openclaw-adapter/src/node-manager.ts +429 -0
  169. package/packages/openclaw-adapter/src/plugin.test.ts +439 -0
  170. package/packages/openclaw-adapter/src/plugin.ts +104 -0
  171. package/packages/openclaw-adapter/src/reputation.test.ts +221 -0
  172. package/packages/openclaw-adapter/src/reputation.ts +368 -0
  173. package/packages/openclaw-adapter/src/task-guard.test.ts +502 -0
  174. package/packages/openclaw-adapter/src/task-guard.ts +860 -0
  175. package/packages/openclaw-adapter/src/task-queue.concurrency.test.ts +462 -0
  176. package/packages/openclaw-adapter/src/task-queue.edge-cases.test.ts +284 -0
  177. package/packages/openclaw-adapter/src/task-queue.persistence.test.ts +408 -0
  178. package/packages/openclaw-adapter/src/task-queue.ts +668 -0
  179. package/packages/openclaw-adapter/src/tool-handlers.test.ts +906 -0
  180. package/packages/openclaw-adapter/src/tool-handlers.ts +574 -0
  181. package/packages/openclaw-adapter/src/types.ts +361 -0
  182. package/packages/openclaw-adapter/src/webhook-pusher.test.ts +188 -0
  183. package/packages/openclaw-adapter/src/webhook-pusher.ts +220 -0
  184. package/packages/openclaw-adapter/src/webhook-server.test.ts +580 -0
  185. package/packages/openclaw-adapter/src/webhook-server.ts +202 -0
  186. package/packages/openclaw-adapter/tsconfig.json +20 -0
  187. package/src/cli/commands.test.ts +157 -0
  188. package/src/cli/commands.ts +129 -0
  189. package/src/cli/index.test.ts +77 -0
  190. package/src/cli/index.ts +234 -0
  191. package/src/core/autonomous-economy.test.ts +291 -0
  192. package/src/core/autonomous-economy.ts +428 -0
  193. package/src/core/e2ee-crypto.test.ts +125 -0
  194. package/src/core/e2ee-crypto.ts +246 -0
  195. package/src/core/f2a.test.ts +269 -0
  196. package/src/core/f2a.ts +618 -0
  197. package/src/core/p2p-network.test.ts +199 -0
  198. package/src/core/p2p-network.ts +1432 -0
  199. package/src/core/reputation-security.test.ts +403 -0
  200. package/src/core/reputation-security.ts +562 -0
  201. package/src/core/reputation.test.ts +260 -0
  202. package/src/core/reputation.ts +576 -0
  203. package/src/core/review-committee.test.ts +380 -0
  204. package/src/core/review-committee.ts +401 -0
  205. package/src/core/token-manager.test.ts +133 -0
  206. package/src/core/token-manager.ts +140 -0
  207. package/src/daemon/control-server.test.ts +216 -0
  208. package/src/daemon/control-server.ts +292 -0
  209. package/src/daemon/index.test.ts +85 -0
  210. package/src/daemon/index.ts +89 -0
  211. package/src/daemon/main.ts +44 -0
  212. package/src/daemon/start.ts +29 -0
  213. package/src/daemon/webhook.test.ts +68 -0
  214. package/src/daemon/webhook.ts +105 -0
  215. package/src/index.test.ts +436 -0
  216. package/src/index.ts +72 -0
  217. package/src/types/index.test.ts +87 -0
  218. package/src/types/index.ts +341 -0
  219. package/src/types/result.ts +68 -0
  220. package/src/utils/benchmark.ts +237 -0
  221. package/src/utils/logger.ts +331 -0
  222. package/src/utils/middleware.ts +229 -0
  223. package/src/utils/rate-limiter.ts +207 -0
  224. package/src/utils/signature.ts +136 -0
  225. package/src/utils/validation.ts +186 -0
  226. package/tests/docker/Dockerfile.node +23 -0
  227. package/tests/docker/Dockerfile.runner +18 -0
  228. package/tests/docker/docker-compose.test.yml +73 -0
  229. package/tests/integration/message-passing.test.ts +109 -0
  230. package/tests/integration/multi-node.test.ts +92 -0
  231. package/tests/integration/p2p-connection.test.ts +83 -0
  232. package/tests/integration/test-config.ts +32 -0
  233. package/tsconfig.json +21 -0
  234. package/vitest.config.ts +26 -0
@@ -0,0 +1,449 @@
1
+ /**
2
+ * AnnouncementQueue 测试
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
+ import { AnnouncementQueue } from './announcement-queue.js';
7
+ import type { TaskAnnouncement, TaskClaim } from './types.js';
8
+
9
+ describe('AnnouncementQueue', () => {
10
+ let queue: AnnouncementQueue;
11
+
12
+ beforeEach(() => {
13
+ queue = new AnnouncementQueue({ maxSize: 10, maxAgeMs: 1000 });
14
+ });
15
+
16
+ afterEach(() => {
17
+ queue.clear();
18
+ });
19
+
20
+ describe('create()', () => {
21
+ it('应该创建任务广播', () => {
22
+ const announcement = queue.create({
23
+ taskType: 'test',
24
+ description: 'Test task',
25
+ timeout: 5000,
26
+ from: 'peer-1'
27
+ });
28
+
29
+ expect(announcement.announcementId).toMatch(/^ann-/);
30
+ expect(announcement.taskType).toBe('test');
31
+ expect(announcement.description).toBe('Test task');
32
+ expect(announcement.status).toBe('open');
33
+ expect(announcement.from).toBe('peer-1');
34
+ expect(announcement.claims).toEqual([]);
35
+ expect(announcement.timestamp).toBeGreaterThan(0);
36
+ });
37
+
38
+ it('应该在队列满时抛出错误', () => {
39
+ const smallQueue = new AnnouncementQueue({ maxSize: 2 });
40
+
41
+ smallQueue.create({ taskType: 'test', description: 'task 1', timeout: 5000, from: 'peer-1' });
42
+ smallQueue.create({ taskType: 'test', description: 'task 2', timeout: 5000, from: 'peer-1' });
43
+
44
+ expect(() => smallQueue.create({ taskType: 'test', description: 'task 3', timeout: 5000, from: 'peer-1' }))
45
+ .toThrow('Announcement queue is full');
46
+ });
47
+
48
+ it('应该为每个广播生成唯一 ID', () => {
49
+ const a1 = queue.create({ taskType: 'test', description: 'task 1', timeout: 5000, from: 'peer-1' });
50
+ const a2 = queue.create({ taskType: 'test', description: 'task 2', timeout: 5000, from: 'peer-1' });
51
+
52
+ expect(a1.announcementId).not.toBe(a2.announcementId);
53
+ });
54
+ });
55
+
56
+ describe('get() 和 getOpen()', () => {
57
+ it('应该获取特定广播', () => {
58
+ const created = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
59
+ const fetched = queue.get(created.announcementId);
60
+
61
+ expect(fetched).toBeDefined();
62
+ expect(fetched?.announcementId).toBe(created.announcementId);
63
+ });
64
+
65
+ it('应该对不存在的广播返回 undefined', () => {
66
+ expect(queue.get('non-existent')).toBeUndefined();
67
+ });
68
+
69
+ it('应该获取所有开放的广播', () => {
70
+ queue.create({ taskType: 'test', description: 'task 1', timeout: 5000, from: 'peer-1' });
71
+ queue.create({ taskType: 'test', description: 'task 2', timeout: 5000, from: 'peer-1' });
72
+
73
+ const open = queue.getOpen();
74
+ expect(open).toHaveLength(2);
75
+ expect(open.every(a => a.status === 'open')).toBe(true);
76
+ });
77
+
78
+ it('应该按时间排序返回开放广播', async () => {
79
+ queue.create({ taskType: 'test', description: 'first', timeout: 5000, from: 'peer-1' });
80
+ await new Promise(r => setTimeout(r, 10));
81
+ queue.create({ taskType: 'test', description: 'second', timeout: 5000, from: 'peer-1' });
82
+
83
+ const open = queue.getOpen();
84
+ expect(open[0].description).toBe('first');
85
+ expect(open[1].description).toBe('second');
86
+ });
87
+ });
88
+
89
+ describe('submitClaim()', () => {
90
+ it('应该提交认领', () => {
91
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
92
+ const claim = queue.submitClaim(announcement.announcementId, {
93
+ claimant: 'peer-2',
94
+ claimantName: 'Worker 2',
95
+ confidence: 0.9
96
+ });
97
+
98
+ expect(claim).toBeDefined();
99
+ expect(claim?.claimId).toMatch(/^claim-/);
100
+ expect(claim?.claimant).toBe('peer-2');
101
+ expect(claim?.status).toBe('pending');
102
+ expect(claim?.announcementId).toBe(announcement.announcementId);
103
+ });
104
+
105
+ it('应该对不存在的广播返回 null', () => {
106
+ const claim = queue.submitClaim('non-existent', {
107
+ claimant: 'peer-2'
108
+ });
109
+ expect(claim).toBeNull();
110
+ });
111
+
112
+ it('应该对非开放状态的广播返回 null', () => {
113
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
114
+ queue.markDelegated(announcement.announcementId);
115
+
116
+ const claim = queue.submitClaim(announcement.announcementId, {
117
+ claimant: 'peer-2'
118
+ });
119
+ expect(claim).toBeNull();
120
+ });
121
+
122
+ it('应该防止重复认领', () => {
123
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
124
+
125
+ const claim1 = queue.submitClaim(announcement.announcementId, {
126
+ claimant: 'peer-2',
127
+ confidence: 0.8
128
+ });
129
+
130
+ const claim2 = queue.submitClaim(announcement.announcementId, {
131
+ claimant: 'peer-2',
132
+ confidence: 0.9 // 不同的 confidence
133
+ });
134
+
135
+ // 应该返回同一个认领
136
+ expect(claim1?.claimId).toBe(claim2?.claimId);
137
+ expect(announcement.claims).toHaveLength(1);
138
+ });
139
+
140
+ it('应该允许不同用户认领同一广播', () => {
141
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
142
+
143
+ const claim1 = queue.submitClaim(announcement.announcementId, { claimant: 'peer-2' });
144
+ const claim2 = queue.submitClaim(announcement.announcementId, { claimant: 'peer-3' });
145
+
146
+ expect(claim1?.claimId).not.toBe(claim2?.claimId);
147
+ expect(announcement.claims).toHaveLength(2);
148
+ });
149
+ });
150
+
151
+ describe('acceptClaim() 和 rejectClaim()', () => {
152
+ it('应该接受认领', () => {
153
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
154
+ const claim = queue.submitClaim(announcement.announcementId, { claimant: 'peer-2' });
155
+
156
+ const accepted = queue.acceptClaim(announcement.announcementId, claim!.claimId);
157
+
158
+ expect(accepted?.status).toBe('accepted');
159
+ expect(announcement.status).toBe('claimed');
160
+ });
161
+
162
+ it('接受认领时应该拒绝其他认领', () => {
163
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
164
+ const claim1 = queue.submitClaim(announcement.announcementId, { claimant: 'peer-2' });
165
+ const claim2 = queue.submitClaim(announcement.announcementId, { claimant: 'peer-3' });
166
+
167
+ queue.acceptClaim(announcement.announcementId, claim1!.claimId);
168
+
169
+ expect(claim1?.status).toBe('accepted');
170
+ expect(claim2?.status).toBe('rejected');
171
+ });
172
+
173
+ it('应该拒绝认领', () => {
174
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
175
+ const claim = queue.submitClaim(announcement.announcementId, { claimant: 'peer-2' });
176
+
177
+ const rejected = queue.rejectClaim(announcement.announcementId, claim!.claimId);
178
+
179
+ expect(rejected?.status).toBe('rejected');
180
+ expect(announcement.status).toBe('open'); // 广播仍然开放
181
+ });
182
+
183
+ it('应该对不存在的广播返回 null', () => {
184
+ const result = queue.acceptClaim('non-existent', 'claim-1');
185
+ expect(result).toBeNull();
186
+ });
187
+
188
+ it('应该对不存在的认领返回 null', () => {
189
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
190
+ const result = queue.acceptClaim(announcement.announcementId, 'non-existent-claim');
191
+ expect(result).toBeNull();
192
+ });
193
+ });
194
+
195
+ describe('getMyClaims() 和 getMyAnnouncements()', () => {
196
+ it('应该获取我的认领', () => {
197
+ const a1 = queue.create({ taskType: 'test', description: 'Task 1', timeout: 5000, from: 'peer-1' });
198
+ const a2 = queue.create({ taskType: 'test', description: 'Task 2', timeout: 5000, from: 'peer-1' });
199
+
200
+ queue.submitClaim(a1.announcementId, { claimant: 'peer-2' });
201
+ queue.submitClaim(a2.announcementId, { claimant: 'peer-2' });
202
+ queue.submitClaim(a1.announcementId, { claimant: 'peer-3' });
203
+
204
+ const myClaims = queue.getMyClaims('peer-2');
205
+ expect(myClaims).toHaveLength(2);
206
+ expect(myClaims.every(c => c.claimant === 'peer-2')).toBe(true);
207
+ });
208
+
209
+ it('应该获取我的广播', () => {
210
+ queue.create({ taskType: 'test', description: 'Task 1', timeout: 5000, from: 'peer-1' });
211
+ queue.create({ taskType: 'test', description: 'Task 2', timeout: 5000, from: 'peer-1' });
212
+ queue.create({ taskType: 'test', description: 'Task 3', timeout: 5000, from: 'peer-2' });
213
+
214
+ const myAnnouncements = queue.getMyAnnouncements('peer-1');
215
+ expect(myAnnouncements).toHaveLength(2);
216
+ expect(myAnnouncements.every(a => a.from === 'peer-1')).toBe(true);
217
+ });
218
+
219
+ it('应该按时间倒序返回', async () => {
220
+ queue.create({ taskType: 'test', description: 'first', timeout: 5000, from: 'peer-1' });
221
+ await new Promise(r => setTimeout(r, 10));
222
+ queue.create({ taskType: 'test', description: 'second', timeout: 5000, from: 'peer-1' });
223
+
224
+ const myAnnouncements = queue.getMyAnnouncements('peer-1');
225
+ expect(myAnnouncements[0].description).toBe('second');
226
+ expect(myAnnouncements[1].description).toBe('first');
227
+ });
228
+ });
229
+
230
+ describe('markDelegated()', () => {
231
+ it('应该标记为已委托', () => {
232
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
233
+ const result = queue.markDelegated(announcement.announcementId);
234
+
235
+ expect(result).toBe(true);
236
+ expect(announcement.status).toBe('delegated');
237
+ });
238
+
239
+ it('应该对不存在的广播返回 false', () => {
240
+ expect(queue.markDelegated('non-existent')).toBe(false);
241
+ });
242
+ });
243
+
244
+ describe('getStats()', () => {
245
+ it('应该返回正确的统计', () => {
246
+ const a1 = queue.create({ taskType: 'test', description: 'Task 1', timeout: 5000, from: 'peer-1' });
247
+ const a2 = queue.create({ taskType: 'test', description: 'Task 2', timeout: 5000, from: 'peer-1' });
248
+
249
+ const claim = queue.submitClaim(a1.announcementId, { claimant: 'peer-2' });
250
+ queue.acceptClaim(a1.announcementId, claim!.claimId);
251
+ queue.markDelegated(a2.announcementId);
252
+
253
+ const stats = queue.getStats();
254
+ expect(stats.open).toBe(0);
255
+ expect(stats.claimed).toBe(1);
256
+ expect(stats.delegated).toBe(1);
257
+ expect(stats.total).toBe(2);
258
+ });
259
+ });
260
+
261
+ describe('cleanup()', () => {
262
+ it('应该将过期广播标记为 expired', async () => {
263
+ const fastExpireQueue = new AnnouncementQueue({ maxSize: 10, maxAgeMs: 50 });
264
+ fastExpireQueue.create({ taskType: 'test', description: 'Task', timeout: 5000, from: 'peer-1' });
265
+
266
+ await new Promise(r => setTimeout(r, 60));
267
+
268
+ // 通过创建新任务触发清理
269
+ fastExpireQueue.create({ taskType: 'test', description: 'New task', timeout: 5000, from: 'peer-1' });
270
+
271
+ const stats = fastExpireQueue.getStats();
272
+ expect(stats.expired).toBe(1);
273
+ });
274
+
275
+ it('应该删除过期很久的广播', async () => {
276
+ const fastExpireQueue = new AnnouncementQueue({ maxSize: 10, maxAgeMs: 50 });
277
+ fastExpireQueue.create({ taskType: 'test', description: 'Task', timeout: 5000, from: 'peer-1' });
278
+
279
+ await new Promise(r => setTimeout(r, 120));
280
+
281
+ // 触发清理
282
+ fastExpireQueue.create({ taskType: 'test', description: 'New task', timeout: 5000, from: 'peer-1' });
283
+
284
+ const stats = fastExpireQueue.getStats();
285
+ expect(stats.total).toBe(1); // 只有新任务
286
+ });
287
+ });
288
+
289
+ describe('clear()', () => {
290
+ it('应该清空队列', () => {
291
+ queue.create({ taskType: 'test', description: 'Task 1', timeout: 5000, from: 'peer-1' });
292
+ queue.create({ taskType: 'test', description: 'Task 2', timeout: 5000, from: 'peer-1' });
293
+
294
+ queue.clear();
295
+
296
+ expect(queue.getStats().total).toBe(0);
297
+ expect(queue.getOpen()).toEqual([]);
298
+ });
299
+ });
300
+
301
+ describe('边界条件', () => {
302
+ it('应该处理空输入', () => {
303
+ const announcement = queue.create({
304
+ taskType: '',
305
+ description: '',
306
+ timeout: 0,
307
+ from: ''
308
+ });
309
+
310
+ expect(announcement).toBeDefined();
311
+ expect(announcement.taskType).toBe('');
312
+ });
313
+
314
+ it('应该处理特殊字符', () => {
315
+ const announcement = queue.create({
316
+ taskType: 'test-特殊字符-🎮',
317
+ description: 'Test with special chars: <>&"\'',
318
+ timeout: 5000,
319
+ from: 'peer-1'
320
+ });
321
+
322
+ expect(announcement.taskType).toBe('test-特殊字符-🎮');
323
+ expect(announcement.description).toBe('Test with special chars: <>&"\'');
324
+ });
325
+ });
326
+
327
+ // P1 修复:新增边界和并发测试
328
+ describe('并发和边界情况', () => {
329
+ it('应该正确处理 acceptClaim 的并发调用', async () => {
330
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
331
+ const claim1 = queue.submitClaim(announcement.announcementId, { claimant: 'peer-2' });
332
+ const claim2 = queue.submitClaim(announcement.announcementId, { claimant: 'peer-3' });
333
+
334
+ // 模拟并发调用(虽然 JS 是单线程,但可以测试锁逻辑)
335
+ const results = await Promise.all([
336
+ Promise.resolve(queue.acceptClaim(announcement.announcementId, claim1!.claimId)),
337
+ Promise.resolve(queue.acceptClaim(announcement.announcementId, claim2!.claimId))
338
+ ]);
339
+
340
+ // 只有一个应该成功
341
+ const successCount = results.filter(r => r !== null).length;
342
+ expect(successCount).toBeLessThanOrEqual(1);
343
+ });
344
+
345
+ it('应该处理已认领广播的再次认领尝试', () => {
346
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
347
+ const claim = queue.submitClaim(announcement.announcementId, { claimant: 'peer-2' });
348
+ queue.acceptClaim(announcement.announcementId, claim!.claimId);
349
+
350
+ // 广播已认领,新认领应该失败
351
+ const newClaim = queue.submitClaim(announcement.announcementId, { claimant: 'peer-3' });
352
+ expect(newClaim).toBeNull();
353
+ });
354
+
355
+ it('应该处理同一认领的重复接受', () => {
356
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
357
+ const claim = queue.submitClaim(announcement.announcementId, { claimant: 'peer-2' });
358
+
359
+ // 第一次接受
360
+ const result1 = queue.acceptClaim(announcement.announcementId, claim!.claimId);
361
+ expect(result1?.status).toBe('accepted');
362
+
363
+ // 第二次接受应该失败(广播已不是 open)
364
+ const result2 = queue.acceptClaim(announcement.announcementId, claim!.claimId);
365
+ expect(result2).toBeNull();
366
+ });
367
+
368
+ it('应该正确处理大量认领', () => {
369
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
370
+
371
+ // 添加 100 个认领
372
+ for (let i = 0; i < 100; i++) {
373
+ queue.submitClaim(announcement.announcementId, { claimant: `peer-${i}` });
374
+ }
375
+
376
+ expect(announcement.claims).toHaveLength(100);
377
+
378
+ // 接受其中一个
379
+ const claim = announcement.claims![50];
380
+ const result = queue.acceptClaim(announcement.announcementId, claim.claimId);
381
+
382
+ expect(result?.status).toBe('accepted');
383
+ // 其他认领应该被拒绝
384
+ const rejectedCount = announcement.claims!.filter(c => c.status === 'rejected').length;
385
+ expect(rejectedCount).toBe(99);
386
+ });
387
+ });
388
+
389
+ describe('forceClearOrphanLocks', () => {
390
+ it('应该清除孤立锁', () => {
391
+ // 直接创建一个有锁的场景
392
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
393
+ const claim = queue.submitClaim(announcement.announcementId, { claimant: 'peer-2' });
394
+
395
+ // 模拟锁未释放的情况(直接操作内部状态)
396
+ // 注意:这是一个测试内部实现的测试
397
+ expect(queue.hasOrphanLocks()).toBe(false);
398
+
399
+ // 清除应该返回 0
400
+ expect(queue.forceClearOrphanLocks()).toBe(0);
401
+ });
402
+
403
+ it('clear 应该同时清除锁', () => {
404
+ queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
405
+ queue.clear();
406
+
407
+ // 队列应该为空,锁也应该为空
408
+ expect(queue.getStats().total).toBe(0);
409
+ expect(queue.hasOrphanLocks()).toBe(false);
410
+ });
411
+ });
412
+
413
+ describe('事件', () => {
414
+ it('应该在创建时发出 announcement:created 事件', () => {
415
+ const handler = vi.fn();
416
+ queue.on('announcement:created', handler);
417
+
418
+ queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
419
+
420
+ expect(handler).toHaveBeenCalledTimes(1);
421
+ expect(handler.mock.calls[0][0].taskType).toBe('test');
422
+ });
423
+
424
+ it('应该在过期时发出 announcement:expired 事件', async () => {
425
+ const handler = vi.fn();
426
+ const fastExpireQueue = new AnnouncementQueue({ maxSize: 10, maxAgeMs: 50 });
427
+ fastExpireQueue.on('announcement:expired', handler);
428
+
429
+ fastExpireQueue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
430
+
431
+ await new Promise(r => setTimeout(r, 60));
432
+ fastExpireQueue.create({ taskType: 'test', description: 'New', timeout: 5000, from: 'peer-1' });
433
+
434
+ expect(handler).toHaveBeenCalledTimes(1);
435
+ expect(handler.mock.calls[0][0].reason).toBe('timeout');
436
+ });
437
+
438
+ it('应该在认领时发出 announcement:claimed 事件', () => {
439
+ const handler = vi.fn();
440
+ queue.on('announcement:claimed', handler);
441
+
442
+ const announcement = queue.create({ taskType: 'test', description: 'Test', timeout: 5000, from: 'peer-1' });
443
+ const claim = queue.submitClaim(announcement.announcementId, { claimant: 'peer-2' });
444
+ queue.acceptClaim(announcement.announcementId, claim!.claimId);
445
+
446
+ expect(handler).toHaveBeenCalledTimes(1);
447
+ });
448
+ });
449
+ });