agentgui 1.0.906 → 1.0.907

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/test.js +70 -72
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.906",
3
+ "version": "1.0.907",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/test.js CHANGED
@@ -14,10 +14,12 @@ import { maskKey, buildSystemPrompt } from './lib/provider-config.js';
14
14
  import { buildBaseUrl, isRemoteRequest, encodeOAuthState, decodeOAuthState } from './lib/oauth-common.js';
15
15
  import { compareVersions } from './lib/tool-version-check.js';
16
16
  import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
17
+ import { createACPProtocolHandler } from './lib/acp-protocol.js';
18
+ import { sendJSON, compressAndSend, acceptsEncoding } from './lib/http-utils.js';
19
+ import { JsonlParser } from './lib/jsonl-parser.js';
17
20
  const require = createRequire(import.meta.url);
18
21
  let Database;
19
22
  try { Database = (await import('bun:sqlite')).default; } catch { Database = require('better-sqlite3'); }
20
-
21
23
  let passed = 0, failed = 0;
22
24
  const ok = (name, fn) => Promise.resolve().then(fn).then(
23
25
  () => { console.log(`ok — ${name}`); passed++; },
@@ -25,26 +27,26 @@ const ok = (name, fn) => Promise.resolve().then(fn).then(
25
27
  function inMemDb() {
26
28
  const db = new Database(':memory:');
27
29
  if (db.pragma) db.pragma('foreign_keys = ON'); else db.run('PRAGMA foreign_keys = ON');
28
- const origLog = console.log, origWarn = console.warn;
29
- console.log = () => {}; console.warn = () => {};
30
+ const oL = console.log, oW = console.warn; console.log = () => {}; console.warn = () => {};
30
31
  try { initSchema(db); migrateConversationColumns(db); migrateACPSchema(db); }
31
- finally { console.log = origLog; console.warn = origWarn; }
32
+ finally { console.log = oL; console.warn = oW; }
32
33
  const gid = (p) => `${p}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
33
34
  return { db, prep: (sql) => db.prepare(sql), gid };
34
35
  }
36
+ function mockRes() {
37
+ const r = { headers: {}, body: null, statusCode: 0 };
38
+ r.writeHead = (s, h) => { r.statusCode = s; r.headers = h; };
39
+ r.end = (b) => { r.body = b; };
40
+ return r;
41
+ }
35
42
  const run = async () => {
36
43
  await ok('codec: roundtrip + binary', () => {
37
- const obj = { a: 1, b: 'str', c: [1, 2, 3], d: { nested: true } };
38
- assert.deepEqual(decode(encode(obj)), obj);
39
- const round = decode(encode({ bin: Buffer.from([1, 2, 3, 4]) }));
40
- assert.deepEqual(Array.from(round.bin), [1, 2, 3, 4]);
44
+ assert.deepEqual(decode(encode({ a: 1, b: 'str', c: [1, 2, 3], d: { nested: true } })), { a: 1, b: 'str', c: [1, 2, 3], d: { nested: true } });
45
+ assert.deepEqual(Array.from(decode(encode({ bin: Buffer.from([1, 2, 3, 4]) })).bin), [1, 2, 3, 4]);
41
46
  });
42
-
43
47
  await ok('db: init schema creates conversations table', () => {
44
- const { db } = inMemDb();
45
- assert.ok(db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='conversations'").get());
48
+ assert.ok(inMemDb().db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='conversations'").get());
46
49
  });
47
-
48
50
  await ok('db-queries: createConversation round-trip', () => {
49
51
  const { db, prep, gid } = inMemDb();
50
52
  const q = createQueries(db, prep, gid);
@@ -52,7 +54,6 @@ await ok('db-queries: createConversation round-trip', () => {
52
54
  assert.equal(q.getConversation(c.id).title, 'Test');
53
55
  assert.equal(q.getConversation(c.id).status, 'active');
54
56
  });
55
-
56
57
  await ok('db-queries: archive + restore + streaming flag', () => {
57
58
  const { db, prep, gid } = inMemDb();
58
59
  const q = createQueries(db, prep, gid);
@@ -62,7 +63,6 @@ await ok('db-queries: archive + restore + streaming flag', () => {
62
63
  q.setIsStreaming(c.id, true); assert.equal(q.getIsStreaming(c.id), true);
63
64
  q.setIsStreaming(c.id, false); assert.equal(q.getIsStreaming(c.id), false);
64
65
  });
65
-
66
66
  await ok('acp-queries: thread crud + search', () => {
67
67
  const { db, prep, gid } = inMemDb();
68
68
  const q = createQueries(db, prep, gid);
@@ -73,13 +73,11 @@ await ok('acp-queries: thread crud + search', () => {
73
73
  q.createThread({ kind: 'b' });
74
74
  assert.equal(q.searchThreads({}).total, 2);
75
75
  });
76
-
77
76
  await ok('WsRouter: dispatch + 404 + error + legacy', async () => {
78
77
  const router = new WsRouter();
79
78
  router.handle('ping', async (p) => ({ pong: p.n }));
80
79
  router.handle('boom', async () => { throw Object.assign(new Error('kaboom'), { code: 422 }); });
81
- let legacy = null;
82
- router.onLegacy((m) => { legacy = m; });
80
+ let legacy = null; router.onLegacy((m) => { legacy = m; });
83
81
  const replies = [];
84
82
  const ws = { readyState: 1, send: (b) => replies.push(decode(b)), clientId: 'c' };
85
83
  await router.onMessage(ws, encode({ r: 1, m: 'ping', p: { n: 7 } }));
@@ -87,11 +85,8 @@ await ok('WsRouter: dispatch + 404 + error + legacy', async () => {
87
85
  await router.onMessage(ws, encode({ r: 3, m: 'boom', p: {} }));
88
86
  await router.onMessage(ws, encode({ type: 'subscribe', id: 'x' }));
89
87
  assert.deepEqual(replies[0], { r: 1, d: { pong: 7 } });
90
- assert.equal(replies[1].e.c, 404);
91
- assert.equal(replies[2].e.c, 422);
92
- assert.equal(legacy.type, 'subscribe');
88
+ assert.equal(replies[1].e.c, 404); assert.equal(replies[2].e.c, 422); assert.equal(legacy.type, 'subscribe');
93
89
  });
94
-
95
90
  await ok('machines: tool-install + execution + acp-server lifecycle', () => {
96
91
  assert.ok(tim.getMachineActors() instanceof Map);
97
92
  assert.equal(exm.snapshot('nonexistent-conv-id'), null);
@@ -102,99 +97,102 @@ await ok('machines: tool-install + execution + acp-server lifecycle', () => {
102
97
  assert.equal(asm.isHealthy('test-tool'), true);
103
98
  asm.stopAll();
104
99
  });
105
-
106
100
  await ok('workflow-plugin + agent-registry hermes', async () => {
107
101
  const wp = await import('./lib/plugins/workflow-plugin.js');
108
102
  assert.deepEqual(wp.default.dependencies, ['database']);
109
103
  const { registry } = await import('./lib/claude-runner-agents.js');
110
104
  const h = registry.get('hermes');
111
- assert.equal(h.protocol, 'acp');
112
- assert.deepEqual(h.buildArgs(), ['acp']);
105
+ assert.equal(h.protocol, 'acp'); assert.deepEqual(h.buildArgs(), ['acp']);
113
106
  });
114
-
115
107
  await ok('delete-all: soft-deletes + wipes related', () => {
116
108
  const { db, prep, gid } = inMemDb();
117
109
  const q = createQueries(db, prep, gid);
118
110
  const c1 = q.createConversation('claude-code', 'A');
119
111
  q.createConversation('claude-code', 'B');
120
- q.createSession(c1.id);
121
- q.createMessage(c1.id, 'user', 'hello');
122
- const origLog = console.log; console.log = () => {};
123
- try { q.deleteAllConversations(); } finally { console.log = origLog; }
124
- const rows = db.prepare('SELECT status, count(*) as c FROM conversations GROUP BY status').all();
125
- assert.deepEqual(rows, [{ status: 'deleted', c: 2 }]);
112
+ q.createSession(c1.id); q.createMessage(c1.id, 'user', 'hello');
113
+ const oL = console.log; console.log = () => {};
114
+ try { q.deleteAllConversations(); } finally { console.log = oL; }
115
+ assert.deepEqual(db.prepare('SELECT status, count(*) as c FROM conversations GROUP BY status').all(), [{ status: 'deleted', c: 2 }]);
126
116
  assert.equal(db.prepare('SELECT count(*) as c FROM messages').get().c, 0);
127
117
  assert.equal(q.getConversationsList().length, 0);
128
118
  });
129
-
130
119
  await ok('provider-config: maskKey + buildSystemPrompt', () => {
131
- assert.equal(maskKey(''), '****');
132
- assert.equal(maskKey('short'), '****');
133
- assert.equal(maskKey('sk-abcd1234efgh'), '****efgh');
120
+ assert.equal(maskKey(''), '****'); assert.equal(maskKey('short'), '****'); assert.equal(maskKey('sk-abcd1234efgh'), '****efgh');
134
121
  assert.equal(buildSystemPrompt('claude-code'), '');
135
122
  assert.equal(buildSystemPrompt('opencode', 'sonnet'), 'Use opencode subagent for all tasks. Model: sonnet.');
136
123
  assert.equal(buildSystemPrompt('foo-·-bar', null, 'sub'), 'Use foo subagent for all tasks. Subagent: sub.');
137
124
  });
138
-
139
125
  await ok('oauth-common: state codec + url helpers', () => {
140
- const enc = encodeOAuthState('csrf-tok', 'https://relay.test/cb');
141
- const dec = decodeOAuthState(enc);
142
- assert.equal(dec.csrfToken, 'csrf-tok');
143
- assert.equal(dec.relayUrl, 'https://relay.test/cb');
144
- const fallback = decodeOAuthState('not-base64-json');
145
- assert.equal(fallback.csrfToken, 'not-base64-json');
146
- assert.equal(fallback.relayUrl, null);
126
+ const dec = decodeOAuthState(encodeOAuthState('csrf-tok', 'https://relay.test/cb'));
127
+ assert.equal(dec.csrfToken, 'csrf-tok'); assert.equal(dec.relayUrl, 'https://relay.test/cb');
128
+ const fb = decodeOAuthState('not-base64-json');
129
+ assert.equal(fb.csrfToken, 'not-base64-json'); assert.equal(fb.relayUrl, null);
147
130
  const reqRemote = { headers: { 'x-forwarded-host': 'a.com', 'x-forwarded-proto': 'https' }, socket: {} };
148
- assert.equal(buildBaseUrl(reqRemote, 3000), 'https://a.com');
149
- assert.equal(isRemoteRequest(reqRemote), true);
131
+ assert.equal(buildBaseUrl(reqRemote, 3000), 'https://a.com'); assert.equal(isRemoteRequest(reqRemote), true);
150
132
  assert.equal(buildBaseUrl({ headers: {}, socket: {} }, 3000), 'http://127.0.0.1:3000');
151
133
  assert.equal(isRemoteRequest({ headers: {} }), false);
152
134
  });
153
-
154
135
  await ok('tool-version-check: compareVersions', () => {
155
- assert.equal(compareVersions('1.0.0', '1.0.1'), true);
156
- assert.equal(compareVersions('1.0.1', '1.0.0'), false);
157
- assert.equal(compareVersions('1.0.0', '1.0.0'), false);
158
- assert.equal(compareVersions('1.2', '1.2.1'), true);
136
+ assert.equal(compareVersions('1.0.0', '1.0.1'), true); assert.equal(compareVersions('1.0.1', '1.0.0'), false);
137
+ assert.equal(compareVersions('1.0.0', '1.0.0'), false); assert.equal(compareVersions('1.2', '1.2.1'), true);
159
138
  assert.equal(compareVersions('2.0.0', '1.99.99'), false);
160
- assert.equal(compareVersions(null, '1.0.0'), false);
161
- assert.equal(compareVersions('1.0.0', null), false);
139
+ assert.equal(compareVersions(null, '1.0.0'), false); assert.equal(compareVersions('1.0.0', null), false);
162
140
  });
163
-
164
141
  await ok('agent-descriptors: initialize + cache', () => {
165
- const n = initializeDescriptors([{ id: 'claude-code', name: 'Claude Code', path: '/x' }]);
166
- assert.equal(n, 1);
142
+ assert.equal(initializeDescriptors([{ id: 'claude-code', name: 'Claude Code', path: '/x' }]), 1);
167
143
  const d = getAgentDescriptor('claude-code');
168
144
  assert.equal(d.metadata.ref.name, 'Claude Code');
169
- assert.ok(d.specs.input.properties.model);
170
- assert.ok(d.specs.thread_state.properties.sessionId);
145
+ assert.ok(d.specs.input.properties.model); assert.ok(d.specs.thread_state.properties.sessionId);
171
146
  assert.equal(getAgentDescriptor('nope'), null);
172
147
  });
173
-
174
148
  await ok('ws-optimizer: high-priority flushes immediately', () => {
175
- const opt = new WSOptimizer();
176
- const sent = [];
149
+ const opt = new WSOptimizer(); const sent = [];
177
150
  const ws = { readyState: 1, clientId: 'c1', send: (b) => sent.push(decode(b)) };
178
151
  opt.sendToClient(ws, { type: 'streaming_start', id: 1 });
179
- assert.equal(sent.length, 1);
180
- assert.equal(sent[0].type, 'streaming_start');
181
- opt.removeClient(ws);
182
- assert.equal(opt.getStats().clients, 0);
152
+ assert.equal(sent.length, 1); assert.equal(sent[0].type, 'streaming_start');
153
+ opt.removeClient(ws); assert.equal(opt.getStats().clients, 0);
183
154
  });
184
-
185
155
  await ok('ws-optimizer: low-priority batches via timer', async () => {
186
- const opt = new WSOptimizer();
187
- const sent = [];
156
+ const opt = new WSOptimizer(); const sent = [];
188
157
  const ws = { readyState: 1, clientId: 'c2', latencyTier: 'excellent', send: (b) => sent.push(decode(b)) };
189
- opt.sendToClient(ws, { type: 'tts_audio', n: 1 });
190
- opt.sendToClient(ws, { type: 'tts_audio', n: 2 });
158
+ opt.sendToClient(ws, { type: 'tts_audio', n: 1 }); opt.sendToClient(ws, { type: 'tts_audio', n: 2 });
191
159
  assert.equal(sent.length, 0);
192
160
  await new Promise(r => setTimeout(r, 40));
193
- assert.equal(sent.length, 1);
194
- assert.equal(sent[0].length, 2);
161
+ assert.equal(sent.length, 1); assert.equal(sent[0].length, 2);
195
162
  opt.removeClient(ws);
196
163
  });
197
-
164
+ await ok('acp-protocol: session/update + result + error mapping', () => {
165
+ const h = createACPProtocolHandler(); const ctx = { sessionId: 's1' };
166
+ assert.equal(h(null, ctx), null);
167
+ const chunk = h({ method: 'session/update', params: { sessionId: 's1', update: { sessionUpdate: 'agent_message_chunk', content: 'hi' } } }, ctx);
168
+ assert.equal(chunk.type, 'assistant'); assert.equal(chunk.message.content[0].text, 'hi');
169
+ const tc = h({ method: 'session/update', params: { sessionId: 's1', update: { sessionUpdate: 'tool_call', toolCallId: 'x', kind: 'read', rawInput: { p: 1 } } } }, ctx);
170
+ assert.equal(tc.message.content[0].type, 'tool_use');
171
+ assert.equal(h({ id: 1, result: { stopReason: 'end_turn', usage: {} } }, ctx).type, 'result');
172
+ assert.equal(h({ method: 'error', error: { message: 'x' } }, ctx).type, 'error');
173
+ assert.equal(h({ method: 'session/update', params: { sessionId: 's1', update: { sessionUpdate: 'unknown' } } }, ctx), null);
174
+ });
175
+ await ok('http-utils: sendJSON + compressAndSend size threshold', () => {
176
+ const req = { headers: { 'accept-encoding': 'gzip, deflate' } };
177
+ assert.equal(acceptsEncoding(req, 'gzip'), true); assert.equal(acceptsEncoding({ headers: {} }, 'gzip'), false);
178
+ const small = mockRes(); sendJSON(req, small, 200, { ok: 1 });
179
+ assert.equal(small.statusCode, 200); assert.equal(small.headers['Content-Type'], 'application/json');
180
+ assert.ok(!small.headers['Content-Encoding']);
181
+ const big = mockRes(); compressAndSend(req, big, 200, 'text/plain', 'x'.repeat(2000));
182
+ assert.equal(big.headers['Content-Encoding'], 'gzip');
183
+ const noGz = mockRes(); compressAndSend({ headers: {} }, noGz, 200, 'text/html', 'y'.repeat(2000));
184
+ assert.equal(noGz.headers['Cache-Control'], 'no-store'); assert.ok(!noGz.headers['Content-Encoding']);
185
+ });
186
+ await ok('jsonl-parser: register + remove + clear', () => {
187
+ const calls = []; const fakeQ = { getConversationByClaudeSessionId: () => null };
188
+ const p = new JsonlParser({ broadcastSync: (m) => calls.push(m), queries: fakeQ });
189
+ p.registerSession('claude-1', 'conv-1', 'db-sess-1');
190
+ assert.equal(p._convMap.get('claude-1'), 'conv-1'); assert.equal(p._sessions.get('claude-1'), 'db-sess-1');
191
+ p.removeSid('claude-1');
192
+ assert.equal(p._convMap.has('claude-1'), false); assert.equal(p._sessions.has('claude-1'), false);
193
+ p.registerSession('s2', 'c2', 'd2'); p.clear();
194
+ assert.equal(p._convMap.size, 0); assert.equal(p._sessions.size, 0); assert.equal(calls.length, 0);
195
+ });
198
196
  console.log(`\n${passed} passed, ${failed} failed`);
199
197
  process.exit(failed === 0 ? 0 : 1);
200
198
  }; run();