@blocks-network/mcp-server 0.1.62 → 0.1.65

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,142 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { downloadArtifact } from '../src/tools.js';
3
+ import { makeFakeDeps, makeFakeClient, makeFakeSession } from './helpers.js';
4
+
5
+ describe('download_artifact', () => {
6
+ it('returns inline text content for a text artifact when savePath is omitted', async () => {
7
+ const session = makeFakeSession({
8
+ artifacts: [
9
+ {
10
+ fileName: 'output.json',
11
+ mimeType: 'application/json',
12
+ data: new TextEncoder().encode('{"ok":true}'),
13
+ },
14
+ ],
15
+ });
16
+ const client = makeFakeClient({
17
+ task: { taskId: 't1', state: 'completed' },
18
+ session,
19
+ });
20
+ const { deps } = makeFakeDeps({ client });
21
+
22
+ const res = await downloadArtifact(
23
+ { taskId: 't1', fileName: 'output.json' },
24
+ deps,
25
+ );
26
+
27
+ expect(client.connectMock).toHaveBeenCalledWith({ taskId: 't1' });
28
+ expect(res.isError).toBeUndefined();
29
+ expect(res.content[0].text).toContain('[artifact: output.json]');
30
+ expect(res.content[0].text).toContain('{"ok":true}');
31
+ expect(session.closeMock).toHaveBeenCalledOnce();
32
+ });
33
+
34
+ it('returns base64 content for binary artifacts when savePath is omitted', async () => {
35
+ const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
36
+ const session = makeFakeSession({
37
+ artifacts: [{ fileName: 'image.png', mimeType: 'image/png', data: bytes }],
38
+ });
39
+ const client = makeFakeClient({
40
+ task: { taskId: 't2', state: 'completed' },
41
+ session,
42
+ });
43
+ const { deps } = makeFakeDeps({ client });
44
+
45
+ const res = await downloadArtifact(
46
+ { taskId: 't2', fileName: 'image.png' },
47
+ deps,
48
+ );
49
+
50
+ expect(res.isError).toBeUndefined();
51
+ expect(res.content[0].text).toContain('image/png');
52
+ expect(res.content[0].text).toContain('base64');
53
+ expect(res.content[0].text).toContain(Buffer.from(bytes).toString('base64'));
54
+ });
55
+
56
+ it('writes to disk when savePath is provided', async () => {
57
+ const bytes = new TextEncoder().encode('hello');
58
+ const session = makeFakeSession({
59
+ artifacts: [{ fileName: 'note.txt', mimeType: 'text/plain', data: bytes }],
60
+ });
61
+ const client = makeFakeClient({
62
+ task: { taskId: 't3', state: 'completed' },
63
+ session,
64
+ });
65
+ const { deps, mocks } = makeFakeDeps({ client });
66
+
67
+ const res = await downloadArtifact(
68
+ { taskId: 't3', fileName: 'note.txt', savePath: 'out/note.txt' },
69
+ deps,
70
+ );
71
+
72
+ expect(res.isError).toBeUndefined();
73
+ expect(mocks.resolveSavePath).toHaveBeenCalledWith('out/note.txt');
74
+ expect(mocks.writeFile).toHaveBeenCalledWith('out/note.txt', bytes);
75
+ expect(res.content[0].text).toContain('Saved 5 bytes to out/note.txt');
76
+ });
77
+
78
+ it('returns an error when the requested artifact is not found', async () => {
79
+ const session = makeFakeSession({
80
+ artifacts: [
81
+ {
82
+ fileName: 'other.txt',
83
+ mimeType: 'text/plain',
84
+ data: new TextEncoder().encode('x'),
85
+ },
86
+ ],
87
+ });
88
+ const client = makeFakeClient({
89
+ task: { taskId: 't4', state: 'completed' },
90
+ session,
91
+ });
92
+ const { deps } = makeFakeDeps({ client });
93
+
94
+ const res = await downloadArtifact(
95
+ { taskId: 't4', fileName: 'missing.txt' },
96
+ deps,
97
+ );
98
+
99
+ expect(res.isError).toBe(true);
100
+ expect(res.content[0].text).toContain('"missing.txt" not found');
101
+ expect(res.content[0].text).toContain('other.txt');
102
+ expect(session.closeMock).toHaveBeenCalledOnce();
103
+ });
104
+
105
+ it('uses paid TaskClient for paid tasks', async () => {
106
+ const session = makeFakeSession({
107
+ artifacts: [
108
+ {
109
+ fileName: 'a.txt',
110
+ mimeType: 'text/plain',
111
+ data: new TextEncoder().encode('a'),
112
+ },
113
+ ],
114
+ });
115
+ const client = makeFakeClient({
116
+ task: { taskId: 't5', state: 'completed', billingMode: 'paid' },
117
+ session,
118
+ });
119
+ const { deps, mocks } = makeFakeDeps({ client });
120
+
121
+ await downloadArtifact({ taskId: 't5', fileName: 'a.txt' }, deps);
122
+
123
+ expect(mocks.getTaskClient).toHaveBeenCalledWith('free');
124
+ expect(mocks.getTaskClient).toHaveBeenCalledWith('paid');
125
+ });
126
+
127
+ it('returns an error when connect fails', async () => {
128
+ const client = makeFakeClient({
129
+ task: { taskId: 't6', state: 'completed' },
130
+ });
131
+ client.connectMock.mockRejectedValueOnce(new Error('connect refused'));
132
+ const { deps } = makeFakeDeps({ client });
133
+
134
+ const res = await downloadArtifact(
135
+ { taskId: 't6', fileName: 'x.txt' },
136
+ deps,
137
+ );
138
+
139
+ expect(res.isError).toBe(true);
140
+ expect(res.content[0].text).toContain('connect refused');
141
+ });
142
+ });
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { getAgentStatus } from '../src/tools.js';
3
+ import {
4
+ fetchAgentStatus,
5
+ type AgentStatusResponse,
6
+ } from '../src/agent-status.js';
7
+ import {
8
+ PROTOCOL_VERSION_HEADER,
9
+ CURRENT_PROTOCOL_VERSION,
10
+ } from '../src/protocol-headers.js';
11
+ import { makeFakeDeps } from './helpers.js';
12
+
13
+ function mockResponse(body: unknown, ok = true, status = 200): Response {
14
+ return { ok, status, json: async () => body } as Response;
15
+ }
16
+
17
+ describe('get_agent_status (handler)', () => {
18
+ it('forwards baseUrl, apiKey and agentNames to fetchAgentStatus and emits JSON', async () => {
19
+ const result: AgentStatusResponse = {
20
+ agents: {
21
+ alice: {
22
+ agentName: 'alice',
23
+ instances: [],
24
+ onlineCount: 2,
25
+ totalActiveTasks: 1,
26
+ taskCount: 1,
27
+ },
28
+ },
29
+ };
30
+ const { deps, mocks } = makeFakeDeps({
31
+ apiKey: 'bk_test',
32
+ agentStatusResult: result,
33
+ });
34
+
35
+ const res = await getAgentStatus({ agentNames: ['alice'] }, deps);
36
+
37
+ expect(mocks.fetchAgentStatus).toHaveBeenCalledWith({
38
+ baseUrl: 'http://api.test',
39
+ apiKey: 'bk_test',
40
+ agentNames: ['alice'],
41
+ });
42
+ const parsed = JSON.parse(res.content[0].text);
43
+ expect(parsed.agents.alice.onlineCount).toBe(2);
44
+ });
45
+ });
46
+
47
+ describe('fetchAgentStatus (HTTP helper)', () => {
48
+ it('builds /api/v1/agent-status URL with agentNames csv and protocol header', async () => {
49
+ const fetchImpl = vi
50
+ .fn()
51
+ .mockResolvedValue(mockResponse({ agents: {} }));
52
+
53
+ await fetchAgentStatus({
54
+ baseUrl: 'http://api.test/',
55
+ agentNames: ['alice', 'bob'],
56
+ apiKey: 'bk_test',
57
+ fetchImpl: fetchImpl as unknown as typeof fetch,
58
+ });
59
+
60
+ const [url, init] = fetchImpl.mock.calls[0];
61
+ const parsed = new URL(String(url));
62
+ expect(parsed.pathname).toBe('/api/v1/agent-status');
63
+ expect(parsed.searchParams.get('agentNames')).toBe('alice,bob');
64
+ const headers = init?.headers as Record<string, string>;
65
+ expect(headers[PROTOCOL_VERSION_HEADER]).toBe(CURRENT_PROTOCOL_VERSION);
66
+ expect(headers['Authorization']).toBe('Bearer bk_test');
67
+ });
68
+
69
+ it('rejects empty input', async () => {
70
+ await expect(
71
+ fetchAgentStatus({
72
+ baseUrl: 'http://api.test',
73
+ agentNames: [],
74
+ }),
75
+ ).rejects.toThrow(/at least one/);
76
+ });
77
+
78
+ it('rejects more than 50 names', async () => {
79
+ const names = Array.from({ length: 51 }, (_, i) => `a${i}`);
80
+ await expect(
81
+ fetchAgentStatus({
82
+ baseUrl: 'http://api.test',
83
+ agentNames: names,
84
+ }),
85
+ ).rejects.toThrow(/at most 50/);
86
+ });
87
+
88
+ it('rejects names with invalid characters', async () => {
89
+ await expect(
90
+ fetchAgentStatus({
91
+ baseUrl: 'http://api.test',
92
+ agentNames: ['has-a-hyphen'],
93
+ }),
94
+ ).rejects.toThrow(/Invalid agent name/);
95
+ });
96
+
97
+ it('throws on non-OK responses', async () => {
98
+ const fetchImpl = vi.fn().mockResolvedValue(mockResponse({}, false, 500));
99
+
100
+ await expect(
101
+ fetchAgentStatus({
102
+ baseUrl: 'http://api.test',
103
+ agentNames: ['alice'],
104
+ fetchImpl: fetchImpl as unknown as typeof fetch,
105
+ }),
106
+ ).rejects.toThrow('HTTP 500');
107
+ });
108
+ });
package/tests/helpers.ts CHANGED
@@ -89,6 +89,9 @@ export interface FakeClientConfig {
89
89
  export function makeFakeClient(cfg: FakeClientConfig = {}): TaskClientLike & {
90
90
  sendMessageMock: ReturnType<typeof vi.fn>;
91
91
  cancelTaskMock: ReturnType<typeof vi.fn>;
92
+ pauseTaskMock: ReturnType<typeof vi.fn>;
93
+ resumeTaskMock: ReturnType<typeof vi.fn>;
94
+ retryTaskMock: ReturnType<typeof vi.fn>;
92
95
  connectMock: ReturnType<typeof vi.fn>;
93
96
  listTasksMock: ReturnType<typeof vi.fn>;
94
97
  getTaskMock: ReturnType<typeof vi.fn>;
@@ -97,6 +100,9 @@ export function makeFakeClient(cfg: FakeClientConfig = {}): TaskClientLike & {
97
100
  const sendMessageMock = vi.fn().mockResolvedValue(session);
98
101
  const connectMock = vi.fn().mockResolvedValue(session);
99
102
  const cancelTaskMock = vi.fn().mockResolvedValue(undefined);
103
+ const pauseTaskMock = vi.fn().mockResolvedValue(undefined);
104
+ const resumeTaskMock = vi.fn().mockResolvedValue(undefined);
105
+ const retryTaskMock = vi.fn().mockResolvedValue(undefined);
100
106
  const listTasksMock = vi.fn().mockResolvedValue(
101
107
  cfg.listResult ?? { tasks: [], totalCount: 0 },
102
108
  );
@@ -105,11 +111,17 @@ export function makeFakeClient(cfg: FakeClientConfig = {}): TaskClientLike & {
105
111
  return {
106
112
  sendMessage: sendMessageMock,
107
113
  cancelTask: cancelTaskMock,
114
+ pauseTask: pauseTaskMock,
115
+ resumeTask: resumeTaskMock,
116
+ retryTask: retryTaskMock,
108
117
  connect: connectMock,
109
118
  listTasks: listTasksMock,
110
119
  getTask: getTaskMock,
111
120
  sendMessageMock,
112
121
  cancelTaskMock,
122
+ pauseTaskMock,
123
+ resumeTaskMock,
124
+ retryTaskMock,
113
125
  connectMock,
114
126
  listTasksMock,
115
127
  getTaskMock,
@@ -119,9 +131,13 @@ export function makeFakeClient(cfg: FakeClientConfig = {}): TaskClientLike & {
119
131
  export interface FakeDepsOverrides {
120
132
  baseUrl?: string;
121
133
  apiKey?: string;
134
+ orgId?: string;
122
135
  client?: TaskClientLike;
123
136
  agentEntry?: AgentEntryLike | null;
124
137
  listAgentsResult?: ListAgentsResult;
138
+ agentStatusResult?: { agents: Record<string, unknown> };
139
+ balance?: Record<string, unknown>;
140
+ topUpSession?: Record<string, unknown>;
125
141
  maxUploadBytes?: number;
126
142
  fileSize?: number;
127
143
  }
@@ -130,36 +146,76 @@ export function makeFakeDeps(overrides: FakeDepsOverrides = {}) {
130
146
  const client = overrides.client ?? makeFakeClient();
131
147
  const getBaseUrl = vi.fn().mockResolvedValue(overrides.baseUrl ?? 'http://api.test');
132
148
  const getApiKey = vi.fn().mockReturnValue(overrides.apiKey);
149
+ const getOrgId = vi.fn().mockReturnValue(overrides.orgId);
133
150
  const getTaskClient = vi.fn().mockResolvedValue(client);
134
151
  const getAgentByName = vi.fn().mockResolvedValue(overrides.agentEntry ?? null);
135
152
  const listAgents = vi
136
153
  .fn()
137
154
  .mockResolvedValue(overrides.listAgentsResult ?? { agents: [], totalCount: 0 });
155
+ const fetchAgentStatus = vi
156
+ .fn<ToolDeps['fetchAgentStatus']>()
157
+ .mockResolvedValue(
158
+ (overrides.agentStatusResult ?? { agents: {} }) as Awaited<
159
+ ReturnType<ToolDeps['fetchAgentStatus']>
160
+ >,
161
+ );
162
+ const getConsumerBalance = vi
163
+ .fn<ToolDeps['getConsumerBalance']>()
164
+ .mockResolvedValue(
165
+ (overrides.balance ?? {
166
+ balance: '0',
167
+ reservedBalance: '0',
168
+ availableBalance: '0',
169
+ updatedAt: '2026-05-28T00:00:00Z',
170
+ }) as Awaited<ReturnType<ToolDeps['getConsumerBalance']>>,
171
+ );
172
+ const createConsumerTopUp = vi
173
+ .fn<ToolDeps['createConsumerTopUp']>()
174
+ .mockResolvedValue(
175
+ (overrides.topUpSession ?? {
176
+ checkoutUrl: 'https://stripe.test/checkout/abc',
177
+ sessionId: 'cs_test_default',
178
+ }) as Awaited<ReturnType<ToolDeps['createConsumerTopUp']>>,
179
+ );
138
180
  const validateFilePath = vi.fn((p: string) => p);
181
+ const resolveSavePath = vi.fn((p: string) => p);
182
+ const writeFile = vi.fn();
139
183
  const fileSize = vi.fn().mockReturnValue(overrides.fileSize ?? 100);
140
- const filePartFromPath = vi.fn(async (path: string, opts: { partId?: string; contentType?: string }) => ({
141
- type: 'file' as const,
142
- partId: opts.partId ?? 'file',
143
- file: { id: 'file-id', mimeType: opts.contentType ?? 'application/octet-stream' },
144
- _path: path,
145
- }));
146
- const textPart = vi.fn((text: string, partId: string) => ({
147
- type: 'text' as const,
148
- partId,
149
- text,
150
- }));
184
+ const filePartFromPath = vi.fn<ToolDeps['filePartFromPath']>(
185
+ async (path, opts) =>
186
+ ({
187
+ type: 'file' as const,
188
+ partId: opts?.partId ?? 'file',
189
+ file: { id: 'file-id', mimeType: opts?.contentType ?? 'application/octet-stream' },
190
+ _path: path,
191
+ }) as Awaited<ReturnType<ToolDeps['filePartFromPath']>>,
192
+ );
193
+ const textPart = vi.fn<ToolDeps['textPart']>(
194
+ (text, partId) =>
195
+ ({
196
+ type: 'text' as const,
197
+ partId,
198
+ text,
199
+ }) as ReturnType<ToolDeps['textPart']>,
200
+ );
151
201
 
152
202
  const deps: ToolDeps = {
153
203
  getBaseUrl,
154
204
  getApiKey,
205
+ getOrgId,
155
206
  getTaskClient,
156
207
  getAgentByName,
157
208
  listAgents,
209
+ fetchAgentStatus,
210
+ getConsumerBalance,
211
+ createConsumerTopUp,
158
212
  validateFilePath,
213
+ resolveSavePath,
214
+ writeFile,
159
215
  fileSize,
160
216
  maxUploadBytes: overrides.maxUploadBytes ?? 25 * 1024 * 1024,
161
- filePartFromPath: filePartFromPath as unknown as ToolDeps['filePartFromPath'],
162
- textPart: textPart as unknown as ToolDeps['textPart'],
217
+ filePartFromPath,
218
+ textPart,
163
219
  };
164
220
 
165
221
  return {
@@ -168,10 +224,16 @@ export function makeFakeDeps(overrides: FakeDepsOverrides = {}) {
168
224
  mocks: {
169
225
  getBaseUrl,
170
226
  getApiKey,
227
+ getOrgId,
171
228
  getTaskClient,
172
229
  getAgentByName,
173
230
  listAgents,
231
+ fetchAgentStatus,
232
+ getConsumerBalance,
233
+ createConsumerTopUp,
174
234
  validateFilePath,
235
+ resolveSavePath,
236
+ writeFile,
175
237
  fileSize,
176
238
  filePartFromPath,
177
239
  textPart,
@@ -2,8 +2,18 @@ import { describe, it, expect } from 'vitest';
2
2
  import { listAgents } from '../src/tools.js';
3
3
  import { makeFakeDeps } from './helpers.js';
4
4
 
5
+ // A status response where every named agent has one online instance, so the
6
+ // default online-only filter is a no-op for these agents.
7
+ function allOnline(...names: string[]) {
8
+ const agents: Record<string, unknown> = {};
9
+ for (const name of names) {
10
+ agents[name] = { agentName: name, instances: [], onlineCount: 1, taskCount: 0 };
11
+ }
12
+ return { agents };
13
+ }
14
+
5
15
  describe('list_agents', () => {
6
- it('renders one row per agent with skill names joined by commas', async () => {
16
+ it('renders one row per agent with tag names joined by commas', async () => {
7
17
  const { deps } = makeFakeDeps({
8
18
  listAgentsResult: {
9
19
  agents: [
@@ -11,7 +21,7 @@ describe('list_agents', () => {
11
21
  agentName: 'alice',
12
22
  name: 'Alice',
13
23
  listing: 'public',
14
- skills: [
24
+ tags: [
15
25
  { id: 'translate', name: 'Translate' },
16
26
  { id: 'summarize', name: 'Summarize' },
17
27
  ],
@@ -20,55 +30,142 @@ describe('list_agents', () => {
20
30
  ],
21
31
  totalCount: 2,
22
32
  },
33
+ agentStatusResult: allOnline('alice', 'bob'),
23
34
  });
24
35
 
25
36
  const res = await listAgents({}, deps);
26
37
  const lines = res.content[0].text.split('\n');
27
38
  expect(lines).toEqual([
28
- 'Agents (2):',
39
+ 'Agents (2 online of 2 total):',
29
40
  'alice | Alice | public | Translate, Summarize',
30
41
  'bob | bob | private | ',
31
42
  ]);
32
43
  });
33
44
 
34
- it('forwards baseUrl, apiKey, skill, listing, limit to the registry helper', async () => {
45
+ it('forwards baseUrl, apiKey, tag, listing, limit (as maxAgents) to the registry helper', async () => {
35
46
  const { deps, mocks } = makeFakeDeps({
36
47
  apiKey: 'sk-test',
37
48
  baseUrl: 'http://api.test',
38
49
  });
39
50
 
40
51
  await listAgents(
41
- { skill: 'translate', listing: 'private', limit: 25 },
52
+ { tag: 'translate', listing: 'private', limit: 25 },
42
53
  deps,
43
54
  );
44
55
 
45
56
  expect(mocks.listAgents).toHaveBeenCalledWith({
46
57
  baseUrl: 'http://api.test',
47
58
  apiKey: 'sk-test',
48
- skill: 'translate',
59
+ tag: 'translate',
49
60
  listing: 'private',
50
- limit: 25,
61
+ maxAgents: 25,
51
62
  });
52
63
  });
53
64
 
54
65
  it('defaults to "public" for missing listing and uses agentName when name is absent', async () => {
55
66
  const { deps } = makeFakeDeps({
56
67
  listAgentsResult: {
57
- agents: [{ agentName: 'naked', skills: [] }],
68
+ agents: [{ agentName: 'naked', tags: [] }],
58
69
  totalCount: 1,
59
70
  },
71
+ agentStatusResult: allOnline('naked'),
60
72
  });
61
73
 
62
74
  const res = await listAgents({}, deps);
63
75
  expect(res.content[0].text).toContain('naked | naked | public |');
64
76
  });
65
77
 
66
- it('uses agents.length as the count when totalCount is omitted', async () => {
78
+ it('uses the online agent count for the header, falling back to length for total', async () => {
67
79
  const { deps } = makeFakeDeps({
68
80
  listAgentsResult: { agents: [{ agentName: 'a' }, { agentName: 'b' }] },
81
+ agentStatusResult: allOnline('a', 'b'),
82
+ });
83
+
84
+ const res = await listAgents({}, deps);
85
+ expect(res.content[0].text.split('\n')[0]).toBe('Agents (2 online of 2 total):');
86
+ });
87
+
88
+ it('reports the registry total separately from the online count', async () => {
89
+ const { deps } = makeFakeDeps({
90
+ listAgentsResult: {
91
+ agents: [{ agentName: 'online' }, { agentName: 'offline' }],
92
+ totalCount: 50,
93
+ },
94
+ agentStatusResult: {
95
+ agents: {
96
+ online: { agentName: 'online', instances: [], onlineCount: 1, taskCount: 0 },
97
+ offline: { agentName: 'offline', instances: [], onlineCount: 0, taskCount: 0 },
98
+ },
99
+ },
100
+ });
101
+
102
+ const res = await listAgents({}, deps);
103
+ expect(res.content[0].text.split('\n')[0]).toBe('Agents (1 online of 50 total):');
104
+ });
105
+
106
+ it('drops offline agents by default, keeping only those with online instances', async () => {
107
+ const { deps, mocks } = makeFakeDeps({
108
+ listAgentsResult: {
109
+ agents: [
110
+ { agentName: 'online', name: 'Online' },
111
+ { agentName: 'offline', name: 'Offline' },
112
+ ],
113
+ totalCount: 2,
114
+ },
115
+ agentStatusResult: {
116
+ agents: {
117
+ online: { agentName: 'online', instances: [], onlineCount: 1, taskCount: 0 },
118
+ offline: { agentName: 'offline', instances: [], onlineCount: 0, taskCount: 0 },
119
+ },
120
+ },
69
121
  });
70
122
 
71
123
  const res = await listAgents({}, deps);
72
- expect(res.content[0].text.split('\n')[0]).toBe('Agents (2):');
124
+ const lines = res.content[0].text.split('\n');
125
+ expect(lines).toEqual([
126
+ 'Agents (1 online of 2 total):',
127
+ 'online | Online | public | ',
128
+ ]);
129
+ expect(mocks.fetchAgentStatus).toHaveBeenCalledWith({
130
+ baseUrl: 'http://api.test',
131
+ apiKey: undefined,
132
+ agentNames: ['online', 'offline'],
133
+ });
134
+ });
135
+
136
+ it('includes offline agents and skips the status check when includeOffline is true', async () => {
137
+ const { deps, mocks } = makeFakeDeps({
138
+ listAgentsResult: {
139
+ agents: [
140
+ { agentName: 'online' },
141
+ { agentName: 'offline' },
142
+ ],
143
+ totalCount: 2,
144
+ },
145
+ });
146
+
147
+ const res = await listAgents({ includeOffline: true }, deps);
148
+ expect(res.content[0].text.split('\n')[0]).toBe('Agents (2 of 2 total):');
149
+ expect(mocks.fetchAgentStatus).not.toHaveBeenCalled();
150
+ });
151
+
152
+ it('drops agents whose names the status endpoint cannot query', async () => {
153
+ const { deps } = makeFakeDeps({
154
+ listAgentsResult: {
155
+ agents: [
156
+ { agentName: 'valid_name' },
157
+ { agentName: 'has-dash' },
158
+ ],
159
+ totalCount: 2,
160
+ },
161
+ agentStatusResult: allOnline('valid_name'),
162
+ });
163
+
164
+ const res = await listAgents({}, deps);
165
+ const lines = res.content[0].text.split('\n');
166
+ expect(lines).toEqual([
167
+ 'Agents (1 online of 2 total):',
168
+ 'valid_name | valid_name | public | ',
169
+ ]);
73
170
  });
74
171
  });