a2acalling 0.6.73 → 0.6.74
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/.a2a-manifest.json +2 -2
- package/ARCHITECTURE.md +29 -16
- package/CONVENTIONS.md +30 -6
- package/biome.json +27 -0
- package/docs/assessments/2026-02-27-google-a2a-protocol-assessment.md +292 -0
- package/docs/plans/2026-03-01-a2a-68-openclaw-integration-tests.md +676 -0
- package/docs/plans/2026-03-01-a2a-77-invoke-security-tests.md +661 -0
- package/eslint.config.js +16 -0
- package/knip.json +17 -0
- package/package.json +11 -2
- package/scripts/install-openclaw.js +3 -5
- package/src/lib/agent-card.js +111 -0
- package/src/lib/client.js +290 -49
- package/src/lib/conversations.js +2 -0
- package/src/lib/local-request.js +69 -0
- package/src/lib/logger.js +2 -0
- package/src/lib/runtime-adapter.js +41 -1
- package/src/routes/a2a.js +393 -66
- package/src/routes/dashboard.js +1 -27
- package/src/server.js +19 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
# A2A-68: Unit Tests for openclaw-integration.js
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add ~20 unit tests covering all 6 exports of `src/lib/openclaw-integration.js` — the critical untested summary-generation module.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Pure function tests use temp directories with `USER.md` / `MEMORY.md` fixtures. HTTP summarizer tests use ephemeral `http.createServer` on port 0. Exec summarizer tests monkey-patch `require('child_process').execSync`. All tests follow the project's zero-dependency custom runner pattern.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js, custom test runner (`test/run.js`), `node:fs`, `node:http`, `node:child_process`
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Shared Utilities (top of test file)
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const http = require('http');
|
|
20
|
+
|
|
21
|
+
function freshModule() {
|
|
22
|
+
delete require.cache[require.resolve('../../src/lib/openclaw-integration')];
|
|
23
|
+
delete require.cache[require.resolve('../../src/lib/summary-prompt')];
|
|
24
|
+
return require('../../src/lib/openclaw-integration');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeTmpDir() {
|
|
28
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'a2a-oc-'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function rmTmpDir(dir) {
|
|
32
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mockExecSync(mockFn) {
|
|
36
|
+
const cp = require('child_process');
|
|
37
|
+
const original = cp.execSync;
|
|
38
|
+
cp.execSync = mockFn;
|
|
39
|
+
return () => { cp.execSync = original; };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function startMockServer(handler) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const server = http.createServer(handler);
|
|
45
|
+
server.listen(0, '127.0.0.1', () => {
|
|
46
|
+
resolve({
|
|
47
|
+
port: server.address().port,
|
|
48
|
+
close() { return new Promise((r) => server.close(r)); }
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### Task 1: Scaffold test file + loadOwnerContext — empty workspace
|
|
58
|
+
|
|
59
|
+
**Files:**
|
|
60
|
+
- Create: `test/unit/openclaw-integration.test.js`
|
|
61
|
+
|
|
62
|
+
**Step 1: Create test file with module header, shared utils, and first test**
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
/**
|
|
66
|
+
* A2A-68: Unit tests for openclaw-integration.js
|
|
67
|
+
*
|
|
68
|
+
* Covers: loadOwnerContext, buildSummaryPrompt, createExecSummarizer,
|
|
69
|
+
* createHttpSummarizer, createSessionSummarizer, createAutoSummarizer.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
module.exports = function (test, assert, helpers) {
|
|
73
|
+
|
|
74
|
+
// ── Shared utilities ──────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
const fs = require('fs');
|
|
77
|
+
const path = require('path');
|
|
78
|
+
const os = require('os');
|
|
79
|
+
const http = require('http');
|
|
80
|
+
|
|
81
|
+
function freshModule() {
|
|
82
|
+
delete require.cache[require.resolve('../../src/lib/openclaw-integration')];
|
|
83
|
+
delete require.cache[require.resolve('../../src/lib/summary-prompt')];
|
|
84
|
+
return require('../../src/lib/openclaw-integration');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function makeTmpDir() {
|
|
88
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'a2a-oc-'));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function rmTmpDir(dir) {
|
|
92
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function mockExecSync(mockFn) {
|
|
96
|
+
const cp = require('child_process');
|
|
97
|
+
const original = cp.execSync;
|
|
98
|
+
cp.execSync = mockFn;
|
|
99
|
+
return () => { cp.execSync = original; };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function startMockServer(handler) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
const server = http.createServer(handler);
|
|
105
|
+
server.listen(0, '127.0.0.1', () => {
|
|
106
|
+
resolve({
|
|
107
|
+
port: server.address().port,
|
|
108
|
+
close() { return new Promise((r) => server.close(r)); }
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── loadOwnerContext ──────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
test('loadOwnerContext returns empty context when workspace has no USER.md or memory/', () => {
|
|
117
|
+
const dir = makeTmpDir();
|
|
118
|
+
try {
|
|
119
|
+
const { loadOwnerContext } = freshModule();
|
|
120
|
+
const ctx = loadOwnerContext(dir);
|
|
121
|
+
assert.deepEqual(ctx.goals, []);
|
|
122
|
+
assert.deepEqual(ctx.interests, []);
|
|
123
|
+
assert.equal(ctx.user, null);
|
|
124
|
+
assert.equal(ctx.memory, null);
|
|
125
|
+
assert.equal(ctx.context, '');
|
|
126
|
+
} finally {
|
|
127
|
+
rmTmpDir(dir);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Step 2: Run test to verify it passes**
|
|
135
|
+
|
|
136
|
+
Run: `node test/run.js --filter openclaw-integration`
|
|
137
|
+
Expected: 1 passing
|
|
138
|
+
|
|
139
|
+
**Step 3: Commit**
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
git add test/unit/openclaw-integration.test.js
|
|
143
|
+
git commit -m "test(a2a-68): scaffold openclaw-integration tests, first loadOwnerContext test"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### Task 2: loadOwnerContext — goals, interests, tierGoals, memory (5 tests)
|
|
149
|
+
|
|
150
|
+
**Files:**
|
|
151
|
+
- Modify: `test/unit/openclaw-integration.test.js`
|
|
152
|
+
|
|
153
|
+
**Step 1: Add 5 remaining loadOwnerContext tests**
|
|
154
|
+
|
|
155
|
+
Add after the first test, inside the `// ── loadOwnerContext ──` section:
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
test('loadOwnerContext extracts goals from ## Goals section in USER.md', () => {
|
|
159
|
+
const dir = makeTmpDir();
|
|
160
|
+
fs.writeFileSync(path.join(dir, 'USER.md'),
|
|
161
|
+
'# About Me\n\n## Goals\n- Learn Rust\n- Ship v2\n\n## Other\nstuff');
|
|
162
|
+
try {
|
|
163
|
+
const { loadOwnerContext } = freshModule();
|
|
164
|
+
const ctx = loadOwnerContext(dir);
|
|
165
|
+
assert.deepEqual(ctx.goals, ['Learn Rust', 'Ship v2']);
|
|
166
|
+
} finally {
|
|
167
|
+
rmTmpDir(dir);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('loadOwnerContext extracts interests from ## Interests section', () => {
|
|
172
|
+
const dir = makeTmpDir();
|
|
173
|
+
fs.writeFileSync(path.join(dir, 'USER.md'),
|
|
174
|
+
'# Me\n\n## Interests\n- Photography\n* Cooking\n\n## Next\nblah');
|
|
175
|
+
try {
|
|
176
|
+
const { loadOwnerContext } = freshModule();
|
|
177
|
+
const ctx = loadOwnerContext(dir);
|
|
178
|
+
assert.deepEqual(ctx.interests, ['Photography', 'Cooking']);
|
|
179
|
+
} finally {
|
|
180
|
+
rmTmpDir(dir);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('loadOwnerContext tierGoals option takes priority over USER.md goals', () => {
|
|
185
|
+
const dir = makeTmpDir();
|
|
186
|
+
fs.writeFileSync(path.join(dir, 'USER.md'), '## Goals\n- From USER.md');
|
|
187
|
+
try {
|
|
188
|
+
const { loadOwnerContext } = freshModule();
|
|
189
|
+
const ctx = loadOwnerContext(dir, { tierGoals: ['Tier goal A', 'Tier goal B'] });
|
|
190
|
+
assert.deepEqual(ctx.goals, ['Tier goal A', 'Tier goal B']);
|
|
191
|
+
} finally {
|
|
192
|
+
rmTmpDir(dir);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('loadOwnerContext loads MEMORY.md and up to 3 memory files', () => {
|
|
197
|
+
const dir = makeTmpDir();
|
|
198
|
+
fs.writeFileSync(path.join(dir, 'MEMORY.md'), 'Main memory content');
|
|
199
|
+
const memDir = path.join(dir, 'memory');
|
|
200
|
+
fs.mkdirSync(memDir);
|
|
201
|
+
fs.writeFileSync(path.join(memDir, 'a.md'), 'Memory A');
|
|
202
|
+
fs.writeFileSync(path.join(memDir, 'b.md'), 'Memory B');
|
|
203
|
+
fs.writeFileSync(path.join(memDir, 'c.md'), 'Memory C');
|
|
204
|
+
fs.writeFileSync(path.join(memDir, 'd.md'), 'Memory D');
|
|
205
|
+
try {
|
|
206
|
+
const { loadOwnerContext } = freshModule();
|
|
207
|
+
const ctx = loadOwnerContext(dir);
|
|
208
|
+
assert.ok(ctx.memory.includes('Main memory content'), 'includes MEMORY.md');
|
|
209
|
+
// sorted reverse alpha → d, c, b picked (3 cap); a excluded
|
|
210
|
+
assert.ok(ctx.memory.includes('Memory D'), 'includes d.md');
|
|
211
|
+
assert.ok(ctx.memory.includes('Memory C'), 'includes c.md');
|
|
212
|
+
assert.ok(ctx.memory.includes('Memory B'), 'includes b.md');
|
|
213
|
+
assert.ok(!ctx.memory.includes('Memory A'), 'a.md is capped out');
|
|
214
|
+
} finally {
|
|
215
|
+
rmTmpDir(dir);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('loadOwnerContext handles empty USER.md gracefully', () => {
|
|
220
|
+
const dir = makeTmpDir();
|
|
221
|
+
fs.writeFileSync(path.join(dir, 'USER.md'), '');
|
|
222
|
+
try {
|
|
223
|
+
const { loadOwnerContext } = freshModule();
|
|
224
|
+
const ctx = loadOwnerContext(dir);
|
|
225
|
+
assert.equal(ctx.user, '');
|
|
226
|
+
assert.deepEqual(ctx.goals, []);
|
|
227
|
+
assert.deepEqual(ctx.interests, []);
|
|
228
|
+
} finally {
|
|
229
|
+
rmTmpDir(dir);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Step 2: Run tests**
|
|
235
|
+
|
|
236
|
+
Run: `node test/run.js --filter openclaw-integration`
|
|
237
|
+
Expected: 6 passing
|
|
238
|
+
|
|
239
|
+
**Step 3: Commit**
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
git add test/unit/openclaw-integration.test.js
|
|
243
|
+
git commit -m "test(a2a-68): loadOwnerContext — goals, interests, tierGoals, memory, empty USER.md"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### Task 3: buildSummaryPrompt (3 tests)
|
|
249
|
+
|
|
250
|
+
**Files:**
|
|
251
|
+
- Modify: `test/unit/openclaw-integration.test.js`
|
|
252
|
+
|
|
253
|
+
**Step 1: Add buildSummaryPrompt tests**
|
|
254
|
+
|
|
255
|
+
Add after the loadOwnerContext section:
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
// ── buildSummaryPrompt ────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
test('buildSummaryPrompt maps messages and ownerContext to unified prompt', () => {
|
|
261
|
+
const { buildSummaryPrompt } = freshModule();
|
|
262
|
+
const messages = [
|
|
263
|
+
{ direction: 'inbound', content: 'Hello from Alice' },
|
|
264
|
+
{ direction: 'outbound', content: 'Hi Alice, welcome' }
|
|
265
|
+
];
|
|
266
|
+
const ownerContext = { goals: ['Ship v2', 'Learn Rust'] };
|
|
267
|
+
const callerInfo = { name: 'Alice', owner: 'Alice Corp', context: 'Partnership chat' };
|
|
268
|
+
|
|
269
|
+
const prompt = buildSummaryPrompt(messages, ownerContext, callerInfo);
|
|
270
|
+
assert.type(prompt, 'string');
|
|
271
|
+
assert.ok(prompt.includes('[Alice]'), 'includes caller label');
|
|
272
|
+
assert.ok(prompt.includes('Hello from Alice'), 'includes inbound message');
|
|
273
|
+
assert.ok(prompt.includes('Hi Alice, welcome'), 'includes outbound message');
|
|
274
|
+
assert.ok(prompt.includes('Ship v2'), 'includes owner goals');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('buildSummaryPrompt handles missing callerInfo fields', () => {
|
|
278
|
+
const { buildSummaryPrompt } = freshModule();
|
|
279
|
+
const messages = [{ direction: 'inbound', content: 'Test message' }];
|
|
280
|
+
const ownerContext = { goals: [] };
|
|
281
|
+
|
|
282
|
+
const prompt = buildSummaryPrompt(messages, ownerContext);
|
|
283
|
+
assert.type(prompt, 'string');
|
|
284
|
+
assert.ok(prompt.includes('[Caller]'), 'falls back to default caller label');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('buildSummaryPrompt handles null ownerContext', () => {
|
|
288
|
+
const { buildSummaryPrompt } = freshModule();
|
|
289
|
+
const messages = [{ direction: 'inbound', content: 'Test' }];
|
|
290
|
+
|
|
291
|
+
const prompt = buildSummaryPrompt(messages, null, { name: 'Bob' });
|
|
292
|
+
assert.type(prompt, 'string');
|
|
293
|
+
assert.ok(prompt.includes('[Bob]'), 'includes caller name');
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Step 2: Run tests**
|
|
298
|
+
|
|
299
|
+
Run: `node test/run.js --filter openclaw-integration`
|
|
300
|
+
Expected: 9 passing
|
|
301
|
+
|
|
302
|
+
**Step 3: Commit**
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
git add test/unit/openclaw-integration.test.js
|
|
306
|
+
git commit -m "test(a2a-68): buildSummaryPrompt — mapping, missing callerInfo, null ownerContext"
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
### Task 4: createExecSummarizer (4 tests)
|
|
312
|
+
|
|
313
|
+
**Files:**
|
|
314
|
+
- Modify: `test/unit/openclaw-integration.test.js`
|
|
315
|
+
|
|
316
|
+
**Step 1: Add createExecSummarizer tests**
|
|
317
|
+
|
|
318
|
+
Add after the buildSummaryPrompt section:
|
|
319
|
+
|
|
320
|
+
```javascript
|
|
321
|
+
// ── createExecSummarizer ──────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
test('createExecSummarizer parses JSON response from CLI', async () => {
|
|
324
|
+
const dir = makeTmpDir();
|
|
325
|
+
const restore = mockExecSync(() => JSON.stringify({
|
|
326
|
+
summary: 'Call went well',
|
|
327
|
+
relevance: 'high'
|
|
328
|
+
}));
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const { createExecSummarizer } = freshModule();
|
|
332
|
+
const summarize = createExecSummarizer(dir);
|
|
333
|
+
const result = await summarize(
|
|
334
|
+
[{ direction: 'inbound', content: 'Hello' }],
|
|
335
|
+
{ name: 'Alice' }
|
|
336
|
+
);
|
|
337
|
+
assert.equal(result.summary, 'Call went well');
|
|
338
|
+
assert.equal(result.relevance, 'high');
|
|
339
|
+
} finally {
|
|
340
|
+
restore();
|
|
341
|
+
rmTmpDir(dir);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('createExecSummarizer falls back to raw text when no JSON in output', async () => {
|
|
346
|
+
const dir = makeTmpDir();
|
|
347
|
+
const restore = mockExecSync(() => 'Just plain text summary output');
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const { createExecSummarizer } = freshModule();
|
|
351
|
+
const summarize = createExecSummarizer(dir);
|
|
352
|
+
const result = await summarize([{ direction: 'inbound', content: 'Hello' }]);
|
|
353
|
+
assert.equal(result.summary, 'Just plain text summary output');
|
|
354
|
+
assert.equal(result.relevance, 'unknown');
|
|
355
|
+
} finally {
|
|
356
|
+
restore();
|
|
357
|
+
rmTmpDir(dir);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('createExecSummarizer returns { summary: null } on exec error', async () => {
|
|
362
|
+
const dir = makeTmpDir();
|
|
363
|
+
const restore = mockExecSync(() => { throw new Error('Command timed out'); });
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const { createExecSummarizer } = freshModule();
|
|
367
|
+
const summarize = createExecSummarizer(dir);
|
|
368
|
+
const result = await summarize([{ direction: 'inbound', content: 'Hello' }]);
|
|
369
|
+
assert.equal(result.summary, null);
|
|
370
|
+
} finally {
|
|
371
|
+
restore();
|
|
372
|
+
rmTmpDir(dir);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('createExecSummarizer cleans up temp file after execution', async () => {
|
|
377
|
+
const dir = makeTmpDir();
|
|
378
|
+
let capturedCmd = null;
|
|
379
|
+
const restore = mockExecSync((cmd) => {
|
|
380
|
+
capturedCmd = cmd;
|
|
381
|
+
return '{}';
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const { createExecSummarizer } = freshModule();
|
|
386
|
+
const summarize = createExecSummarizer(dir);
|
|
387
|
+
await summarize([{ direction: 'inbound', content: 'Hello' }]);
|
|
388
|
+
|
|
389
|
+
const tmpFileMatch = capturedCmd.match(/\/tmp\/a2a-summary-\d+\.txt/);
|
|
390
|
+
assert.ok(tmpFileMatch, 'command references tmp file');
|
|
391
|
+
assert.ok(!fs.existsSync(tmpFileMatch[0]), 'tmp file is cleaned up after call');
|
|
392
|
+
} finally {
|
|
393
|
+
restore();
|
|
394
|
+
rmTmpDir(dir);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**Step 2: Run tests**
|
|
400
|
+
|
|
401
|
+
Run: `node test/run.js --filter openclaw-integration`
|
|
402
|
+
Expected: 13 passing
|
|
403
|
+
|
|
404
|
+
**Step 3: Commit**
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
git add test/unit/openclaw-integration.test.js
|
|
408
|
+
git commit -m "test(a2a-68): createExecSummarizer — JSON parse, raw text fallback, error, cleanup"
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
### Task 5: createHttpSummarizer + createSessionSummarizer (4 tests)
|
|
414
|
+
|
|
415
|
+
**Files:**
|
|
416
|
+
- Modify: `test/unit/openclaw-integration.test.js`
|
|
417
|
+
|
|
418
|
+
**Step 1: Add HTTP + session summarizer tests**
|
|
419
|
+
|
|
420
|
+
Add after the createExecSummarizer section:
|
|
421
|
+
|
|
422
|
+
```javascript
|
|
423
|
+
// ── createHttpSummarizer ──────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
test('createHttpSummarizer sends POST and resolves with parsed JSON', async () => {
|
|
426
|
+
let receivedBody = null;
|
|
427
|
+
const srv = await startMockServer((req, res) => {
|
|
428
|
+
let data = '';
|
|
429
|
+
req.on('data', c => data += c);
|
|
430
|
+
req.on('end', () => {
|
|
431
|
+
receivedBody = JSON.parse(data);
|
|
432
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
433
|
+
res.end(JSON.stringify({ summary: 'HTTP summary result' }));
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
const { createHttpSummarizer } = freshModule();
|
|
439
|
+
const summarize = createHttpSummarizer(`http://127.0.0.1:${srv.port}/api/summarize`);
|
|
440
|
+
const result = await summarize(
|
|
441
|
+
[{ direction: 'inbound', content: 'Hello' }],
|
|
442
|
+
{ name: 'Alice' }
|
|
443
|
+
);
|
|
444
|
+
assert.equal(result.summary, 'HTTP summary result');
|
|
445
|
+
assert.ok(receivedBody.prompt, 'sent prompt in body');
|
|
446
|
+
assert.ok(receivedBody.messages, 'sent messages in body');
|
|
447
|
+
assert.ok(receivedBody.callerInfo, 'sent callerInfo in body');
|
|
448
|
+
} finally {
|
|
449
|
+
await srv.close();
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test('createHttpSummarizer resolves { summary: null } on connection error', async () => {
|
|
454
|
+
const { createHttpSummarizer } = freshModule();
|
|
455
|
+
const summarize = createHttpSummarizer('http://127.0.0.1:1/api/summarize');
|
|
456
|
+
const result = await summarize([{ direction: 'inbound', content: 'Hello' }]);
|
|
457
|
+
assert.equal(result.summary, null);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// ── createSessionSummarizer ───────────────────────────────────
|
|
461
|
+
|
|
462
|
+
test('createSessionSummarizer sends POST with auth header to gateway', async () => {
|
|
463
|
+
let receivedHeaders = null;
|
|
464
|
+
let receivedPath = null;
|
|
465
|
+
const srv = await startMockServer((req, res) => {
|
|
466
|
+
receivedHeaders = req.headers;
|
|
467
|
+
receivedPath = req.url;
|
|
468
|
+
let data = '';
|
|
469
|
+
req.on('data', c => data += c);
|
|
470
|
+
req.on('end', () => {
|
|
471
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
472
|
+
res.end(JSON.stringify({ summary: { headline: 'Session summary' } }));
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
const { createSessionSummarizer } = freshModule();
|
|
478
|
+
const summarize = createSessionSummarizer(
|
|
479
|
+
`http://127.0.0.1:${srv.port}`,
|
|
480
|
+
'test-gateway-token'
|
|
481
|
+
);
|
|
482
|
+
const result = await summarize(
|
|
483
|
+
[{ direction: 'inbound', content: 'Hello' }],
|
|
484
|
+
{ name: 'Bob' }
|
|
485
|
+
);
|
|
486
|
+
assert.equal(receivedPath, '/api/internal/summarize');
|
|
487
|
+
assert.equal(receivedHeaders.authorization, 'Bearer test-gateway-token');
|
|
488
|
+
assert.equal(result.headline, 'Session summary');
|
|
489
|
+
} finally {
|
|
490
|
+
await srv.close();
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test('createSessionSummarizer resolves { summary: null } on connection error', async () => {
|
|
495
|
+
const { createSessionSummarizer } = freshModule();
|
|
496
|
+
const summarize = createSessionSummarizer('http://127.0.0.1:1', 'token');
|
|
497
|
+
const result = await summarize([{ direction: 'inbound', content: 'Hello' }]);
|
|
498
|
+
assert.equal(result.summary, null);
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Step 2: Run tests**
|
|
503
|
+
|
|
504
|
+
Run: `node test/run.js --filter openclaw-integration`
|
|
505
|
+
Expected: 17 passing
|
|
506
|
+
|
|
507
|
+
**Step 3: Commit**
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
git add test/unit/openclaw-integration.test.js
|
|
511
|
+
git commit -m "test(a2a-68): HTTP + session summarizers — payload, success, connection error"
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
### Task 6: createAutoSummarizer (3 tests)
|
|
517
|
+
|
|
518
|
+
**Files:**
|
|
519
|
+
- Modify: `test/unit/openclaw-integration.test.js`
|
|
520
|
+
|
|
521
|
+
**Step 1: Add createAutoSummarizer tests and closing brace**
|
|
522
|
+
|
|
523
|
+
Add after the session summarizer section:
|
|
524
|
+
|
|
525
|
+
```javascript
|
|
526
|
+
// ── createAutoSummarizer ──────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
test('createAutoSummarizer selects session summarizer when gatewayUrl is set', async () => {
|
|
529
|
+
let requestPath = null;
|
|
530
|
+
const srv = await startMockServer((req, res) => {
|
|
531
|
+
requestPath = req.url;
|
|
532
|
+
let data = '';
|
|
533
|
+
req.on('data', c => data += c);
|
|
534
|
+
req.on('end', () => {
|
|
535
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
536
|
+
res.end(JSON.stringify({ summary: 'ok' }));
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
const { createAutoSummarizer } = freshModule();
|
|
542
|
+
const summarize = createAutoSummarizer({
|
|
543
|
+
gatewayUrl: `http://127.0.0.1:${srv.port}`
|
|
544
|
+
});
|
|
545
|
+
await summarize([{ direction: 'inbound', content: 'Hello' }]);
|
|
546
|
+
assert.equal(requestPath, '/api/internal/summarize');
|
|
547
|
+
} finally {
|
|
548
|
+
await srv.close();
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test('createAutoSummarizer selects HTTP summarizer when summaryEndpoint is provided', async () => {
|
|
553
|
+
let requestPath = null;
|
|
554
|
+
const srv = await startMockServer((req, res) => {
|
|
555
|
+
requestPath = req.url;
|
|
556
|
+
let data = '';
|
|
557
|
+
req.on('data', c => data += c);
|
|
558
|
+
req.on('end', () => {
|
|
559
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
560
|
+
res.end(JSON.stringify({ summary: 'ok' }));
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
const { createAutoSummarizer } = freshModule();
|
|
566
|
+
const summarize = createAutoSummarizer({
|
|
567
|
+
summaryEndpoint: `http://127.0.0.1:${srv.port}/custom/summarize`
|
|
568
|
+
});
|
|
569
|
+
await summarize([{ direction: 'inbound', content: 'Hello' }]);
|
|
570
|
+
assert.equal(requestPath, '/custom/summarize');
|
|
571
|
+
} finally {
|
|
572
|
+
await srv.close();
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test('createAutoSummarizer falls back to exec summarizer when no gateway or endpoint', async () => {
|
|
577
|
+
const dir = makeTmpDir();
|
|
578
|
+
let execCalled = false;
|
|
579
|
+
const restore = mockExecSync(() => {
|
|
580
|
+
execCalled = true;
|
|
581
|
+
return JSON.stringify({ summary: 'exec result' });
|
|
582
|
+
});
|
|
583
|
+
const savedGateway = process.env.OPENCLAW_GATEWAY_URL;
|
|
584
|
+
delete process.env.OPENCLAW_GATEWAY_URL;
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
const { createAutoSummarizer } = freshModule();
|
|
588
|
+
const summarize = createAutoSummarizer({ workspaceDir: dir });
|
|
589
|
+
const result = await summarize([{ direction: 'inbound', content: 'Hello' }]);
|
|
590
|
+
assert.ok(execCalled, 'execSync was called');
|
|
591
|
+
assert.equal(result.summary, 'exec result');
|
|
592
|
+
} finally {
|
|
593
|
+
restore();
|
|
594
|
+
if (savedGateway) process.env.OPENCLAW_GATEWAY_URL = savedGateway;
|
|
595
|
+
else delete process.env.OPENCLAW_GATEWAY_URL;
|
|
596
|
+
rmTmpDir(dir);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**Step 2: Run tests**
|
|
602
|
+
|
|
603
|
+
Run: `node test/run.js --filter openclaw-integration`
|
|
604
|
+
Expected: 20 passing
|
|
605
|
+
|
|
606
|
+
**Step 3: Commit**
|
|
607
|
+
|
|
608
|
+
```bash
|
|
609
|
+
git add test/unit/openclaw-integration.test.js
|
|
610
|
+
git commit -m "test(a2a-68): createAutoSummarizer — session/HTTP/exec selection"
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
### Task 7: Quality gate
|
|
616
|
+
|
|
617
|
+
**Step 1: Run full test suite**
|
|
618
|
+
|
|
619
|
+
Run: `npm test`
|
|
620
|
+
Expected: All existing tests + 20 new tests pass, 0 failures
|
|
621
|
+
|
|
622
|
+
**Step 2: Run biome lint**
|
|
623
|
+
|
|
624
|
+
Run: `npx @biomejs/biome check test/unit/openclaw-integration.test.js`
|
|
625
|
+
Expected: No new errors (warnings OK if pre-existing)
|
|
626
|
+
|
|
627
|
+
**Step 3: Run eslint**
|
|
628
|
+
|
|
629
|
+
Run: `npx eslint test/unit/openclaw-integration.test.js`
|
|
630
|
+
Expected: 0 errors
|
|
631
|
+
|
|
632
|
+
**Step 4: Run knip**
|
|
633
|
+
|
|
634
|
+
Run: `npx knip`
|
|
635
|
+
Expected: No new unused exports
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
### Task 8: Final commit, push, PR, merge, update Linear
|
|
640
|
+
|
|
641
|
+
**Step 1: Commit any remaining changes**
|
|
642
|
+
|
|
643
|
+
```bash
|
|
644
|
+
git add test/unit/openclaw-integration.test.js
|
|
645
|
+
git commit -m "test(a2a-68): add unit tests for openclaw-integration.js (20 tests)"
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
**Step 2: Push and create PR**
|
|
649
|
+
|
|
650
|
+
```bash
|
|
651
|
+
git push -u origin feature/a2a-68
|
|
652
|
+
gh pr create --title "test(a2a-68): add unit tests for openclaw-integration.js" \
|
|
653
|
+
--body "$(cat <<'EOF'
|
|
654
|
+
## Summary
|
|
655
|
+
- Adds `test/unit/openclaw-integration.test.js` with 20 unit tests covering all 6 exports
|
|
656
|
+
- Tests use temp directories, ephemeral HTTP servers, and execSync monkey-patching
|
|
657
|
+
- Zero new dependencies
|
|
658
|
+
|
|
659
|
+
## Test plan
|
|
660
|
+
- [x] All 20 new tests pass
|
|
661
|
+
- [x] Full suite passes (`npm test`)
|
|
662
|
+
- [x] Biome + ESLint clean
|
|
663
|
+
- [x] Knip: no new unused exports
|
|
664
|
+
|
|
665
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
666
|
+
EOF
|
|
667
|
+
)"
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
**Step 3: Merge and update Linear**
|
|
671
|
+
|
|
672
|
+
```bash
|
|
673
|
+
gh pr merge --merge
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
Update Linear A2A-68 to Done.
|