@f2a/network 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/README.md +278 -63
  2. package/dist/cli/commands.d.ts.map +1 -1
  3. package/dist/cli/commands.js +29 -2
  4. package/dist/cli/commands.js.map +1 -1
  5. package/dist/cli/config.d.ts +176 -0
  6. package/dist/cli/config.d.ts.map +1 -0
  7. package/dist/cli/config.js +386 -0
  8. package/dist/cli/config.js.map +1 -0
  9. package/dist/cli/daemon.d.ts +54 -0
  10. package/dist/cli/daemon.d.ts.map +1 -0
  11. package/dist/cli/daemon.js +572 -0
  12. package/dist/cli/daemon.js.map +1 -0
  13. package/dist/cli/index.js +90 -16
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/init.d.ts +13 -0
  16. package/dist/cli/init.d.ts.map +1 -0
  17. package/dist/cli/init.js +352 -0
  18. package/dist/cli/init.js.map +1 -0
  19. package/dist/core/e2ee-crypto.d.ts +127 -1
  20. package/dist/core/e2ee-crypto.d.ts.map +1 -1
  21. package/dist/core/e2ee-crypto.js +446 -12
  22. package/dist/core/e2ee-crypto.js.map +1 -1
  23. package/dist/core/f2a.d.ts +2 -1
  24. package/dist/core/f2a.d.ts.map +1 -1
  25. package/dist/core/f2a.js +6 -2
  26. package/dist/core/f2a.js.map +1 -1
  27. package/dist/core/identity/encrypted-key-store.d.ts +19 -0
  28. package/dist/core/identity/encrypted-key-store.d.ts.map +1 -0
  29. package/dist/core/identity/encrypted-key-store.js +72 -0
  30. package/dist/core/identity/encrypted-key-store.js.map +1 -0
  31. package/dist/core/identity/identity-manager.d.ts +133 -0
  32. package/dist/core/identity/identity-manager.d.ts.map +1 -0
  33. package/dist/core/identity/identity-manager.js +454 -0
  34. package/dist/core/identity/identity-manager.js.map +1 -0
  35. package/dist/core/identity/index.d.ts +8 -0
  36. package/dist/core/identity/index.d.ts.map +1 -0
  37. package/dist/core/identity/index.js +7 -0
  38. package/dist/core/identity/index.js.map +1 -0
  39. package/dist/core/identity/types.d.ts +70 -0
  40. package/dist/core/identity/types.d.ts.map +1 -0
  41. package/dist/core/identity/types.js +17 -0
  42. package/dist/core/identity/types.js.map +1 -0
  43. package/dist/core/p2p-network.d.ts +26 -0
  44. package/dist/core/p2p-network.d.ts.map +1 -1
  45. package/dist/core/p2p-network.js +434 -105
  46. package/dist/core/p2p-network.js.map +1 -1
  47. package/dist/core/reputation-security.d.ts +15 -0
  48. package/dist/core/reputation-security.d.ts.map +1 -1
  49. package/dist/core/reputation-security.js +73 -3
  50. package/dist/core/reputation-security.js.map +1 -1
  51. package/dist/core/reputation.d.ts +129 -4
  52. package/dist/core/reputation.d.ts.map +1 -1
  53. package/dist/core/reputation.js +294 -1
  54. package/dist/core/reputation.js.map +1 -1
  55. package/dist/core/review-committee.d.ts +2 -2
  56. package/dist/core/review-committee.d.ts.map +1 -1
  57. package/dist/core/review-committee.js +17 -0
  58. package/dist/core/review-committee.js.map +1 -1
  59. package/dist/daemon/control-server.d.ts.map +1 -1
  60. package/dist/daemon/control-server.js +44 -1
  61. package/dist/daemon/control-server.js.map +1 -1
  62. package/dist/daemon/webhook.d.ts +3 -0
  63. package/dist/daemon/webhook.d.ts.map +1 -1
  64. package/dist/daemon/webhook.js +318 -6
  65. package/dist/daemon/webhook.js.map +1 -1
  66. package/dist/index.d.ts +3 -3
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +7 -3
  69. package/dist/index.js.map +1 -1
  70. package/dist/types/index.d.ts +4 -0
  71. package/dist/types/index.d.ts.map +1 -1
  72. package/dist/types/index.js.map +1 -1
  73. package/dist/types/result.d.ts +1 -1
  74. package/dist/types/result.d.ts.map +1 -1
  75. package/dist/types/result.js.map +1 -1
  76. package/dist/utils/crypto-utils.d.ts +17 -0
  77. package/dist/utils/crypto-utils.d.ts.map +1 -0
  78. package/dist/utils/crypto-utils.js +28 -0
  79. package/dist/utils/crypto-utils.js.map +1 -0
  80. package/dist/utils/logger.d.ts +1 -0
  81. package/dist/utils/logger.d.ts.map +1 -1
  82. package/dist/utils/logger.js +9 -3
  83. package/dist/utils/logger.js.map +1 -1
  84. package/dist/utils/rate-limiter.d.ts.map +1 -1
  85. package/dist/utils/rate-limiter.js +3 -1
  86. package/dist/utils/rate-limiter.js.map +1 -1
  87. package/dist/utils/signature.d.ts +47 -1
  88. package/dist/utils/signature.d.ts.map +1 -1
  89. package/dist/utils/signature.js +166 -11
  90. package/dist/utils/signature.js.map +1 -1
  91. package/package.json +9 -1
  92. package/.github/workflows/ci.yml +0 -113
  93. package/.github/workflows/publish.yml +0 -60
  94. package/MONOREPO.md +0 -58
  95. package/SKILL.md +0 -137
  96. package/dist/adapters/openclaw.d.ts +0 -103
  97. package/dist/adapters/openclaw.d.ts.map +0 -1
  98. package/dist/adapters/openclaw.js +0 -297
  99. package/dist/adapters/openclaw.js.map +0 -1
  100. package/dist/core/connection-manager.d.ts +0 -80
  101. package/dist/core/connection-manager.d.ts.map +0 -1
  102. package/dist/core/connection-manager.js +0 -235
  103. package/dist/core/connection-manager.js.map +0 -1
  104. package/dist/core/connection-manager.test.d.ts +0 -2
  105. package/dist/core/connection-manager.test.d.ts.map +0 -1
  106. package/dist/core/connection-manager.test.js +0 -52
  107. package/dist/core/connection-manager.test.js.map +0 -1
  108. package/dist/core/identity.d.ts +0 -47
  109. package/dist/core/identity.d.ts.map +0 -1
  110. package/dist/core/identity.js +0 -130
  111. package/dist/core/identity.js.map +0 -1
  112. package/dist/core/identity.test.d.ts +0 -2
  113. package/dist/core/identity.test.d.ts.map +0 -1
  114. package/dist/core/identity.test.js +0 -43
  115. package/dist/core/identity.test.js.map +0 -1
  116. package/dist/core/serverless.d.ts +0 -155
  117. package/dist/core/serverless.d.ts.map +0 -1
  118. package/dist/core/serverless.js +0 -615
  119. package/dist/core/serverless.js.map +0 -1
  120. package/dist/daemon/webhook.test.d.ts +0 -2
  121. package/dist/daemon/webhook.test.d.ts.map +0 -1
  122. package/dist/daemon/webhook.test.js +0 -24
  123. package/dist/daemon/webhook.test.js.map +0 -1
  124. package/dist/protocol/messages.d.ts +0 -739
  125. package/dist/protocol/messages.d.ts.map +0 -1
  126. package/dist/protocol/messages.js +0 -188
  127. package/dist/protocol/messages.js.map +0 -1
  128. package/dist/protocol/messages.test.d.ts +0 -2
  129. package/dist/protocol/messages.test.d.ts.map +0 -1
  130. package/dist/protocol/messages.test.js +0 -55
  131. package/dist/protocol/messages.test.js.map +0 -1
  132. package/docs/F2A-PROTOCOL.md +0 -61
  133. package/docs/MOBILE_BOOTSTRAP_DESIGN.md +0 -126
  134. package/docs/a2a-lessons.md +0 -316
  135. package/docs/middleware-guide.md +0 -448
  136. package/docs/readme-update-checklist.md +0 -90
  137. package/docs/reputation-guide.md +0 -396
  138. package/docs/rfcs/001-reputation-system.md +0 -712
  139. package/docs/security-design.md +0 -247
  140. package/install.sh +0 -231
  141. package/packages/openclaw-adapter/README.md +0 -510
  142. package/packages/openclaw-adapter/openclaw.plugin.json +0 -106
  143. package/packages/openclaw-adapter/package.json +0 -40
  144. package/packages/openclaw-adapter/src/announcement-queue.test.ts +0 -449
  145. package/packages/openclaw-adapter/src/announcement-queue.ts +0 -403
  146. package/packages/openclaw-adapter/src/capability-detector.test.ts +0 -99
  147. package/packages/openclaw-adapter/src/capability-detector.ts +0 -183
  148. package/packages/openclaw-adapter/src/claim-handlers.test.ts +0 -974
  149. package/packages/openclaw-adapter/src/claim-handlers.ts +0 -482
  150. package/packages/openclaw-adapter/src/connector.business.test.ts +0 -583
  151. package/packages/openclaw-adapter/src/connector.ts +0 -795
  152. package/packages/openclaw-adapter/src/index.test.ts +0 -82
  153. package/packages/openclaw-adapter/src/index.ts +0 -18
  154. package/packages/openclaw-adapter/src/integration.e2e.test.ts +0 -829
  155. package/packages/openclaw-adapter/src/logger.ts +0 -51
  156. package/packages/openclaw-adapter/src/network-client.test.ts +0 -266
  157. package/packages/openclaw-adapter/src/network-client.ts +0 -251
  158. package/packages/openclaw-adapter/src/network-recovery.test.ts +0 -465
  159. package/packages/openclaw-adapter/src/node-manager.test.ts +0 -136
  160. package/packages/openclaw-adapter/src/node-manager.ts +0 -429
  161. package/packages/openclaw-adapter/src/plugin.test.ts +0 -439
  162. package/packages/openclaw-adapter/src/plugin.ts +0 -104
  163. package/packages/openclaw-adapter/src/reputation.test.ts +0 -221
  164. package/packages/openclaw-adapter/src/reputation.ts +0 -368
  165. package/packages/openclaw-adapter/src/task-guard.test.ts +0 -502
  166. package/packages/openclaw-adapter/src/task-guard.ts +0 -860
  167. package/packages/openclaw-adapter/src/task-queue.concurrency.test.ts +0 -462
  168. package/packages/openclaw-adapter/src/task-queue.edge-cases.test.ts +0 -284
  169. package/packages/openclaw-adapter/src/task-queue.persistence.test.ts +0 -408
  170. package/packages/openclaw-adapter/src/task-queue.ts +0 -668
  171. package/packages/openclaw-adapter/src/tool-handlers.test.ts +0 -906
  172. package/packages/openclaw-adapter/src/tool-handlers.ts +0 -574
  173. package/packages/openclaw-adapter/src/types.ts +0 -361
  174. package/packages/openclaw-adapter/src/webhook-pusher.test.ts +0 -188
  175. package/packages/openclaw-adapter/src/webhook-pusher.ts +0 -220
  176. package/packages/openclaw-adapter/src/webhook-server.test.ts +0 -580
  177. package/packages/openclaw-adapter/src/webhook-server.ts +0 -202
  178. package/packages/openclaw-adapter/tsconfig.json +0 -20
  179. package/src/cli/commands.test.ts +0 -157
  180. package/src/cli/commands.ts +0 -129
  181. package/src/cli/index.test.ts +0 -77
  182. package/src/cli/index.ts +0 -234
  183. package/src/core/autonomous-economy.test.ts +0 -291
  184. package/src/core/autonomous-economy.ts +0 -428
  185. package/src/core/e2ee-crypto.test.ts +0 -125
  186. package/src/core/e2ee-crypto.ts +0 -246
  187. package/src/core/f2a.test.ts +0 -269
  188. package/src/core/f2a.ts +0 -618
  189. package/src/core/p2p-network.test.ts +0 -199
  190. package/src/core/p2p-network.ts +0 -1432
  191. package/src/core/reputation-security.test.ts +0 -403
  192. package/src/core/reputation-security.ts +0 -562
  193. package/src/core/reputation.test.ts +0 -260
  194. package/src/core/reputation.ts +0 -576
  195. package/src/core/review-committee.test.ts +0 -380
  196. package/src/core/review-committee.ts +0 -401
  197. package/src/core/token-manager.test.ts +0 -133
  198. package/src/core/token-manager.ts +0 -140
  199. package/src/daemon/control-server.test.ts +0 -216
  200. package/src/daemon/control-server.ts +0 -292
  201. package/src/daemon/index.test.ts +0 -85
  202. package/src/daemon/index.ts +0 -89
  203. package/src/daemon/main.ts +0 -44
  204. package/src/daemon/start.ts +0 -29
  205. package/src/daemon/webhook.test.ts +0 -68
  206. package/src/daemon/webhook.ts +0 -105
  207. package/src/index.test.ts +0 -436
  208. package/src/index.ts +0 -72
  209. package/src/types/index.test.ts +0 -87
  210. package/src/types/index.ts +0 -341
  211. package/src/types/result.ts +0 -68
  212. package/src/utils/benchmark.ts +0 -237
  213. package/src/utils/logger.ts +0 -331
  214. package/src/utils/middleware.ts +0 -229
  215. package/src/utils/rate-limiter.ts +0 -207
  216. package/src/utils/signature.ts +0 -136
  217. package/src/utils/validation.ts +0 -186
  218. package/tests/docker/Dockerfile.node +0 -23
  219. package/tests/docker/Dockerfile.runner +0 -18
  220. package/tests/docker/docker-compose.test.yml +0 -73
  221. package/tests/integration/message-passing.test.ts +0 -109
  222. package/tests/integration/multi-node.test.ts +0 -92
  223. package/tests/integration/p2p-connection.test.ts +0 -83
  224. package/tests/integration/test-config.ts +0 -32
  225. package/tsconfig.json +0 -21
  226. package/vitest.config.ts +0 -26
