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.
@@ -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.