@zooid/context-mcp 0.13.0 → 0.14.0
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/dist/bin.js +44 -4
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-L5USYHTS.js → chunk-IZ2JCKE4.js} +168 -14
- package/dist/chunk-IZ2JCKE4.js.map +1 -0
- package/dist/index.js +34 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bin.ts +63 -9
- package/src/daemon-socket.test.ts +152 -19
- package/src/daemon-socket.ts +124 -12
- package/src/factory.test.ts +10 -0
- package/src/index.ts +3 -2
- package/src/integration.test.ts +99 -15
- package/src/mcp-server.test.ts +225 -21
- package/src/mcp-server.ts +87 -7
- package/src/socket-paths.test.ts +25 -0
- package/src/socket-paths.ts +29 -0
- package/src/spawn-registry.test.ts +14 -1
- package/src/spawn-registry.ts +29 -1
- package/src/types.ts +2 -0
- package/dist/chunk-L5USYHTS.js.map +0 -1
package/src/daemon-socket.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { createServer, createConnection, type Server, type Socket } from 'node:net'
|
|
2
2
|
import { unlink } from 'node:fs/promises'
|
|
3
3
|
import type { SpawnRegistry } from './spawn-registry.js'
|
|
4
|
+
import type { CompleteTaskInput, StartTasksInput } from '@zooid/core'
|
|
5
|
+
import { agentSocketPath } from './socket-paths.js'
|
|
4
6
|
|
|
5
|
-
interface DaemonRequest {
|
|
6
|
-
spawnId
|
|
7
|
+
export interface DaemonRequest {
|
|
8
|
+
spawnId?: string
|
|
9
|
+
acpSessionId?: string
|
|
7
10
|
method:
|
|
8
11
|
| 'getRoomHistory'
|
|
9
12
|
| 'getRecentThreads'
|
|
10
13
|
| 'getThreadHistory'
|
|
11
14
|
| 'getChannelMembers'
|
|
12
|
-
| '
|
|
15
|
+
| 'getRoomInfo'
|
|
16
|
+
| 'getRooms'
|
|
17
|
+
| 'sendMessage'
|
|
18
|
+
| 'startTasks'
|
|
19
|
+
| 'completeTask'
|
|
20
|
+
| 'describeRole'
|
|
13
21
|
params: Record<string, unknown>
|
|
14
22
|
}
|
|
15
23
|
|
|
@@ -27,9 +35,20 @@ export interface DaemonSocketHandle {
|
|
|
27
35
|
close(): Promise<void>
|
|
28
36
|
}
|
|
29
37
|
|
|
38
|
+
export interface AgentSocketsHandle {
|
|
39
|
+
/** Agent → host socket path. A missing path means its listener failed. */
|
|
40
|
+
paths: Record<string, string | undefined>
|
|
41
|
+
close(): Promise<void>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Same wire error for absent and foreign bindings, so sockets are not an ID oracle. */
|
|
45
|
+
const NOT_OWNED = 'binding not owned by caller'
|
|
46
|
+
|
|
30
47
|
export async function startDaemonSocketServer(opts: {
|
|
31
48
|
sockPath: string
|
|
32
49
|
registry: SpawnRegistry
|
|
50
|
+
/** The only agent whose bindings this listener may serve. */
|
|
51
|
+
agentName: string
|
|
33
52
|
}): Promise<DaemonSocketHandle> {
|
|
34
53
|
await unlink(opts.sockPath).catch(() => {})
|
|
35
54
|
// net.Server.close() waits for every open connection and — unlike
|
|
@@ -52,7 +71,7 @@ export async function startDaemonSocketServer(opts: {
|
|
|
52
71
|
const line = buf.slice(0, idx)
|
|
53
72
|
buf = buf.slice(idx + 1)
|
|
54
73
|
if (!line) continue
|
|
55
|
-
await handleLine(line, socket, opts.registry)
|
|
74
|
+
await handleLine(line, socket, opts.registry, opts.agentName)
|
|
56
75
|
}
|
|
57
76
|
})
|
|
58
77
|
socket.on('error', () => {})
|
|
@@ -77,29 +96,52 @@ export async function startDaemonSocketServer(opts: {
|
|
|
77
96
|
}
|
|
78
97
|
}
|
|
79
98
|
|
|
80
|
-
async function handleLine(
|
|
99
|
+
async function handleLine(
|
|
100
|
+
line: string,
|
|
101
|
+
socket: Socket,
|
|
102
|
+
registry: SpawnRegistry,
|
|
103
|
+
callerAgent: string,
|
|
104
|
+
) {
|
|
81
105
|
let req: DaemonRequest
|
|
82
106
|
try {
|
|
83
107
|
req = JSON.parse(line) as DaemonRequest
|
|
84
108
|
} catch {
|
|
85
|
-
socket.write(
|
|
109
|
+
socket.write(
|
|
110
|
+
JSON.stringify({
|
|
111
|
+
ok: false,
|
|
112
|
+
error: 'invalid json',
|
|
113
|
+
} satisfies DaemonError) + '\n',
|
|
114
|
+
)
|
|
86
115
|
return
|
|
87
116
|
}
|
|
88
|
-
const binding =
|
|
117
|
+
const binding = req.spawnId
|
|
118
|
+
? registry.get(req.spawnId)
|
|
119
|
+
: req.acpSessionId
|
|
120
|
+
? registry.getByAcpSession(req.acpSessionId)
|
|
121
|
+
: undefined
|
|
89
122
|
if (!binding) {
|
|
123
|
+
const address = req.acpSessionId ? `session: ${req.acpSessionId}` : `spawn-id: ${req.spawnId ?? ''}`
|
|
90
124
|
process.stderr.write(
|
|
91
|
-
`[context-mcp] daemon: unknown
|
|
125
|
+
`[context-mcp] daemon: unknown ${address} for method=${req.method}\n`,
|
|
92
126
|
)
|
|
93
127
|
socket.write(
|
|
94
128
|
JSON.stringify({
|
|
95
129
|
ok: false,
|
|
96
|
-
error:
|
|
130
|
+
error: NOT_OWNED,
|
|
97
131
|
} satisfies DaemonError) + '\n',
|
|
98
132
|
)
|
|
99
133
|
return
|
|
100
134
|
}
|
|
135
|
+
if (binding.agentName !== callerAgent) {
|
|
136
|
+
process.stderr.write(
|
|
137
|
+
`[context-mcp] daemon: refused ${req.method} — binding owned by ${binding.agentName}, ` +
|
|
138
|
+
`caller is ${callerAgent}\n`,
|
|
139
|
+
)
|
|
140
|
+
socket.write(JSON.stringify({ ok: false, error: NOT_OWNED } satisfies DaemonError) + '\n')
|
|
141
|
+
return
|
|
142
|
+
}
|
|
101
143
|
process.stderr.write(
|
|
102
|
-
`[context-mcp] daemon: ${req.method} spawn=${
|
|
144
|
+
`[context-mcp] daemon: ${req.method} spawn=${binding.spawnId.slice(0, 8)} agent=${binding.agentName}\n`,
|
|
103
145
|
)
|
|
104
146
|
try {
|
|
105
147
|
let result: unknown
|
|
@@ -113,8 +155,41 @@ async function handleLine(line: string, socket: Socket, registry: SpawnRegistry)
|
|
|
113
155
|
result = await binding.provider.getThreadHistory(channelId, threadId, req.params)
|
|
114
156
|
} else if (req.method === 'getChannelMembers') {
|
|
115
157
|
result = await binding.provider.getChannelMembers(channelId)
|
|
116
|
-
} else if (req.method === '
|
|
117
|
-
result = await binding.provider.
|
|
158
|
+
} else if (req.method === 'getRoomInfo') {
|
|
159
|
+
result = await binding.provider.getRoomInfo(channelId)
|
|
160
|
+
} else if (req.method === 'getRooms') {
|
|
161
|
+
result = await binding.provider.getRooms()
|
|
162
|
+
} else if (req.method === 'sendMessage') {
|
|
163
|
+
result = await binding.provider.sendMessage(
|
|
164
|
+
req.params as unknown as Parameters<typeof binding.provider.sendMessage>[0],
|
|
165
|
+
)
|
|
166
|
+
} else if (
|
|
167
|
+
req.method === 'startTasks' ||
|
|
168
|
+
req.method === 'completeTask' ||
|
|
169
|
+
req.method === 'describeRole'
|
|
170
|
+
) {
|
|
171
|
+
const actions = registry.taskActions
|
|
172
|
+
if (!actions) {
|
|
173
|
+
socket.write(
|
|
174
|
+
JSON.stringify({
|
|
175
|
+
ok: false,
|
|
176
|
+
error: 'task actions unavailable',
|
|
177
|
+
} satisfies DaemonError) + '\n',
|
|
178
|
+
)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
const caller = {
|
|
182
|
+
agentName: binding.agentName,
|
|
183
|
+
channelId,
|
|
184
|
+
threadRoot: binding.threadRef.threadId,
|
|
185
|
+
sessionKey: binding.sessionKey ?? binding.threadRef.threadId,
|
|
186
|
+
}
|
|
187
|
+
result =
|
|
188
|
+
req.method === 'startTasks'
|
|
189
|
+
? await actions.startTasks(caller, req.params as unknown as StartTasksInput)
|
|
190
|
+
: req.method === 'completeTask'
|
|
191
|
+
? await actions.completeTask(caller, req.params as unknown as CompleteTaskInput)
|
|
192
|
+
: await actions.describeRole(caller)
|
|
118
193
|
} else {
|
|
119
194
|
socket.write(
|
|
120
195
|
JSON.stringify({
|
|
@@ -135,6 +210,43 @@ async function handleLine(line: string, socket: Socket, registry: SpawnRegistry)
|
|
|
135
210
|
}
|
|
136
211
|
}
|
|
137
212
|
|
|
213
|
+
/** Start independent context listeners; a bind failure disables only that agent. */
|
|
214
|
+
export async function startAgentSocketServers(opts: {
|
|
215
|
+
runDir: string
|
|
216
|
+
registry: SpawnRegistry
|
|
217
|
+
agentNames: string[]
|
|
218
|
+
listen?: (sockPath: string, agentName: string) => Promise<DaemonSocketHandle>
|
|
219
|
+
}): Promise<AgentSocketsHandle> {
|
|
220
|
+
const listen =
|
|
221
|
+
opts.listen ??
|
|
222
|
+
((sockPath: string, agentName: string) =>
|
|
223
|
+
startDaemonSocketServer({ sockPath, registry: opts.registry, agentName }))
|
|
224
|
+
const derived = opts.agentNames.map((agentName) => ({
|
|
225
|
+
agentName,
|
|
226
|
+
sockPath: agentSocketPath({ runDir: opts.runDir, agentName }),
|
|
227
|
+
}))
|
|
228
|
+
const paths: Record<string, string | undefined> = {}
|
|
229
|
+
const handles: Array<{ handle: DaemonSocketHandle; path: string }> = []
|
|
230
|
+
for (const { agentName, sockPath } of derived) {
|
|
231
|
+
try {
|
|
232
|
+
const handle = await listen(sockPath, agentName)
|
|
233
|
+
paths[agentName] = sockPath
|
|
234
|
+
handles.push({ handle, path: sockPath })
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.warn(`[context] socket bind failed for agent=${agentName}; context disabled for it:`, err)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
paths,
|
|
241
|
+
close: async () => {
|
|
242
|
+
for (const { handle, path } of handles) {
|
|
243
|
+
await handle.close()
|
|
244
|
+
await unlink(path).catch(() => {})
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
138
250
|
export async function callDaemon(sockPath: string, req: DaemonRequest): Promise<unknown> {
|
|
139
251
|
return new Promise((resolve, reject) => {
|
|
140
252
|
const socket = createConnection(sockPath)
|
package/src/factory.test.ts
CHANGED
|
@@ -82,6 +82,16 @@ describe('contextContainerMounts', () => {
|
|
|
82
82
|
])
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
+
it('uses each agent host socket at the same container target', () => {
|
|
86
|
+
const alice = contextContainerMounts({ sockPath: '/data/run/context-alice.sock' })
|
|
87
|
+
const bob = contextContainerMounts({ sockPath: '/data/run/context-bob.sock' })
|
|
88
|
+
const aliceSocket = alice.find((mount) => mount.target === CONTEXT_CONTAINER_SOCK)!
|
|
89
|
+
const bobSocket = bob.find((mount) => mount.target === CONTEXT_CONTAINER_SOCK)!
|
|
90
|
+
expect(aliceSocket.path).not.toBe(bobSocket.path)
|
|
91
|
+
expect(aliceSocket).toMatchObject({ target: CONTEXT_CONTAINER_SOCK, mode: 'rw' })
|
|
92
|
+
expect(bobSocket.target).toBe(CONTEXT_CONTAINER_SOCK)
|
|
93
|
+
})
|
|
94
|
+
|
|
85
95
|
it('defaults the bin dir to the resolved package dist on disk', () => {
|
|
86
96
|
const mounts = contextContainerMounts({ sockPath: '/tmp/x.sock' })
|
|
87
97
|
const binMount = mounts.find((m) => m.target === CONTEXT_CONTAINER_BIN_DIR)!
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { SpawnRegistry } from './spawn-registry.js'
|
|
2
|
-
export { startDaemonSocketServer, callDaemon } from './daemon-socket.js'
|
|
3
|
-
export type { DaemonSocketHandle } from './daemon-socket.js'
|
|
2
|
+
export { startDaemonSocketServer, startAgentSocketServers, callDaemon } from './daemon-socket.js'
|
|
3
|
+
export type { DaemonRequest, DaemonSocketHandle, AgentSocketsHandle } from './daemon-socket.js'
|
|
4
|
+
export { agentSocketPath, SUN_PATH_MAX } from './socket-paths.js'
|
|
4
5
|
export { buildContextMcpServer } from './mcp-server.js'
|
|
5
6
|
export {
|
|
6
7
|
buildContextServerSpec,
|
package/src/integration.test.ts
CHANGED
|
@@ -3,11 +3,12 @@ import { fileURLToPath } from 'node:url'
|
|
|
3
3
|
import { dirname, join } from 'node:path'
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { randomUUID } from 'node:crypto'
|
|
6
|
-
import { existsSync } from 'node:fs'
|
|
6
|
+
import { existsSync, mkdtempSync } from 'node:fs'
|
|
7
7
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
8
8
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
9
9
|
import { SpawnRegistry } from './spawn-registry.js'
|
|
10
|
-
import { startDaemonSocketServer } from './daemon-socket.js'
|
|
10
|
+
import { callDaemon, startAgentSocketServers, startDaemonSocketServer } from './daemon-socket.js'
|
|
11
|
+
import { agentSocketPath } from './socket-paths.js'
|
|
11
12
|
import type { TransportContextProvider } from '@zooid/core'
|
|
12
13
|
|
|
13
14
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
@@ -25,7 +26,7 @@ function fakeProvider(over: Partial<TransportContextProvider> = {}): TransportCo
|
|
|
25
26
|
getRecentThreads: async () => ({ threads: [], has_more: false }),
|
|
26
27
|
getThreadHistory: async () => ({ messages: [], has_more: false }),
|
|
27
28
|
getChannelMembers: async () => [],
|
|
28
|
-
|
|
29
|
+
getRoomInfo: async () => ({ id: 'r', name: 'r', transport: 'matrix' }),
|
|
29
30
|
...over,
|
|
30
31
|
}
|
|
31
32
|
}
|
|
@@ -35,7 +36,13 @@ describe.skipIf(!existsSync(BIN))('zooid-context MCP server (out-of-process)', (
|
|
|
35
36
|
const provider = fakeProvider({
|
|
36
37
|
getRoomHistory: async () => ({
|
|
37
38
|
messages: [
|
|
38
|
-
{
|
|
39
|
+
{
|
|
40
|
+
id: 'e1',
|
|
41
|
+
sender: 'alice',
|
|
42
|
+
text: 'hi',
|
|
43
|
+
timestamp: 'T',
|
|
44
|
+
is_agent: false,
|
|
45
|
+
},
|
|
39
46
|
],
|
|
40
47
|
has_more: false,
|
|
41
48
|
}),
|
|
@@ -47,7 +54,7 @@ describe.skipIf(!existsSync(BIN))('zooid-context MCP server (out-of-process)', (
|
|
|
47
54
|
provider,
|
|
48
55
|
})
|
|
49
56
|
const sockPath = join(tmpdir(), `zooid-it-${randomUUID()}.sock`)
|
|
50
|
-
const server = await startDaemonSocketServer({ sockPath, registry })
|
|
57
|
+
const server = await startDaemonSocketServer({ sockPath, registry, agentName: 'architect' })
|
|
51
58
|
cleanup.push(() => server.close())
|
|
52
59
|
|
|
53
60
|
const transport = new StdioClientTransport({
|
|
@@ -61,16 +68,24 @@ describe.skipIf(!existsSync(BIN))('zooid-context MCP server (out-of-process)', (
|
|
|
61
68
|
await client.close()
|
|
62
69
|
})
|
|
63
70
|
|
|
71
|
+
// No taskActions registered on this registry, so the daemon-side
|
|
72
|
+
// describeRole query fails, bin.ts's role stays undefined, and neither
|
|
73
|
+
// task tool registers ([[ZOD084]] role-conditional registration).
|
|
64
74
|
const list = await client.listTools()
|
|
65
75
|
expect(list.tools.map((t) => t.name).sort()).toEqual([
|
|
66
|
-
'zooid_get_channel_info',
|
|
67
76
|
'zooid_get_history',
|
|
68
77
|
'zooid_get_members',
|
|
69
78
|
'zooid_get_recent_threads',
|
|
79
|
+
'zooid_get_room_info',
|
|
80
|
+
'zooid_get_rooms',
|
|
70
81
|
'zooid_get_thread_history',
|
|
82
|
+
'zooid_send_message',
|
|
71
83
|
])
|
|
72
84
|
|
|
73
|
-
const result = await client.callTool({
|
|
85
|
+
const result = await client.callTool({
|
|
86
|
+
name: 'zooid_get_history',
|
|
87
|
+
arguments: {},
|
|
88
|
+
})
|
|
74
89
|
const payload = JSON.parse((result.content as Array<{ text: string }>)[0].text)
|
|
75
90
|
expect(payload.messages[0].id).toBe('e1')
|
|
76
91
|
})
|
|
@@ -78,17 +93,41 @@ describe.skipIf(!existsSync(BIN))('zooid-context MCP server (out-of-process)', (
|
|
|
78
93
|
it('two MCP server subprocesses sharing one socket route to their own bindings', async () => {
|
|
79
94
|
const providerA = fakeProvider({
|
|
80
95
|
getRoomHistory: async () => ({
|
|
81
|
-
messages: [
|
|
96
|
+
messages: [
|
|
97
|
+
{
|
|
98
|
+
id: 'A1',
|
|
99
|
+
sender: 'alice',
|
|
100
|
+
text: 'from A',
|
|
101
|
+
timestamp: 'T',
|
|
102
|
+
is_agent: false,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
82
105
|
has_more: false,
|
|
83
106
|
}),
|
|
84
|
-
|
|
107
|
+
getRoomInfo: async () => ({
|
|
108
|
+
id: '!a:hs',
|
|
109
|
+
name: 'room-A',
|
|
110
|
+
transport: 'matrix',
|
|
111
|
+
}),
|
|
85
112
|
})
|
|
86
113
|
const providerB = fakeProvider({
|
|
87
114
|
getRoomHistory: async () => ({
|
|
88
|
-
messages: [
|
|
115
|
+
messages: [
|
|
116
|
+
{
|
|
117
|
+
id: 'B1',
|
|
118
|
+
sender: 'bob',
|
|
119
|
+
text: 'from B',
|
|
120
|
+
timestamp: 'T',
|
|
121
|
+
is_agent: false,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
89
124
|
has_more: false,
|
|
90
125
|
}),
|
|
91
|
-
|
|
126
|
+
getRoomInfo: async () => ({
|
|
127
|
+
id: '!b:hs',
|
|
128
|
+
name: 'room-B',
|
|
129
|
+
transport: 'matrix',
|
|
130
|
+
}),
|
|
92
131
|
})
|
|
93
132
|
const registry = new SpawnRegistry()
|
|
94
133
|
const spawnA = registry.register({
|
|
@@ -97,12 +136,12 @@ describe.skipIf(!existsSync(BIN))('zooid-context MCP server (out-of-process)', (
|
|
|
97
136
|
provider: providerA,
|
|
98
137
|
})
|
|
99
138
|
const spawnB = registry.register({
|
|
100
|
-
agentName: '
|
|
139
|
+
agentName: 'architect',
|
|
101
140
|
threadRef: { channelId: '!b:hs', threadId: '!b:hs' },
|
|
102
141
|
provider: providerB,
|
|
103
142
|
})
|
|
104
143
|
const sockPath = join(tmpdir(), `zooid-it-${randomUUID()}.sock`)
|
|
105
|
-
const server = await startDaemonSocketServer({ sockPath, registry })
|
|
144
|
+
const server = await startDaemonSocketServer({ sockPath, registry, agentName: 'architect' })
|
|
106
145
|
cleanup.push(() => server.close())
|
|
107
146
|
|
|
108
147
|
async function startClient(spawnId: string) {
|
|
@@ -130,9 +169,54 @@ describe.skipIf(!existsSync(BIN))('zooid-context MCP server (out-of-process)', (
|
|
|
130
169
|
expect(payloadA.messages[0].id).toBe('A1')
|
|
131
170
|
expect(payloadB.messages[0].id).toBe('B1')
|
|
132
171
|
|
|
133
|
-
const infoA = await clientA.callTool({
|
|
134
|
-
|
|
172
|
+
const infoA = await clientA.callTool({
|
|
173
|
+
name: 'zooid_get_room_info',
|
|
174
|
+
arguments: {},
|
|
175
|
+
})
|
|
176
|
+
const infoB = await clientB.callTool({
|
|
177
|
+
name: 'zooid_get_room_info',
|
|
178
|
+
arguments: {},
|
|
179
|
+
})
|
|
135
180
|
expect(JSON.parse((infoA.content as Array<{ text: string }>)[0].text).id).toBe('!a:hs')
|
|
136
181
|
expect(JSON.parse((infoB.content as Array<{ text: string }>)[0].text).id).toBe('!b:hs')
|
|
137
182
|
})
|
|
138
183
|
})
|
|
184
|
+
|
|
185
|
+
describe('per-agent sockets (integration)', () => {
|
|
186
|
+
it('isolates two agents sharing a registry', async () => {
|
|
187
|
+
const registry = new SpawnRegistry()
|
|
188
|
+
const aliceSpawn = registry.register({
|
|
189
|
+
agentName: 'alice',
|
|
190
|
+
threadRef: { channelId: '!alice:hs', threadId: 'a' },
|
|
191
|
+
provider: fakeProvider({ getRoomInfo: async () => ({ id: '!alice:hs', name: 'alice', transport: 'matrix' }) }),
|
|
192
|
+
})
|
|
193
|
+
const bobSpawn = registry.register({
|
|
194
|
+
agentName: 'bob',
|
|
195
|
+
threadRef: { channelId: '!bob:hs', threadId: 'b' },
|
|
196
|
+
provider: fakeProvider({ getRoomInfo: async () => ({ id: '!bob:hs', name: 'bob', transport: 'matrix' }) }),
|
|
197
|
+
})
|
|
198
|
+
const runDir = mkdtempSync(join(tmpdir(), 'zooid-run-'))
|
|
199
|
+
const sockets = await startAgentSocketServers({ runDir, registry, agentNames: ['alice', 'bob'] })
|
|
200
|
+
cleanup.push(() => sockets.close())
|
|
201
|
+
expect(sockets.paths.alice).toBe(agentSocketPath({ runDir, agentName: 'alice' }))
|
|
202
|
+
await expect(callDaemon(sockets.paths.alice!, { spawnId: aliceSpawn, method: 'getRoomInfo', params: {} })).resolves.toMatchObject({ id: '!alice:hs' })
|
|
203
|
+
await expect(callDaemon(sockets.paths.bob!, { spawnId: aliceSpawn, method: 'getRoomInfo', params: {} })).rejects.toThrow(/binding not owned by caller/)
|
|
204
|
+
await expect(callDaemon(sockets.paths.bob!, { spawnId: bobSpawn, method: 'getRoomInfo', params: {} })).resolves.toMatchObject({ id: '!bob:hs' })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('keeps other listeners serving when one bind fails', async () => {
|
|
208
|
+
const registry = new SpawnRegistry()
|
|
209
|
+
const sockets = await startAgentSocketServers({
|
|
210
|
+
runDir: mkdtempSync(join(tmpdir(), 'zooid-run-')),
|
|
211
|
+
registry,
|
|
212
|
+
agentNames: ['alice', 'bob'],
|
|
213
|
+
listen: async (path, name) => {
|
|
214
|
+
if (name === 'bob') throw new Error('EADDRINUSE')
|
|
215
|
+
return startDaemonSocketServer({ sockPath: path, registry, agentName: name })
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
cleanup.push(() => sockets.close())
|
|
219
|
+
expect(sockets.paths.alice).toBeDefined()
|
|
220
|
+
expect(sockets.paths.bob).toBeUndefined()
|
|
221
|
+
})
|
|
222
|
+
})
|