@@ -1,284 +0,0 @@
1
- /**
2
- * TaskQueue 边界、竞态和幂等性测试
3
- */
4
-
5
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
- import { TaskQueue } from './task-queue.js';
7
- import fs from 'fs';
8
- import path from 'path';
9
-
10
- const TEST_DIR = './test-tmp';
11
-
12
- describe('TaskQueue 边界问题', () => {
13
- let queue: TaskQueue;
14
-
15
- beforeEach(() => {
16
- if (!fs.existsSync(TEST_DIR)) {
17
- fs.mkdirSync(TEST_DIR, { recursive: true });
18
- }
19
- queue = new TaskQueue({
20
- maxSize: 5,
21
- maxAgeMs: 1000,
22
- persistDir: TEST_DIR,
23
- persistEnabled: true
24
- });
25
- });
26
-
27
- afterEach(() => {
28
- queue.close();
29
- // 清理测试目录
30
- if (fs.existsSync(TEST_DIR)) {
31
- fs.rmSync(TEST_DIR, { recursive: true });
32
- }
33
- });
34
-
35
- describe('队列边界', () => {
36
- it('应该在队列满时抛出错误', () => {
37
- // 添加 5 个任务(达到上限)
38
- for (let i = 0; i < 5; i++) {
39
- queue.add({ taskId: `task-${i}`, taskType: 'test' });
40
- }
41
-
42
- // 第 6 个应该失败
43
- expect(() => queue.add({ taskId: 'task-6', taskType: 'test' }))
44
- .toThrow('Task queue is full');
45
- });
46
-
47
- it('应该在队列空时返回空数组', () => {
48
- expect(queue.getPending()).toEqual([]);
49
- expect(queue.getStats().pending).toBe(0);
50
- });
51
-
52
- it('应该正确处理 null/undefined 参数', () => {
53
- // @ts-expect-error 测试边界
54
- expect(() => queue.add(null)).toThrow();
55
- // @ts-expect-error 测试边界
56
- expect(() => queue.add(undefined)).toThrow();
57
- });
58
- });
59
-
60
- describe('任务过期清理', () => {
61
- it('应该清理过期任务', async () => {
62
- queue.add({ taskId: 'task-1', taskType: 'test' });
63
- expect(queue.getStats().pending).toBe(1);
64
-
65
- // 等待过期
66
- await new Promise(r => setTimeout(r, 1100));
67
-
68
- // 触发清理(通过添加新任务)
69
- queue.add({ taskId: 'task-2', taskType: 'test' });
70
-
71
- // 旧任务应该被清理
72
- expect(queue.get('task-1')).toBeUndefined();
73
- });
74
- });
75
-
76
- describe('幂等性', () => {
77
- it('应该覆盖相同 taskId 的任务', () => {
78
- queue.add({ taskId: 'task-1', taskType: 'test', description: 'first' });
79
- queue.add({ taskId: 'task-1', taskType: 'test', description: 'second' });
80
-
81
- const task = queue.get('task-1');
82
- expect(task?.description).toBe('second');
83
- expect(queue.getStats().total).toBe(1); // 只有一个任务
84
- });
85
-
86
- it('应该正确处理重复的 markProcessing', () => {
87
- queue.add({ taskId: 'task-1', taskType: 'test' });
88
-
89
- const first = queue.markProcessing('task-1');
90
- const second = queue.markProcessing('task-1');
91
-
92
- expect(first?.status).toBe('processing');
93
- expect(second?.status).toBe('processing');
94
- expect(queue.getStats().processing).toBe(1);
95
- });
96
-
97
- it('应该正确处理重复的 complete', () => {
98
- queue.add({ taskId: 'task-1', taskType: 'test' });
99
-
100
- queue.complete('task-1', { status: 'success', result: 'first' });
101
- queue.complete('task-1', { status: 'success', result: 'second' });
102
-
103
- const task = queue.get('task-1');
104
- expect(task?.result).toBe('second');
105
- });
106
- });
107
-
108
- describe('SQLite 持久化', () => {
109
- it('应该在关闭后保留任务', () => {
110
- queue.add({ taskId: 'task-1', taskType: 'test' });
111
- queue.add({ taskId: 'task-2', taskType: 'test' });
112
- queue.markProcessing('task-1');
113
-
114
- queue.close();
115
-
116
- // 重新打开
117
- const newQueue = new TaskQueue({
118
- maxSize: 5,
119
- persistDir: TEST_DIR,
120
- persistEnabled: true
121
- });
122
-
123
- // 恢复后 processing 任务被重置为 pending,避免僵尸任务
124
- const stats = newQueue.getStats();
125
- expect(stats.pending).toBe(2); // task-1 和 task-2
126
- expect(stats.processing).toBe(0);
127
-
128
- newQueue.close();
129
- });
130
- });
131
- });
132
-
133
- describe('并发竞态条件', () => {
134
- let queue: TaskQueue;
135
-
136
- beforeEach(() => {
137
- queue = new TaskQueue({ maxSize: 1000 });
138
- });
139
-
140
- afterEach(() => {
141
- queue.close();
142
- });
143
-
144
- it('应该正确处理批量添加', () => {
145
- // add() 是同步方法,不需要 Promise.resolve 包装
146
- for (let i = 0; i < 100; i++) {
147
- queue.add({ taskId: `task-${i}`, taskType: 'test' });
148
- }
149
-
150
- expect(queue.getStats().total).toBe(100);
151
- });
152
-
153
- it('应该正确处理批量 add 和 getPending', () => {
154
- // 同步操作,不需要 Promise 包装
155
- for (let i = 0; i < 50; i++) {
156
- queue.add({ taskId: `task-${i}`, taskType: 'test' });
157
- queue.getPending();
158
- }
159
-
160
- // 最终应该有 50 个任务
161
- expect(queue.getStats().total).toBe(50);
162
- });
163
-
164
- it('应该正确处理批量 complete', () => {
165
- for (let i = 0; i < 50; i++) {
166
- queue.add({ taskId: `task-${i}`, taskType: 'test' });
167
- }
168
-
169
- // complete() 是同步方法,不需要 Promise 包装
170
- for (let i = 0; i < 50; i++) {
171
- queue.complete(`task-${i}`, { status: 'success' });
172
- }
173
-
174
- expect(queue.getStats().completed).toBe(50);
175
- });
176
-
177
- it('应该正确处理 taskId 输入验证', () => {
178
- // 空字符串应该抛出错误
179
- expect(() => queue.add({ taskId: '', taskType: 'test' })).toThrow('taskId must be a non-empty string');
180
-
181
- // 只有空格的字符串应该抛出错误
182
- expect(() => queue.add({ taskId: ' ', taskType: 'test' })).toThrow('taskId must be a non-empty string');
183
-
184
- // 正常的 taskId 应该成功
185
- expect(() => queue.add({ taskId: 'valid-task', taskType: 'test' })).not.toThrow();
186
- });
187
-
188
- it('应该保留重新添加任务的 createdAt 时间戳', () => {
189
- queue.add({ taskId: 'task-1', taskType: 'test', description: 'first' });
190
- const originalTask = queue.get('task-1');
191
- const originalCreatedAt = originalTask?.createdAt;
192
-
193
- // 等待一小段时间
194
- const start = Date.now();
195
- while (Date.now() - start < 10) { /* spin */ }
196
-
197
- // 重新添加相同 taskId 的任务
198
- queue.add({ taskId: 'task-1', taskType: 'test', description: 'second' });
199
- const updatedTask = queue.get('task-1');
200
-
201
- // createdAt 应该保持不变
202
- expect(updatedTask?.createdAt).toBe(originalCreatedAt);
203
- // 但 description 应该更新
204
- expect(updatedTask?.description).toBe('second');
205
- });
206
- });
207
-
208
- // P1 修复:测试僵尸任务重置功能
209
- describe('resetProcessingTask', () => {
210
- let queue: TaskQueue;
211
-
212
- beforeEach(() => {
213
- if (!fs.existsSync(TEST_DIR)) {
214
- fs.mkdirSync(TEST_DIR, { recursive: true });
215
- }
216
- queue = new TaskQueue({
217
- maxSize: 5,
218
- maxAgeMs: 1000,
219
- persistDir: TEST_DIR,
220
- persistEnabled: true
221
- });
222
- });
223
-
224
- afterEach(() => {
225
- queue.close();
226
- if (fs.existsSync(TEST_DIR)) {
227
- fs.rmSync(TEST_DIR, { recursive: true });
228
- }
229
- });
230
-
231
- it('应该将 processing 任务重置为 pending', () => {
232
- queue.add({ taskId: 'task-1', taskType: 'test', from: 'peer-1', timestamp: Date.now(), timeout: 30000 });
233
- queue.markProcessing('task-1');
234
-
235
- expect(queue.get('task-1')?.status).toBe('processing');
236
-
237
- const result = queue.resetProcessingTask('task-1');
238
-
239
- expect(result?.status).toBe('pending');
240
- expect(queue.get('task-1')?.status).toBe('pending');
241
- });
242
-
243
- it('应该只重置 processing 状态的任务', () => {
244
- queue.add({ taskId: 'task-1', taskType: 'test', from: 'peer-1', timestamp: Date.now(), timeout: 30000 });
245
- // 任务状态是 pending,不应该被重置
246
- const result = queue.resetProcessingTask('task-1');
247
- expect(result).toBeUndefined();
248
- expect(queue.get('task-1')?.status).toBe('pending');
249
- });
250
-
251
- it('应该对不存在的任务返回 undefined', () => {
252
- const result = queue.resetProcessingTask('non-existent');
253
- expect(result).toBeUndefined();
254
- });
255
-
256
- it('应该更新 updatedAt 时间戳', () => {
257
- queue.add({ taskId: 'task-1', taskType: 'test', from: 'peer-1', timestamp: Date.now(), timeout: 30000 });
258
- queue.markProcessing('task-1');
259
-
260
- const beforeReset = Date.now();
261
- const result = queue.resetProcessingTask('task-1');
262
- const afterReset = Date.now();
263
-
264
- expect(result?.updatedAt).toBeGreaterThanOrEqual(beforeReset);
265
- expect(result?.updatedAt).toBeLessThanOrEqual(afterReset);
266
- });
267
-
268
- it('应该在持久化后保持重置状态', () => {
269
- queue.add({ taskId: 'task-1', taskType: 'test', from: 'peer-1', timestamp: Date.now(), timeout: 30000 });
270
- queue.markProcessing('task-1');
271
- queue.resetProcessingTask('task-1');
272
- queue.close();
273
-
274
- // 创建新队列,应该恢复任务且状态为 pending
275
- const newQueue = new TaskQueue({
276
- maxSize: 5,
277
- maxAgeMs: 1000,
278
- persistDir: TEST_DIR,
279
- persistEnabled: true
280
- });
281
- expect(newQueue.get('task-1')?.status).toBe('pending');
282
- newQueue.close();
283
- });
284
- });
@@ -1,408 +0,0 @@
1
- /**
2
- * TaskQueue 崩溃恢复测试
3
- * 测试 SQLite 持久化和恢复
4
- */
5
-
6
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
- import { TaskQueue } from './task-queue.js';
8
- import fs from 'fs';
9
- import path from 'path';
10
- import Database from 'better-sqlite3';
11
- import { randomUUID } from 'crypto';
12
-
13
- describe('TaskQueue 崩溃恢复测试', () => {
14
- let queue: TaskQueue;
15
- let testDir: string;
16
-
17
- beforeEach(() => {
18
- // 为每个测试使用唯一的目录,确保测试隔离
19
- testDir = `./test-tmp-persistence-${randomUUID()}`;
20
- fs.mkdirSync(testDir, { recursive: true });
21
- queue = new TaskQueue({
22
- maxSize: 1000,
23
- maxAgeMs: 60000,
24
- persistDir: testDir,
25
- persistEnabled: true
26
- });
27
- });
28
-
29
- afterEach(() => {
30
- try {
31
- queue?.close();
32
- } catch (e) {
33
- // 忽略关闭错误
34
- }
35
- // 使用唯一目录,清理更可靠
36
- if (testDir && fs.existsSync(testDir)) {
37
- try {
38
- fs.rmSync(testDir, { recursive: true, force: true });
39
- } catch (e) {
40
- // 忽略删除错误(可能文件被锁定)
41
- }
42
- }
43
- });
44
-
45
- describe('基本持久化和恢复', () => {
46
- it('应该在关闭后保留 pending 任务', () => {
47
- queue.add({ taskId: 'pending-task-1', taskType: 'test', description: 'Pending task' });
48
- queue.add({ taskId: 'pending-task-2', taskType: 'test', description: 'Pending task 2' });
49
-
50
- queue.close();
51
-
52
- // 重新打开
53
- const newQueue = new TaskQueue({
54
- maxSize: 1000,
55
- persistDir: testDir,
56
- persistEnabled: true
57
- });
58
-
59
- const stats = newQueue.getStats();
60
- expect(stats.pending).toBe(2);
61
-
62
- const task1 = newQueue.get('pending-task-1');
63
- expect(task1?.description).toBe('Pending task');
64
- expect(task1?.status).toBe('pending');
65
-
66
- newQueue.close();
67
- });
68
-
69
- it('应该在关闭后保留 processing 任务(恢复为 pending)', () => {
70
- queue.add({ taskId: 'processing-task', taskType: 'test' });
71
- queue.markProcessing('processing-task');
72
-
73
- queue.close();
74
-
75
- const newQueue = new TaskQueue({
76
- maxSize: 1000,
77
- persistDir: testDir,
78
- persistEnabled: true
79
- });
80
-
81
- // 恢复后 processing 任务被重置为 pending,避免僵尸任务
82
- const stats = newQueue.getStats();
83
- expect(stats.pending).toBe(1);
84
- expect(stats.processing).toBe(0);
85
-
86
- const task = newQueue.get('processing-task');
87
- expect(task?.status).toBe('pending');
88
-
89
- newQueue.close();
90
- });
91
-
92
- it('不应该恢复已完成任务', () => {
93
- queue.add({ taskId: 'completed-task', taskType: 'test' });
94
- queue.complete('completed-task', { status: 'success', result: 'done' });
95
-
96
- queue.close();
97
-
98
- const newQueue = new TaskQueue({
99
- maxSize: 1000,
100
- persistDir: testDir,
101
- persistEnabled: true
102
- });
103
-
104
- const stats = newQueue.getStats();
105
- expect(stats.completed).toBe(0); // 已完成任务不恢复到内存
106
- expect(stats.total).toBe(0);
107
-
108
- newQueue.close();
109
- });
110
-
111
- it('不应该恢复已失败任务', () => {
112
- queue.add({ taskId: 'failed-task', taskType: 'test' });
113
- queue.complete('failed-task', { status: 'error', error: 'Something went wrong' });
114
-
115
- queue.close();
116
-
117
- const newQueue = new TaskQueue({
118
- maxSize: 1000,
119
- persistDir: testDir,
120
- persistEnabled: true
121
- });
122
-
123
- const stats = newQueue.getStats();
124
- expect(stats.failed).toBe(0);
125
- expect(stats.total).toBe(0);
126
-
127
- newQueue.close();
128
- });
129
- });
130
-
131
- describe('模拟崩溃场景', () => {
132
- it('应该在未正常关闭后恢复数据', () => {
133
- queue.add({ taskId: 'crash-task-1', taskType: 'test' });
134
- queue.add({ taskId: 'crash-task-2', taskType: 'test' });
135
- queue.markProcessing('crash-task-1');
136
-
137
- // 模拟崩溃:不调用 close()
138
- // 直接删除队列对象
139
- // @ts-expect-error 模拟崩溃
140
- queue = null;
141
-
142
- // 强制 GC(如果可用)
143
- if (global.gc) {
144
- global.gc();
145
- }
146
-
147
- // 重新打开
148
- const newQueue = new TaskQueue({
149
- maxSize: 1000,
150
- persistDir: testDir,
151
- persistEnabled: true
152
- });
153
-
154
- // 恢复后 processing 任务被重置为 pending,避免僵尸任务
155
- const stats = newQueue.getStats();
156
- expect(stats.pending).toBe(2); // crash-task-1 和 crash-task-2
157
- expect(stats.processing).toBe(0);
158
-
159
- newQueue.close();
160
- });
161
-
162
- it('应该在部分写入后恢复一致状态', () => {
163
- // 直接操作数据库模拟部分写入
164
- const dbPath = path.join(testDir, 'task-queue.db');
165
-
166
- queue.add({ taskId: 'partial-1', taskType: 'test' });
167
-
168
- // 在事务中途关闭队列
169
- const db = new Database(dbPath);
170
- db.exec('BEGIN');
171
- db.prepare('INSERT INTO tasks (id, task_type, status, created_at) VALUES (?, ?, ?, ?)')
172
- .run('partial-2', 'test', 'pending', Date.now());
173
- // 不提交事务,直接关闭
174
- db.close();
175
-
176
- // 重新打开队列
177
- const newQueue = new TaskQueue({
178
- maxSize: 1000,
179
- persistDir: testDir,
180
- persistEnabled: true
181
- });
182
-
183
- // partial-2 应该不在(事务未提交)
184
- const task = newQueue.get('partial-2');
185
- expect(task).toBeUndefined();
186
-
187
- // partial-1 应该在
188
- expect(newQueue.get('partial-1')).toBeDefined();
189
-
190
- newQueue.close();
191
- });
192
-
193
- it('应该处理数据库文件损坏', () => {
194
- queue.add({ taskId: 'before-corrupt', taskType: 'test' });
195
- queue.close();
196
-
197
- // 损坏数据库文件
198
- const dbPath = path.join(testDir, 'task-queue.db');
199
- fs.writeFileSync(dbPath, 'corrupted data here');
200
-
201
- // 应该能够创建新数据库
202
- expect(() => {
203
- const newQueue = new TaskQueue({
204
- maxSize: 1000,
205
- persistDir: testDir,
206
- persistEnabled: true
207
- });
208
- newQueue.close();
209
- }).not.toThrow();
210
- });
211
- });
212
-
213
- describe('数据库状态恢复', () => {
214
- it('应该恢复任务的完整状态', () => {
215
- const taskData = {
216
- taskId: 'full-state-task',
217
- taskType: 'full-test',
218
- description: 'Full state test',
219
- parameters: { key: 'value', nested: { data: 123 } }
220
- };
221
-
222
- queue.add(taskData);
223
- queue.markProcessing('full-state-task');
224
-
225
- queue.close();
226
-
227
- const newQueue = new TaskQueue({
228
- maxSize: 1000,
229
- persistDir: testDir,
230
- persistEnabled: true
231
- });
232
-
233
- const task = newQueue.get('full-state-task');
234
- expect(task?.taskType).toBe('full-test');
235
- expect(task?.description).toBe('Full state test');
236
- expect(task?.parameters).toEqual({ key: 'value', nested: { data: 123 } });
237
- // 恢复后 processing 任务被重置为 pending,避免僵尸任务
238
- expect(task?.status).toBe('pending');
239
-
240
- newQueue.close();
241
- });
242
-
243
- it('应该恢复 webhook 推送状态', () => {
244
- queue.add({ taskId: 'webhook-state-task', taskType: 'webhook' });
245
- queue.markWebhookPushed('webhook-state-task');
246
-
247
- queue.close();
248
-
249
- const newQueue = new TaskQueue({
250
- maxSize: 1000,
251
- persistDir: testDir,
252
- persistEnabled: true
253
- });
254
-
255
- const task = newQueue.get('webhook-state-task');
256
- expect(task?.webhookPushed).toBe(true);
257
-
258
- newQueue.close();
259
- });
260
-
261
- it('应该恢复时间戳', () => {
262
- const beforeAdd = Date.now();
263
- queue.add({ taskId: 'timestamp-task', taskType: 'timestamp' });
264
- queue.markProcessing('timestamp-task');
265
-
266
- queue.close();
267
-
268
- const newQueue = new TaskQueue({
269
- maxSize: 1000,
270
- persistDir: testDir,
271
- persistEnabled: true
272
- });
273
-
274
- const task = newQueue.get('timestamp-task');
275
- expect(task?.createdAt).toBeGreaterThanOrEqual(beforeAdd);
276
- expect(task?.updatedAt).toBeDefined();
277
- expect(task?.updatedAt).toBeGreaterThanOrEqual(beforeAdd);
278
-
279
- newQueue.close();
280
- });
281
- });
282
-
283
- describe('多实例恢复', () => {
284
- it('应该在多实例间共享数据', () => {
285
- queue.add({ taskId: 'shared-task', taskType: 'shared' });
286
- queue.close();
287
-
288
- // 同时打开多个实例(注意:实际中应该避免这种情况)
289
- const queue1 = new TaskQueue({
290
- maxSize: 1000,
291
- persistDir: testDir,
292
- persistEnabled: true
293
- });
294
-
295
- const task = queue1.get('shared-task');
296
- expect(task).toBeDefined();
297
-
298
- queue1.close();
299
- });
300
- });
301
-
302
- describe('数据库清理', () => {
303
- it('应该在恢复时忽略已删除的任务', () => {
304
- queue.add({ taskId: 'to-delete', taskType: 'delete' });
305
- queue.add({ taskId: 'to-keep', taskType: 'keep' });
306
- queue.delete('to-delete');
307
-
308
- queue.close();
309
-
310
- const newQueue = new TaskQueue({
311
- maxSize: 1000,
312
- persistDir: testDir,
313
- persistEnabled: true
314
- });
315
-
316
- expect(newQueue.get('to-delete')).toBeUndefined();
317
- expect(newQueue.get('to-keep')).toBeDefined();
318
-
319
- newQueue.close();
320
- });
321
-
322
- it('应该正确处理大量恢复任务', () => {
323
- // 添加大量任务
324
- for (let i = 0; i < 100; i++) {
325
- queue.add({ taskId: `bulk-task-${i}`, taskType: 'bulk' });
326
- }
327
-
328
- // 标记一些为 processing
329
- for (let i = 0; i < 10; i++) {
330
- queue.markProcessing(`bulk-task-${i}`);
331
- }
332
-
333
- queue.close();
334
-
335
- const newQueue = new TaskQueue({
336
- maxSize: 1000,
337
- persistDir: testDir,
338
- persistEnabled: true
339
- });
340
-
341
- // 恢复后 processing 任务被重置为 pending,避免僵尸任务
342
- const stats = newQueue.getStats();
343
- expect(stats.pending).toBe(100);
344
- expect(stats.processing).toBe(0);
345
- expect(stats.total).toBe(100);
346
-
347
- newQueue.close();
348
- });
349
- });
350
-
351
- describe('错误恢复', () => {
352
- it('应该处理无效的 JSON 数据', () => {
353
- const dbPath = path.join(testDir, 'task-queue.db');
354
-
355
- queue.add({ taskId: 'valid-task', taskType: 'test' });
356
- queue.close();
357
-
358
- // 插入无效 JSON
359
- const db = new Database(dbPath);
360
- db.prepare('INSERT INTO tasks (id, task_type, status, created_at, parameters) VALUES (?, ?, ?, ?, ?)')
361
- .run('invalid-json-task', 'test', 'pending', Date.now(), '{ invalid json }');
362
- db.close();
363
-
364
- // 应该能够启动并跳过无效数据
365
- const newQueue = new TaskQueue({
366
- maxSize: 1000,
367
- persistDir: testDir,
368
- persistEnabled: true
369
- });
370
-
371
- // 有效任务应该存在
372
- expect(newQueue.get('valid-task')).toBeDefined();
373
-
374
- newQueue.close();
375
- });
376
-
377
- it('应该处理缺失的列', () => {
378
- const dbPath = path.join(testDir, 'task-queue.db');
379
-
380
- queue.close();
381
-
382
- // 创建一个不同 schema 的表
383
- const db = new Database(dbPath);
384
- db.exec('DROP TABLE IF EXISTS tasks');
385
- db.exec(`
386
- CREATE TABLE tasks (
387
- id TEXT PRIMARY KEY,
388
- status TEXT NOT NULL,
389
- created_at INTEGER NOT NULL
390
- -- 缺少其他列
391
- )
392
- `);
393
- db.prepare('INSERT INTO tasks (id, status, created_at) VALUES (?, ?, ?)')
394
- .run('minimal-task', 'pending', Date.now());
395
- db.close();
396
-
397
- // 应该能够处理缺失的列
398
- expect(() => {
399
- const newQueue = new TaskQueue({
400
- maxSize: 1000,
401
- persistDir: testDir,
402
- persistEnabled: true
403
- });
404
- newQueue.close();
405
- }).not.toThrow();
406
- });
407
- });
408
- });