openclaw-overlay-plugin 0.7.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +406 -0
- package/SKILL.md +78 -0
- package/clawdbot.plugin.json +106 -0
- package/dist/cli-main.d.ts +7 -0
- package/dist/cli-main.js +192 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +14 -0
- package/dist/core/config.d.ts +11 -0
- package/dist/core/config.js +13 -0
- package/dist/core/index.d.ts +25 -0
- package/dist/core/index.js +26 -0
- package/dist/core/payment.d.ts +16 -0
- package/dist/core/payment.js +94 -0
- package/dist/core/types.d.ts +94 -0
- package/dist/core/types.js +4 -0
- package/dist/core/verify.d.ts +28 -0
- package/dist/core/verify.js +104 -0
- package/dist/core/wallet.d.ts +99 -0
- package/dist/core/wallet.js +219 -0
- package/dist/scripts/baemail/commands.d.ts +64 -0
- package/dist/scripts/baemail/commands.js +258 -0
- package/dist/scripts/baemail/handler.d.ts +36 -0
- package/dist/scripts/baemail/handler.js +284 -0
- package/dist/scripts/baemail/index.d.ts +5 -0
- package/dist/scripts/baemail/index.js +5 -0
- package/dist/scripts/config.d.ts +48 -0
- package/dist/scripts/config.js +68 -0
- package/dist/scripts/index.d.ts +7 -0
- package/dist/scripts/index.js +7 -0
- package/dist/scripts/messaging/connect.d.ts +8 -0
- package/dist/scripts/messaging/connect.js +114 -0
- package/dist/scripts/messaging/handlers.d.ts +21 -0
- package/dist/scripts/messaging/handlers.js +334 -0
- package/dist/scripts/messaging/inbox.d.ts +11 -0
- package/dist/scripts/messaging/inbox.js +51 -0
- package/dist/scripts/messaging/index.d.ts +8 -0
- package/dist/scripts/messaging/index.js +8 -0
- package/dist/scripts/messaging/poll.d.ts +7 -0
- package/dist/scripts/messaging/poll.js +52 -0
- package/dist/scripts/messaging/send.d.ts +7 -0
- package/dist/scripts/messaging/send.js +43 -0
- package/dist/scripts/output.d.ts +12 -0
- package/dist/scripts/output.js +19 -0
- package/dist/scripts/overlay/discover.d.ts +7 -0
- package/dist/scripts/overlay/discover.js +72 -0
- package/dist/scripts/overlay/index.d.ts +7 -0
- package/dist/scripts/overlay/index.js +7 -0
- package/dist/scripts/overlay/registration.d.ts +19 -0
- package/dist/scripts/overlay/registration.js +176 -0
- package/dist/scripts/overlay/services.d.ts +29 -0
- package/dist/scripts/overlay/services.js +167 -0
- package/dist/scripts/overlay/transaction.d.ts +42 -0
- package/dist/scripts/overlay/transaction.js +103 -0
- package/dist/scripts/payment/build.d.ts +24 -0
- package/dist/scripts/payment/build.js +54 -0
- package/dist/scripts/payment/commands.d.ts +15 -0
- package/dist/scripts/payment/commands.js +73 -0
- package/dist/scripts/payment/index.d.ts +6 -0
- package/dist/scripts/payment/index.js +6 -0
- package/dist/scripts/payment/types.d.ts +56 -0
- package/dist/scripts/payment/types.js +4 -0
- package/dist/scripts/services/index.d.ts +6 -0
- package/dist/scripts/services/index.js +6 -0
- package/dist/scripts/services/queue.d.ts +11 -0
- package/dist/scripts/services/queue.js +28 -0
- package/dist/scripts/services/request.d.ts +7 -0
- package/dist/scripts/services/request.js +82 -0
- package/dist/scripts/services/respond.d.ts +11 -0
- package/dist/scripts/services/respond.js +132 -0
- package/dist/scripts/types.d.ts +107 -0
- package/dist/scripts/types.js +4 -0
- package/dist/scripts/utils/index.d.ts +6 -0
- package/dist/scripts/utils/index.js +6 -0
- package/dist/scripts/utils/merkle.d.ts +12 -0
- package/dist/scripts/utils/merkle.js +47 -0
- package/dist/scripts/utils/storage.d.ts +66 -0
- package/dist/scripts/utils/storage.js +211 -0
- package/dist/scripts/utils/woc.d.ts +26 -0
- package/dist/scripts/utils/woc.js +91 -0
- package/dist/scripts/wallet/balance.d.ts +22 -0
- package/dist/scripts/wallet/balance.js +240 -0
- package/dist/scripts/wallet/identity.d.ts +70 -0
- package/dist/scripts/wallet/identity.js +151 -0
- package/dist/scripts/wallet/index.d.ts +6 -0
- package/dist/scripts/wallet/index.js +6 -0
- package/dist/scripts/wallet/setup.d.ts +15 -0
- package/dist/scripts/wallet/setup.js +105 -0
- package/dist/scripts/x-verification/commands.d.ts +27 -0
- package/dist/scripts/x-verification/commands.js +222 -0
- package/dist/scripts/x-verification/index.d.ts +4 -0
- package/dist/scripts/x-verification/index.js +4 -0
- package/dist/services/built-in/api-proxy/index.d.ts +6 -0
- package/dist/services/built-in/api-proxy/index.js +23 -0
- package/dist/services/built-in/code-develop/index.d.ts +6 -0
- package/dist/services/built-in/code-develop/index.js +23 -0
- package/dist/services/built-in/code-review/index.d.ts +10 -0
- package/dist/services/built-in/code-review/index.js +51 -0
- package/dist/services/built-in/image-analysis/index.d.ts +6 -0
- package/dist/services/built-in/image-analysis/index.js +33 -0
- package/dist/services/built-in/memory-store/index.d.ts +6 -0
- package/dist/services/built-in/memory-store/index.js +22 -0
- package/dist/services/built-in/roulette/index.d.ts +6 -0
- package/dist/services/built-in/roulette/index.js +27 -0
- package/dist/services/built-in/summarize/index.d.ts +6 -0
- package/dist/services/built-in/summarize/index.js +21 -0
- package/dist/services/built-in/tell-joke/handler.d.ts +7 -0
- package/dist/services/built-in/tell-joke/handler.js +122 -0
- package/dist/services/built-in/tell-joke/index.d.ts +9 -0
- package/dist/services/built-in/tell-joke/index.js +31 -0
- package/dist/services/built-in/translate/index.d.ts +6 -0
- package/dist/services/built-in/translate/index.js +21 -0
- package/dist/services/built-in/web-research/index.d.ts +9 -0
- package/dist/services/built-in/web-research/index.js +51 -0
- package/dist/services/index.d.ts +13 -0
- package/dist/services/index.js +14 -0
- package/dist/services/loader.d.ts +77 -0
- package/dist/services/loader.js +292 -0
- package/dist/services/manager.d.ts +86 -0
- package/dist/services/manager.js +255 -0
- package/dist/services/registry.d.ts +98 -0
- package/dist/services/registry.js +204 -0
- package/dist/services/types.d.ts +230 -0
- package/dist/services/types.js +30 -0
- package/dist/test/cli.test.d.ts +7 -0
- package/dist/test/cli.test.js +329 -0
- package/dist/test/comprehensive-overlay.test.d.ts +13 -0
- package/dist/test/comprehensive-overlay.test.js +593 -0
- package/dist/test/key-derivation.test.d.ts +12 -0
- package/dist/test/key-derivation.test.js +86 -0
- package/dist/test/overlay-submit.test.d.ts +10 -0
- package/dist/test/overlay-submit.test.js +460 -0
- package/dist/test/request-response-flow.test.d.ts +5 -0
- package/dist/test/request-response-flow.test.js +209 -0
- package/dist/test/service-system.test.d.ts +5 -0
- package/dist/test/service-system.test.js +190 -0
- package/dist/test/utils/server-logic.d.ts +98 -0
- package/dist/test/utils/server-logic.js +286 -0
- package/dist/test/wallet.test.d.ts +7 -0
- package/dist/test/wallet.test.js +146 -0
- package/index.ts +1965 -0
- package/openclaw.plugin.json +106 -0
- package/package.json +73 -0
- package/src/cli-main.ts +230 -0
- package/src/cli.ts +16 -0
- package/src/core/README.md +246 -0
- package/src/core/config.ts +21 -0
- package/src/core/index.ts +42 -0
- package/src/core/payment.ts +111 -0
- package/src/core/types.ts +102 -0
- package/src/core/verify.ts +119 -0
- package/src/core/wallet.ts +282 -0
- package/src/scripts/baemail/commands.ts +326 -0
- package/src/scripts/baemail/handler.ts +338 -0
- package/src/scripts/baemail/index.ts +6 -0
- package/src/scripts/config.ts +81 -0
- package/src/scripts/index.ts +8 -0
- package/src/scripts/messaging/connect.ts +121 -0
- package/src/scripts/messaging/handlers.ts +394 -0
- package/src/scripts/messaging/inbox.ts +64 -0
- package/src/scripts/messaging/index.ts +9 -0
- package/src/scripts/messaging/poll.ts +59 -0
- package/src/scripts/messaging/send.ts +54 -0
- package/src/scripts/output.ts +21 -0
- package/src/scripts/overlay/discover.ts +81 -0
- package/src/scripts/overlay/index.ts +8 -0
- package/src/scripts/overlay/registration.ts +199 -0
- package/src/scripts/overlay/services.ts +199 -0
- package/src/scripts/overlay/transaction.ts +124 -0
- package/src/scripts/payment/build.ts +65 -0
- package/src/scripts/payment/commands.ts +92 -0
- package/src/scripts/payment/index.ts +7 -0
- package/src/scripts/payment/types.ts +62 -0
- package/src/scripts/services/index.ts +7 -0
- package/src/scripts/services/queue.ts +35 -0
- package/src/scripts/services/request.ts +98 -0
- package/src/scripts/services/respond.ts +149 -0
- package/src/scripts/types.ts +121 -0
- package/src/scripts/utils/index.ts +7 -0
- package/src/scripts/utils/merkle.ts +57 -0
- package/src/scripts/utils/storage.ts +231 -0
- package/src/scripts/utils/woc.ts +106 -0
- package/src/scripts/wallet/balance.ts +277 -0
- package/src/scripts/wallet/identity.ts +203 -0
- package/src/scripts/wallet/index.ts +7 -0
- package/src/scripts/wallet/setup.ts +121 -0
- package/src/scripts/x-verification/commands.ts +256 -0
- package/src/scripts/x-verification/index.ts +5 -0
- package/src/services/built-in/api-proxy/index.ts +26 -0
- package/src/services/built-in/api-proxy/prompt.md +26 -0
- package/src/services/built-in/code-develop/index.ts +26 -0
- package/src/services/built-in/code-develop/prompt.md +35 -0
- package/src/services/built-in/code-review/index.ts +54 -0
- package/src/services/built-in/code-review/prompt.md +105 -0
- package/src/services/built-in/image-analysis/index.ts +36 -0
- package/src/services/built-in/image-analysis/prompt.md +42 -0
- package/src/services/built-in/memory-store/index.ts +25 -0
- package/src/services/built-in/memory-store/prompt.md +45 -0
- package/src/services/built-in/roulette/index.ts +30 -0
- package/src/services/built-in/roulette/prompt.md +35 -0
- package/src/services/built-in/summarize/index.ts +24 -0
- package/src/services/built-in/summarize/prompt.md +27 -0
- package/src/services/built-in/tell-joke/handler.ts +134 -0
- package/src/services/built-in/tell-joke/index.ts +34 -0
- package/src/services/built-in/tell-joke/prompt.md +59 -0
- package/src/services/built-in/translate/index.ts +24 -0
- package/src/services/built-in/translate/prompt.md +23 -0
- package/src/services/built-in/web-research/index.ts +54 -0
- package/src/services/built-in/web-research/prompt.md +110 -0
- package/src/services/index.ts +16 -0
- package/src/services/loader.ts +344 -0
- package/src/services/manager.ts +304 -0
- package/src/services/registry.ts +246 -0
- package/src/services/types.ts +259 -0
- package/src/test/cli.test.ts +352 -0
- package/src/test/comprehensive-overlay.test.ts +729 -0
- package/src/test/key-derivation.test.ts +102 -0
- package/src/test/overlay-submit.test.ts +570 -0
- package/src/test/request-response-flow.test.ts +252 -0
- package/src/test/service-system.test.ts +241 -0
- package/src/test/utils/server-logic.ts +368 -0
- package/src/test/wallet.test.ts +166 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for overlay /submit endpoint compatibility.
|
|
3
|
+
*
|
|
4
|
+
* These tests validate that the client constructs BEEF and payloads
|
|
5
|
+
* in the exact format expected by the clawdbot-overlay server's
|
|
6
|
+
* topic managers using PushDrop tokens.
|
|
7
|
+
*
|
|
8
|
+
* Run with: npx tsx src/test/overlay-submit.test.ts
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for overlay /submit endpoint compatibility.
|
|
3
|
+
*
|
|
4
|
+
* These tests validate that the client constructs BEEF and payloads
|
|
5
|
+
* in the exact format expected by the clawdbot-overlay server's
|
|
6
|
+
* topic managers using PushDrop tokens.
|
|
7
|
+
*
|
|
8
|
+
* Run with: npx tsx src/test/overlay-submit.test.ts
|
|
9
|
+
*/
|
|
10
|
+
import { Beef, Transaction, PrivateKey, P2PKH, LockingScript, OP, PushDrop } from '@bsv/sdk';
|
|
11
|
+
const PROTOCOL_ID = 'clawdbot-overlay-v1';
|
|
12
|
+
/**
|
|
13
|
+
* Extract data fields from a PushDrop script using the SDK's decode method.
|
|
14
|
+
*/
|
|
15
|
+
function extractPushDropFields(script) {
|
|
16
|
+
try {
|
|
17
|
+
const decoded = PushDrop.decode(script);
|
|
18
|
+
return decoded.fields;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse identity output using PushDrop decode and server's validation logic.
|
|
26
|
+
*/
|
|
27
|
+
function parseIdentityOutput(script) {
|
|
28
|
+
const fields = extractPushDropFields(script);
|
|
29
|
+
if (!fields || fields.length < 1)
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
const payload = JSON.parse(new TextDecoder().decode(new Uint8Array(fields[0])));
|
|
33
|
+
// Server validation rules
|
|
34
|
+
if (payload.protocol !== PROTOCOL_ID)
|
|
35
|
+
return null;
|
|
36
|
+
if (payload.type !== 'identity')
|
|
37
|
+
return null;
|
|
38
|
+
if (typeof payload.identityKey !== 'string' || !/^[0-9a-fA-F]{66}$/.test(payload.identityKey))
|
|
39
|
+
return null;
|
|
40
|
+
if (typeof payload.name !== 'string' || payload.name.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
if (!Array.isArray(payload.capabilities))
|
|
43
|
+
return null;
|
|
44
|
+
if (typeof payload.timestamp !== 'string')
|
|
45
|
+
return null;
|
|
46
|
+
return payload;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Parse service output using PushDrop decode and server's validation logic.
|
|
54
|
+
*/
|
|
55
|
+
function parseServiceOutput(script) {
|
|
56
|
+
const fields = extractPushDropFields(script);
|
|
57
|
+
if (!fields || fields.length < 1)
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
const payload = JSON.parse(new TextDecoder().decode(new Uint8Array(fields[0])));
|
|
61
|
+
// Server validation rules
|
|
62
|
+
if (payload.protocol !== PROTOCOL_ID)
|
|
63
|
+
return null;
|
|
64
|
+
if (payload.type !== 'service')
|
|
65
|
+
return null;
|
|
66
|
+
if (typeof payload.identityKey !== 'string' || !/^[0-9a-fA-F]{66}$/.test(payload.identityKey))
|
|
67
|
+
return null;
|
|
68
|
+
if (typeof payload.serviceId !== 'string' || payload.serviceId.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
if (typeof payload.name !== 'string' || payload.name.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
if (!payload.pricing || typeof payload.pricing.amountSats !== 'number')
|
|
73
|
+
return null;
|
|
74
|
+
if (typeof payload.timestamp !== 'string')
|
|
75
|
+
return null;
|
|
76
|
+
return payload;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create a minimally encoded push chunk for script building.
|
|
84
|
+
*/
|
|
85
|
+
function createPushChunk(data) {
|
|
86
|
+
if (data.length === 0) {
|
|
87
|
+
return { op: 0 };
|
|
88
|
+
}
|
|
89
|
+
if (data.length === 1 && data[0] === 0) {
|
|
90
|
+
return { op: 0 };
|
|
91
|
+
}
|
|
92
|
+
if (data.length === 1 && data[0] > 0 && data[0] <= 16) {
|
|
93
|
+
return { op: 0x50 + data[0] };
|
|
94
|
+
}
|
|
95
|
+
if (data.length <= 75) {
|
|
96
|
+
return { op: data.length, data };
|
|
97
|
+
}
|
|
98
|
+
if (data.length <= 255) {
|
|
99
|
+
return { op: 0x4c, data };
|
|
100
|
+
}
|
|
101
|
+
if (data.length <= 65535) {
|
|
102
|
+
return { op: 0x4d, data };
|
|
103
|
+
}
|
|
104
|
+
return { op: 0x4e, data };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Build a PushDrop-style locking script with JSON payload.
|
|
108
|
+
* Format: <pubkey> OP_CHECKSIG <jsonBytes> OP_DROP
|
|
109
|
+
*/
|
|
110
|
+
function buildPushDropScript(privKey, payload) {
|
|
111
|
+
const pubKey = privKey.toPublicKey();
|
|
112
|
+
const pubKeyBytes = pubKey.toDER();
|
|
113
|
+
const jsonBytes = Array.from(new TextEncoder().encode(JSON.stringify(payload)));
|
|
114
|
+
const chunks = [];
|
|
115
|
+
// P2PK lock: <pubkey> OP_CHECKSIG
|
|
116
|
+
chunks.push({ op: pubKeyBytes.length, data: pubKeyBytes });
|
|
117
|
+
chunks.push({ op: OP.OP_CHECKSIG });
|
|
118
|
+
// Data field: <jsonBytes>
|
|
119
|
+
chunks.push(createPushChunk(jsonBytes));
|
|
120
|
+
// OP_DROP to clean stack
|
|
121
|
+
chunks.push({ op: OP.OP_DROP });
|
|
122
|
+
return new LockingScript(chunks);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Simulate the server's identifyAdmissibleOutputs logic.
|
|
126
|
+
*/
|
|
127
|
+
function identifyAdmissibleOutputs(beef, type) {
|
|
128
|
+
// Parse BEEF and get the newest (subject) transaction
|
|
129
|
+
const parsedBeef = Beef.fromBinary(beef);
|
|
130
|
+
const subjectTx = parsedBeef.txs[0]._tx;
|
|
131
|
+
if (!subjectTx) {
|
|
132
|
+
return { outputsToAdmit: [], coinsToRetain: [] };
|
|
133
|
+
}
|
|
134
|
+
const outputsToAdmit = [];
|
|
135
|
+
for (let i = 0; i < subjectTx.outputs.length; i++) {
|
|
136
|
+
const output = subjectTx.outputs[i];
|
|
137
|
+
if (output.lockingScript) {
|
|
138
|
+
const parsed = type === 'identity'
|
|
139
|
+
? parseIdentityOutput(output.lockingScript)
|
|
140
|
+
: parseServiceOutput(output.lockingScript);
|
|
141
|
+
if (parsed !== null) {
|
|
142
|
+
outputsToAdmit.push(i);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { outputsToAdmit, coinsToRetain: [] };
|
|
147
|
+
}
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// Test utilities
|
|
150
|
+
// ============================================================================
|
|
151
|
+
let testsPassed = 0;
|
|
152
|
+
let testsFailed = 0;
|
|
153
|
+
function assert(condition, message) {
|
|
154
|
+
if (!condition) {
|
|
155
|
+
console.error(`❌ FAIL: ${message}`);
|
|
156
|
+
testsFailed++;
|
|
157
|
+
throw new Error(message);
|
|
158
|
+
}
|
|
159
|
+
console.log(`✅ PASS: ${message}`);
|
|
160
|
+
testsPassed++;
|
|
161
|
+
}
|
|
162
|
+
function assertThrows(fn, message) {
|
|
163
|
+
try {
|
|
164
|
+
fn();
|
|
165
|
+
console.error(`❌ FAIL: ${message} (expected to throw)`);
|
|
166
|
+
testsFailed++;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
console.log(`✅ PASS: ${message}`);
|
|
170
|
+
testsPassed++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// Test: BEEF format validation
|
|
175
|
+
// ============================================================================
|
|
176
|
+
async function testBeefFormat() {
|
|
177
|
+
console.log('\n=== Test: BEEF Format Validation ===');
|
|
178
|
+
// Create a minimal transaction chain
|
|
179
|
+
const privKey = PrivateKey.fromRandom();
|
|
180
|
+
const pubKeyHash = privKey.toPublicKey().toHash();
|
|
181
|
+
// Source transaction (simulating a mined tx with merkle proof)
|
|
182
|
+
const sourceTx = new Transaction();
|
|
183
|
+
sourceTx.addOutput({
|
|
184
|
+
lockingScript: new P2PKH().lock(pubKeyHash),
|
|
185
|
+
satoshis: 10000,
|
|
186
|
+
});
|
|
187
|
+
// Spending transaction with PushDrop output
|
|
188
|
+
const tx = new Transaction();
|
|
189
|
+
tx.addInput({
|
|
190
|
+
sourceTransaction: sourceTx,
|
|
191
|
+
sourceOutputIndex: 0,
|
|
192
|
+
unlockingScriptTemplate: new P2PKH().unlock(privKey),
|
|
193
|
+
});
|
|
194
|
+
tx.addOutput({
|
|
195
|
+
lockingScript: buildPushDropScript(privKey, { protocol: PROTOCOL_ID, type: 'identity', test: true }),
|
|
196
|
+
satoshis: 1,
|
|
197
|
+
});
|
|
198
|
+
await tx.sign();
|
|
199
|
+
// Build BEEF
|
|
200
|
+
const beef = new Beef();
|
|
201
|
+
beef.mergeTransaction(tx);
|
|
202
|
+
const binary = beef.toBinary();
|
|
203
|
+
// Validate BEEF magic bytes
|
|
204
|
+
const magic = binary.slice(0, 4);
|
|
205
|
+
const magicHex = magic.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
206
|
+
assert(magicHex === '0100beef' || magicHex === '0200beef', `BEEF magic bytes should be 0100beef or 0200beef, got ${magicHex}`);
|
|
207
|
+
// Validate BEEF can be parsed
|
|
208
|
+
const parsed = Beef.fromBinary(binary);
|
|
209
|
+
assert(parsed.txs.length >= 1, `BEEF should contain at least 1 transaction, got ${parsed.txs.length}`);
|
|
210
|
+
// Validate the newest transaction can be found in BEEF
|
|
211
|
+
const beefTx = parsed.txs[0];
|
|
212
|
+
const newestTxid = beefTx.txid || beefTx._tx?.id('hex');
|
|
213
|
+
assert(newestTxid === tx.id('hex'), `Newest transaction in BEEF should match original, got ${newestTxid?.slice(0, 16)}`);
|
|
214
|
+
}
|
|
215
|
+
// ============================================================================
|
|
216
|
+
// Test: Identity payload validation
|
|
217
|
+
// ============================================================================
|
|
218
|
+
async function testIdentityPayload() {
|
|
219
|
+
console.log('\n=== Test: Identity Payload Validation ===');
|
|
220
|
+
const privKey = PrivateKey.fromRandom();
|
|
221
|
+
const identityKey = privKey.toPublicKey().toString();
|
|
222
|
+
// Valid identity payload
|
|
223
|
+
const validPayload = {
|
|
224
|
+
protocol: PROTOCOL_ID,
|
|
225
|
+
type: 'identity',
|
|
226
|
+
identityKey,
|
|
227
|
+
name: 'test-agent',
|
|
228
|
+
description: 'A test agent',
|
|
229
|
+
channels: { overlay: 'https://example.com' },
|
|
230
|
+
capabilities: ['test'],
|
|
231
|
+
timestamp: new Date().toISOString(),
|
|
232
|
+
};
|
|
233
|
+
const script = buildPushDropScript(privKey, validPayload);
|
|
234
|
+
const parsed = parseIdentityOutput(script);
|
|
235
|
+
assert(parsed !== null, 'Valid identity payload should be parsed');
|
|
236
|
+
assert(parsed.identityKey === identityKey, 'Identity key should match');
|
|
237
|
+
assert(parsed.name === 'test-agent', 'Name should match');
|
|
238
|
+
assert(parsed.type === 'identity', 'Type should be identity');
|
|
239
|
+
// Invalid: wrong protocol
|
|
240
|
+
const wrongProtocol = { ...validPayload, protocol: 'wrong-protocol' };
|
|
241
|
+
const script2 = buildPushDropScript(privKey, wrongProtocol);
|
|
242
|
+
assert(parseIdentityOutput(script2) === null, 'Wrong protocol should be rejected');
|
|
243
|
+
// Invalid: wrong type
|
|
244
|
+
const wrongType = { ...validPayload, type: 'service' };
|
|
245
|
+
const script3 = buildPushDropScript(privKey, wrongType);
|
|
246
|
+
assert(parseIdentityOutput(script3) === null, 'Wrong type should be rejected');
|
|
247
|
+
// Invalid: bad identity key
|
|
248
|
+
const badKey = { ...validPayload, identityKey: 'not-a-valid-key' };
|
|
249
|
+
const script4 = buildPushDropScript(privKey, badKey);
|
|
250
|
+
assert(parseIdentityOutput(script4) === null, 'Invalid identity key should be rejected');
|
|
251
|
+
// Invalid: empty name
|
|
252
|
+
const emptyName = { ...validPayload, name: '' };
|
|
253
|
+
const script5 = buildPushDropScript(privKey, emptyName);
|
|
254
|
+
assert(parseIdentityOutput(script5) === null, 'Empty name should be rejected');
|
|
255
|
+
// Invalid: capabilities not array
|
|
256
|
+
const badCaps = { ...validPayload, capabilities: 'not-array' };
|
|
257
|
+
const script6 = buildPushDropScript(privKey, badCaps);
|
|
258
|
+
assert(parseIdentityOutput(script6) === null, 'Non-array capabilities should be rejected');
|
|
259
|
+
}
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// Test: Service payload validation
|
|
262
|
+
// ============================================================================
|
|
263
|
+
async function testServicePayload() {
|
|
264
|
+
console.log('\n=== Test: Service Payload Validation ===');
|
|
265
|
+
const privKey = PrivateKey.fromRandom();
|
|
266
|
+
const identityKey = privKey.toPublicKey().toString();
|
|
267
|
+
// Valid service payload
|
|
268
|
+
const validPayload = {
|
|
269
|
+
protocol: PROTOCOL_ID,
|
|
270
|
+
type: 'service',
|
|
271
|
+
identityKey,
|
|
272
|
+
serviceId: 'test-service',
|
|
273
|
+
name: 'Test Service',
|
|
274
|
+
description: 'A test service',
|
|
275
|
+
pricing: { model: 'per-task', amountSats: 100 },
|
|
276
|
+
timestamp: new Date().toISOString(),
|
|
277
|
+
};
|
|
278
|
+
const script = buildPushDropScript(privKey, validPayload);
|
|
279
|
+
const parsed = parseServiceOutput(script);
|
|
280
|
+
assert(parsed !== null, 'Valid service payload should be parsed');
|
|
281
|
+
assert(parsed.serviceId === 'test-service', 'Service ID should match');
|
|
282
|
+
assert(parsed.pricing.amountSats === 100, 'Price should match');
|
|
283
|
+
// Invalid: missing pricing
|
|
284
|
+
const noPricing = { ...validPayload, pricing: undefined };
|
|
285
|
+
const script2 = buildPushDropScript(privKey, noPricing);
|
|
286
|
+
assert(parseServiceOutput(script2) === null, 'Missing pricing should be rejected');
|
|
287
|
+
// Invalid: empty serviceId
|
|
288
|
+
const emptyId = { ...validPayload, serviceId: '' };
|
|
289
|
+
const script3 = buildPushDropScript(privKey, emptyId);
|
|
290
|
+
assert(parseServiceOutput(script3) === null, 'Empty serviceId should be rejected');
|
|
291
|
+
}
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// Test: Full BEEF submission simulation
|
|
294
|
+
// ============================================================================
|
|
295
|
+
async function testBeefSubmission() {
|
|
296
|
+
console.log('\n=== Test: BEEF Submission Simulation ===');
|
|
297
|
+
const privKey = PrivateKey.fromRandom();
|
|
298
|
+
const pubKeyHash = privKey.toPublicKey().toHash();
|
|
299
|
+
const identityKey = privKey.toPublicKey().toString();
|
|
300
|
+
// Create source transaction (simulating confirmed tx)
|
|
301
|
+
const sourceTx = new Transaction();
|
|
302
|
+
sourceTx.addOutput({
|
|
303
|
+
lockingScript: new P2PKH().lock(pubKeyHash),
|
|
304
|
+
satoshis: 10000,
|
|
305
|
+
});
|
|
306
|
+
// Valid identity registration
|
|
307
|
+
const identityPayload = {
|
|
308
|
+
protocol: PROTOCOL_ID,
|
|
309
|
+
type: 'identity',
|
|
310
|
+
identityKey,
|
|
311
|
+
name: 'test-agent',
|
|
312
|
+
description: 'Test agent for unit tests',
|
|
313
|
+
channels: { overlay: 'https://clawoverlay.com' },
|
|
314
|
+
capabilities: ['testing'],
|
|
315
|
+
timestamp: new Date().toISOString(),
|
|
316
|
+
};
|
|
317
|
+
const tx = new Transaction();
|
|
318
|
+
tx.addInput({
|
|
319
|
+
sourceTransaction: sourceTx,
|
|
320
|
+
sourceOutputIndex: 0,
|
|
321
|
+
unlockingScriptTemplate: new P2PKH().unlock(privKey),
|
|
322
|
+
});
|
|
323
|
+
tx.addOutput({
|
|
324
|
+
lockingScript: buildPushDropScript(privKey, identityPayload),
|
|
325
|
+
satoshis: 1,
|
|
326
|
+
});
|
|
327
|
+
tx.addOutput({
|
|
328
|
+
lockingScript: new P2PKH().lock(pubKeyHash),
|
|
329
|
+
satoshis: 9900,
|
|
330
|
+
});
|
|
331
|
+
await tx.sign();
|
|
332
|
+
// Build BEEF with ancestry
|
|
333
|
+
const beef = new Beef();
|
|
334
|
+
beef.mergeTransaction(tx);
|
|
335
|
+
const beefBinary = beef.toBinary();
|
|
336
|
+
// Simulate server's topic manager
|
|
337
|
+
const result = identifyAdmissibleOutputs(beefBinary, 'identity');
|
|
338
|
+
assert(result.outputsToAdmit.length === 1, `Should admit 1 output, got ${result.outputsToAdmit.length}`);
|
|
339
|
+
assert(result.outputsToAdmit[0] === 0, 'Should admit output index 0 (PushDrop)');
|
|
340
|
+
}
|
|
341
|
+
// ============================================================================
|
|
342
|
+
// Test: Chained transactions (stored BEEF)
|
|
343
|
+
// ============================================================================
|
|
344
|
+
async function testChainedBeef() {
|
|
345
|
+
console.log('\n=== Test: Chained BEEF (multiple unconfirmed txs) ===');
|
|
346
|
+
const privKey = PrivateKey.fromRandom();
|
|
347
|
+
const pubKeyHash = privKey.toPublicKey().toHash();
|
|
348
|
+
const identityKey = privKey.toPublicKey().toString();
|
|
349
|
+
// Grandparent tx (simulating mined tx - would have merkle proof)
|
|
350
|
+
const grandparentTx = new Transaction();
|
|
351
|
+
grandparentTx.addOutput({
|
|
352
|
+
lockingScript: new P2PKH().lock(pubKeyHash),
|
|
353
|
+
satoshis: 100000,
|
|
354
|
+
});
|
|
355
|
+
// Parent tx (first overlay submission - unconfirmed)
|
|
356
|
+
const parentTx = new Transaction();
|
|
357
|
+
parentTx.addInput({
|
|
358
|
+
sourceTransaction: grandparentTx,
|
|
359
|
+
sourceOutputIndex: 0,
|
|
360
|
+
unlockingScriptTemplate: new P2PKH().unlock(privKey),
|
|
361
|
+
});
|
|
362
|
+
parentTx.addOutput({
|
|
363
|
+
lockingScript: buildPushDropScript(privKey, {
|
|
364
|
+
protocol: PROTOCOL_ID,
|
|
365
|
+
type: 'identity',
|
|
366
|
+
identityKey,
|
|
367
|
+
name: 'parent-tx',
|
|
368
|
+
description: 'First registration',
|
|
369
|
+
channels: {},
|
|
370
|
+
capabilities: [],
|
|
371
|
+
timestamp: new Date().toISOString(),
|
|
372
|
+
}),
|
|
373
|
+
satoshis: 1,
|
|
374
|
+
});
|
|
375
|
+
parentTx.addOutput({
|
|
376
|
+
lockingScript: new P2PKH().lock(pubKeyHash),
|
|
377
|
+
satoshis: 99900,
|
|
378
|
+
});
|
|
379
|
+
await parentTx.sign();
|
|
380
|
+
// Child tx (second overlay submission - spending parent's change)
|
|
381
|
+
const childTx = new Transaction();
|
|
382
|
+
childTx.addInput({
|
|
383
|
+
sourceTransaction: parentTx,
|
|
384
|
+
sourceOutputIndex: 1, // Spend the change output
|
|
385
|
+
unlockingScriptTemplate: new P2PKH().unlock(privKey),
|
|
386
|
+
});
|
|
387
|
+
childTx.addOutput({
|
|
388
|
+
lockingScript: buildPushDropScript(privKey, {
|
|
389
|
+
protocol: PROTOCOL_ID,
|
|
390
|
+
type: 'service',
|
|
391
|
+
identityKey,
|
|
392
|
+
serviceId: 'test-svc',
|
|
393
|
+
name: 'Test Service',
|
|
394
|
+
description: 'Service from child tx',
|
|
395
|
+
pricing: { model: 'per-task', amountSats: 50 },
|
|
396
|
+
timestamp: new Date().toISOString(),
|
|
397
|
+
}),
|
|
398
|
+
satoshis: 1,
|
|
399
|
+
});
|
|
400
|
+
childTx.addOutput({
|
|
401
|
+
lockingScript: new P2PKH().lock(pubKeyHash),
|
|
402
|
+
satoshis: 99800,
|
|
403
|
+
});
|
|
404
|
+
await childTx.sign();
|
|
405
|
+
// Build BEEF - should include full chain
|
|
406
|
+
const beef = new Beef();
|
|
407
|
+
beef.mergeTransaction(childTx);
|
|
408
|
+
const beefBinary = beef.toBinary();
|
|
409
|
+
// Verify BEEF contains all transactions
|
|
410
|
+
const parsedBeef = Beef.fromBinary(beefBinary);
|
|
411
|
+
assert(parsedBeef.txs.length >= 2, `BEEF should contain at least 2 txs for chain, got ${parsedBeef.txs.length}`);
|
|
412
|
+
// Verify child tx is the newest in BEEF
|
|
413
|
+
const beefTx = parsedBeef.txs[0];
|
|
414
|
+
const newestTxid = beefTx.txid || beefTx._tx?.id('hex');
|
|
415
|
+
assert(newestTxid === childTx.id('hex'), 'Newest tx in BEEF should be the child transaction');
|
|
416
|
+
// Simulate server validation
|
|
417
|
+
const result = identifyAdmissibleOutputs(beefBinary, 'service');
|
|
418
|
+
assert(result.outputsToAdmit.length === 1, 'Should admit the service output');
|
|
419
|
+
}
|
|
420
|
+
// ============================================================================
|
|
421
|
+
// Test: Invalid BEEF handling
|
|
422
|
+
// ============================================================================
|
|
423
|
+
async function testInvalidBeef() {
|
|
424
|
+
console.log('\n=== Test: Invalid BEEF Handling ===');
|
|
425
|
+
// Empty BEEF
|
|
426
|
+
const emptyBeef = new Beef();
|
|
427
|
+
const emptyBinary = emptyBeef.toBinary();
|
|
428
|
+
assertThrows(() => Transaction.fromBEEF(emptyBinary), 'Empty BEEF should throw when extracting transaction');
|
|
429
|
+
// Malformed BEEF (random bytes)
|
|
430
|
+
const garbage = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
431
|
+
assertThrows(() => Beef.fromBinary(Array.from(garbage)), 'Garbage bytes should throw when parsing BEEF');
|
|
432
|
+
// BEEF with wrong magic
|
|
433
|
+
const wrongMagic = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF, 0, 0]);
|
|
434
|
+
assertThrows(() => Beef.fromBinary(Array.from(wrongMagic)), 'Wrong magic bytes should throw when parsing BEEF');
|
|
435
|
+
}
|
|
436
|
+
// ============================================================================
|
|
437
|
+
// Main test runner
|
|
438
|
+
// ============================================================================
|
|
439
|
+
async function runTests() {
|
|
440
|
+
console.log('Starting overlay submit tests (PushDrop format)...\n');
|
|
441
|
+
try {
|
|
442
|
+
await testBeefFormat();
|
|
443
|
+
await testIdentityPayload();
|
|
444
|
+
await testServicePayload();
|
|
445
|
+
await testBeefSubmission();
|
|
446
|
+
await testChainedBeef();
|
|
447
|
+
await testInvalidBeef();
|
|
448
|
+
console.log(`\n========================================`);
|
|
449
|
+
console.log(`Tests completed: ${testsPassed} passed, ${testsFailed} failed`);
|
|
450
|
+
console.log(`========================================`);
|
|
451
|
+
if (testsFailed > 0) {
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (e) {
|
|
456
|
+
console.error('\nTest suite failed:', e);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
runTests();
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for request/response flow and duplicate prevention.
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
// Simple test runner (matching existing pattern)
|
|
7
|
+
let passed = 0;
|
|
8
|
+
let failed = 0;
|
|
9
|
+
function test(name, fn) {
|
|
10
|
+
return (async () => {
|
|
11
|
+
try {
|
|
12
|
+
await fn();
|
|
13
|
+
console.log(` ✓ ${name}`);
|
|
14
|
+
passed++;
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
18
|
+
console.log(` ✗ ${name}`);
|
|
19
|
+
console.log(` ${msg}`);
|
|
20
|
+
failed++;
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
}
|
|
24
|
+
function assert(condition, message) {
|
|
25
|
+
if (!condition)
|
|
26
|
+
throw new Error(`Assertion failed: ${message}`);
|
|
27
|
+
}
|
|
28
|
+
// Mock paths for testing
|
|
29
|
+
const TEST_DIR = path.join(process.cwd(), 'test-data');
|
|
30
|
+
const TEST_PATHS = {
|
|
31
|
+
serviceQueue: path.join(TEST_DIR, 'service-queue.jsonl'),
|
|
32
|
+
walletDir: path.join(TEST_DIR, 'wallet'),
|
|
33
|
+
registration: path.join(TEST_DIR, 'registration.json'),
|
|
34
|
+
services: path.join(TEST_DIR, 'services.json'),
|
|
35
|
+
};
|
|
36
|
+
function setupTestEnv() {
|
|
37
|
+
// Setup test directory
|
|
38
|
+
if (fs.existsSync(TEST_DIR)) {
|
|
39
|
+
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
fs.mkdirSync(TEST_DIR, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
function cleanupTestEnv() {
|
|
44
|
+
if (fs.existsSync(TEST_DIR)) {
|
|
45
|
+
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function run() {
|
|
49
|
+
console.log('\nRequest/Response Flow Tests\n');
|
|
50
|
+
// ── Queue Cleanup Tests ──────────────────────────────────────────────
|
|
51
|
+
await test('cleanupServiceQueue removes old fulfilled entries', async () => {
|
|
52
|
+
setupTestEnv();
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
const oldTime = now - (3 * 60 * 60 * 1000); // 3 hours ago
|
|
55
|
+
const recentTime = now - (30 * 60 * 1000); // 30 minutes ago
|
|
56
|
+
// Add test entries
|
|
57
|
+
const entries = [
|
|
58
|
+
{ status: 'pending', requestId: 'pending-1', _ts: recentTime },
|
|
59
|
+
{ status: 'fulfilled', requestId: 'old-fulfilled', _ts: oldTime },
|
|
60
|
+
{ status: 'fulfilled', requestId: 'recent-fulfilled', _ts: recentTime },
|
|
61
|
+
{ status: 'rejected', requestId: 'old-rejected', _ts: oldTime }
|
|
62
|
+
];
|
|
63
|
+
const content = entries.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
64
|
+
fs.writeFileSync(TEST_PATHS.serviceQueue, content);
|
|
65
|
+
// Mock PATHS for cleanupServiceQueue
|
|
66
|
+
const originalPaths = await import('../scripts/config.js');
|
|
67
|
+
const mockPaths = { ...originalPaths.PATHS, serviceQueue: TEST_PATHS.serviceQueue };
|
|
68
|
+
// Temporarily replace PATHS
|
|
69
|
+
globalThis.mockPaths = mockPaths;
|
|
70
|
+
// Redefine cleanupServiceQueue with mocked paths
|
|
71
|
+
function mockCleanupServiceQueue(maxAgeMs = 24 * 60 * 60 * 1000, finalStatusMaxAgeMs = 2 * 60 * 60 * 1000) {
|
|
72
|
+
if (!fs.existsSync(TEST_PATHS.serviceQueue))
|
|
73
|
+
return;
|
|
74
|
+
const currentTime = Date.now();
|
|
75
|
+
const finalStatuses = ['fulfilled', 'rejected', 'delivery_failed', 'failed', 'error'];
|
|
76
|
+
const lines = fs.readFileSync(TEST_PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
|
|
77
|
+
const keptLines = [];
|
|
78
|
+
let removedCount = 0;
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
try {
|
|
81
|
+
const entry = JSON.parse(line);
|
|
82
|
+
const entryAge = currentTime - (entry._ts || 0);
|
|
83
|
+
// Always keep pending entries that aren't too old
|
|
84
|
+
if (entry.status === 'pending' && entryAge < maxAgeMs) {
|
|
85
|
+
keptLines.push(line);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Keep final status entries only if they're recent
|
|
89
|
+
if (finalStatuses.includes(entry.status) && entryAge < finalStatusMaxAgeMs) {
|
|
90
|
+
keptLines.push(line);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Remove this entry
|
|
94
|
+
removedCount++;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Keep malformed entries to avoid data loss
|
|
98
|
+
keptLines.push(line);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (removedCount > 0) {
|
|
102
|
+
fs.writeFileSync(TEST_PATHS.serviceQueue, keptLines.join('\n') + (keptLines.length ? '\n' : ''));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Run cleanup with 2 hour limit for final statuses
|
|
106
|
+
mockCleanupServiceQueue(24 * 60 * 60 * 1000, 2 * 60 * 60 * 1000);
|
|
107
|
+
// Check remaining entries
|
|
108
|
+
const lines = fs.readFileSync(TEST_PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
|
|
109
|
+
const remaining = lines.map(line => JSON.parse(line));
|
|
110
|
+
assert(remaining.length === 2, `Expected 2 remaining entries, got ${remaining.length}`);
|
|
111
|
+
assert(remaining.find(e => e.requestId === 'pending-1') !== undefined, 'Should keep recent pending');
|
|
112
|
+
assert(remaining.find(e => e.requestId === 'recent-fulfilled') !== undefined, 'Should keep recent fulfilled');
|
|
113
|
+
assert(remaining.find(e => e.requestId === 'old-fulfilled') === undefined, 'Should remove old fulfilled');
|
|
114
|
+
assert(remaining.find(e => e.requestId === 'old-rejected') === undefined, 'Should remove old rejected');
|
|
115
|
+
cleanupTestEnv();
|
|
116
|
+
});
|
|
117
|
+
await test('updateServiceQueueStatus updates request status atomically', async () => {
|
|
118
|
+
setupTestEnv();
|
|
119
|
+
// Add a test entry
|
|
120
|
+
const entry = {
|
|
121
|
+
status: 'pending',
|
|
122
|
+
requestId: 'test-request-456',
|
|
123
|
+
serviceId: 'test-service',
|
|
124
|
+
from: 'sender-key',
|
|
125
|
+
_ts: Date.now()
|
|
126
|
+
};
|
|
127
|
+
fs.writeFileSync(TEST_PATHS.serviceQueue, JSON.stringify(entry) + '\n');
|
|
128
|
+
// Mock updateServiceQueueStatus with test paths
|
|
129
|
+
function mockUpdateServiceQueueStatus(requestId, newStatus, additionalFields = {}) {
|
|
130
|
+
if (!fs.existsSync(TEST_PATHS.serviceQueue))
|
|
131
|
+
return false;
|
|
132
|
+
const lines = fs.readFileSync(TEST_PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
|
|
133
|
+
let updated = false;
|
|
134
|
+
const updatedLines = lines.map(line => {
|
|
135
|
+
try {
|
|
136
|
+
const entryData = JSON.parse(line);
|
|
137
|
+
if (entryData.requestId === requestId) {
|
|
138
|
+
updated = true;
|
|
139
|
+
return JSON.stringify({
|
|
140
|
+
...entryData,
|
|
141
|
+
status: newStatus,
|
|
142
|
+
...additionalFields,
|
|
143
|
+
updatedAt: Date.now()
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return line;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return line;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
if (updated) {
|
|
153
|
+
fs.writeFileSync(TEST_PATHS.serviceQueue, updatedLines.join('\n') + '\n');
|
|
154
|
+
}
|
|
155
|
+
return updated;
|
|
156
|
+
}
|
|
157
|
+
// Update status
|
|
158
|
+
const updated = mockUpdateServiceQueueStatus('test-request-456', 'fulfilled', {
|
|
159
|
+
fulfilledAt: Date.now(),
|
|
160
|
+
result: { message: 'success' }
|
|
161
|
+
});
|
|
162
|
+
assert(updated === true, 'Should return true for successful update');
|
|
163
|
+
// Verify update
|
|
164
|
+
const lines = fs.readFileSync(TEST_PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
|
|
165
|
+
const updatedEntry = JSON.parse(lines[0]);
|
|
166
|
+
assert(updatedEntry.status === 'fulfilled', 'Status should be updated to fulfilled');
|
|
167
|
+
assert(updatedEntry.requestId === 'test-request-456', 'Request ID should remain the same');
|
|
168
|
+
assert(updatedEntry.fulfilledAt !== undefined, 'Should have fulfilledAt timestamp');
|
|
169
|
+
assert(updatedEntry.updatedAt !== undefined, 'Should have updatedAt timestamp');
|
|
170
|
+
assert(updatedEntry.result?.message === 'success', 'Should have result data');
|
|
171
|
+
cleanupTestEnv();
|
|
172
|
+
});
|
|
173
|
+
await test('updateServiceQueueStatus returns false for non-existent request', async () => {
|
|
174
|
+
setupTestEnv();
|
|
175
|
+
// Mock updateServiceQueueStatus with test paths
|
|
176
|
+
function mockUpdateServiceQueueStatus(requestId, _newStatus) {
|
|
177
|
+
if (!fs.existsSync(TEST_PATHS.serviceQueue))
|
|
178
|
+
return false;
|
|
179
|
+
const lines = fs.readFileSync(TEST_PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
|
|
180
|
+
let updated = false;
|
|
181
|
+
lines.map(line => {
|
|
182
|
+
try {
|
|
183
|
+
const entry = JSON.parse(line);
|
|
184
|
+
if (entry.requestId === requestId) {
|
|
185
|
+
updated = true;
|
|
186
|
+
}
|
|
187
|
+
return line;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return line;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return updated;
|
|
194
|
+
}
|
|
195
|
+
const updated = mockUpdateServiceQueueStatus('non-existent-request', 'fulfilled');
|
|
196
|
+
assert(updated === false, 'Should return false for non-existent request');
|
|
197
|
+
cleanupTestEnv();
|
|
198
|
+
});
|
|
199
|
+
// ── Summary ──────────────────────────────────────────────────────────
|
|
200
|
+
console.log(`\n${passed} passed, ${failed} failed\n`);
|
|
201
|
+
if (failed > 0) {
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Run tests if this file is executed directly
|
|
206
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
207
|
+
run().catch(console.error);
|
|
208
|
+
}
|
|
209
|
+
export { run };
|