@botcord/daemon 0.1.1
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/activity-tracker.d.ts +43 -0
- package/dist/activity-tracker.js +110 -0
- package/dist/adapters/runtimes.d.ts +14 -0
- package/dist/adapters/runtimes.js +18 -0
- package/dist/agent-discovery.d.ts +81 -0
- package/dist/agent-discovery.js +181 -0
- package/dist/agent-workspace.d.ts +31 -0
- package/dist/agent-workspace.js +221 -0
- package/dist/config.d.ts +116 -0
- package/dist/config.js +180 -0
- package/dist/control-channel.d.ts +99 -0
- package/dist/control-channel.js +388 -0
- package/dist/cross-room.d.ts +23 -0
- package/dist/cross-room.js +55 -0
- package/dist/daemon-config-map.d.ts +61 -0
- package/dist/daemon-config-map.js +153 -0
- package/dist/daemon.d.ts +123 -0
- package/dist/daemon.js +349 -0
- package/dist/doctor.d.ts +89 -0
- package/dist/doctor.js +191 -0
- package/dist/gateway/channel-manager.d.ts +54 -0
- package/dist/gateway/channel-manager.js +292 -0
- package/dist/gateway/channels/botcord.d.ts +93 -0
- package/dist/gateway/channels/botcord.js +510 -0
- package/dist/gateway/channels/index.d.ts +2 -0
- package/dist/gateway/channels/index.js +1 -0
- package/dist/gateway/channels/sanitize.d.ts +20 -0
- package/dist/gateway/channels/sanitize.js +56 -0
- package/dist/gateway/dispatcher.d.ts +73 -0
- package/dist/gateway/dispatcher.js +431 -0
- package/dist/gateway/gateway.d.ts +87 -0
- package/dist/gateway/gateway.js +158 -0
- package/dist/gateway/index.d.ts +15 -0
- package/dist/gateway/index.js +15 -0
- package/dist/gateway/log.d.ts +9 -0
- package/dist/gateway/log.js +20 -0
- package/dist/gateway/router.d.ts +10 -0
- package/dist/gateway/router.js +48 -0
- package/dist/gateway/runtimes/claude-code.d.ts +30 -0
- package/dist/gateway/runtimes/claude-code.js +162 -0
- package/dist/gateway/runtimes/codex.d.ts +83 -0
- package/dist/gateway/runtimes/codex.js +272 -0
- package/dist/gateway/runtimes/gemini.d.ts +15 -0
- package/dist/gateway/runtimes/gemini.js +29 -0
- package/dist/gateway/runtimes/ndjson-stream.d.ts +43 -0
- package/dist/gateway/runtimes/ndjson-stream.js +169 -0
- package/dist/gateway/runtimes/probe.d.ts +17 -0
- package/dist/gateway/runtimes/probe.js +54 -0
- package/dist/gateway/runtimes/registry.d.ts +59 -0
- package/dist/gateway/runtimes/registry.js +94 -0
- package/dist/gateway/session-store.d.ts +39 -0
- package/dist/gateway/session-store.js +133 -0
- package/dist/gateway/types.d.ts +265 -0
- package/dist/gateway/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +854 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +44 -0
- package/dist/provision.d.ts +88 -0
- package/dist/provision.js +749 -0
- package/dist/room-context-fetcher.d.ts +18 -0
- package/dist/room-context-fetcher.js +101 -0
- package/dist/room-context.d.ts +53 -0
- package/dist/room-context.js +112 -0
- package/dist/sender-classify.d.ts +30 -0
- package/dist/sender-classify.js +32 -0
- package/dist/snapshot-writer.d.ts +37 -0
- package/dist/snapshot-writer.js +84 -0
- package/dist/status-render.d.ts +28 -0
- package/dist/status-render.js +97 -0
- package/dist/system-context.d.ts +57 -0
- package/dist/system-context.js +91 -0
- package/dist/turn-text.d.ts +36 -0
- package/dist/turn-text.js +57 -0
- package/dist/user-auth.d.ts +75 -0
- package/dist/user-auth.js +245 -0
- package/dist/working-memory.d.ts +46 -0
- package/dist/working-memory.js +274 -0
- package/package.json +39 -0
- package/src/__tests__/activity-tracker.test.ts +130 -0
- package/src/__tests__/agent-discovery.test.ts +191 -0
- package/src/__tests__/agent-workspace.test.ts +147 -0
- package/src/__tests__/control-channel.test.ts +327 -0
- package/src/__tests__/cross-room.test.ts +116 -0
- package/src/__tests__/daemon-config-map.test.ts +416 -0
- package/src/__tests__/daemon.test.ts +300 -0
- package/src/__tests__/device-code.test.ts +152 -0
- package/src/__tests__/doctor.test.ts +218 -0
- package/src/__tests__/protocol-core-reexport.test.ts +24 -0
- package/src/__tests__/provision.test.ts +922 -0
- package/src/__tests__/room-context.test.ts +233 -0
- package/src/__tests__/runtime-discovery.test.ts +173 -0
- package/src/__tests__/snapshot-writer.test.ts +141 -0
- package/src/__tests__/status-render.test.ts +137 -0
- package/src/__tests__/system-context.test.ts +315 -0
- package/src/__tests__/turn-text.test.ts +116 -0
- package/src/__tests__/user-auth.test.ts +125 -0
- package/src/__tests__/working-memory.test.ts +240 -0
- package/src/activity-tracker.ts +140 -0
- package/src/adapters/runtimes.ts +30 -0
- package/src/agent-discovery.ts +262 -0
- package/src/agent-workspace.ts +247 -0
- package/src/config.ts +290 -0
- package/src/control-channel.ts +455 -0
- package/src/cross-room.ts +89 -0
- package/src/daemon-config-map.ts +200 -0
- package/src/daemon.ts +478 -0
- package/src/doctor.ts +282 -0
- package/src/gateway/__tests__/.gitkeep +0 -0
- package/src/gateway/__tests__/botcord-channel.test.ts +480 -0
- package/src/gateway/__tests__/channel-manager.test.ts +475 -0
- package/src/gateway/__tests__/claude-code-adapter.test.ts +318 -0
- package/src/gateway/__tests__/codex-adapter.test.ts +350 -0
- package/src/gateway/__tests__/dispatcher.test.ts +1159 -0
- package/src/gateway/__tests__/gateway-add-channel.test.ts +180 -0
- package/src/gateway/__tests__/gateway-managed-routes.test.ts +181 -0
- package/src/gateway/__tests__/gateway.test.ts +222 -0
- package/src/gateway/__tests__/router.test.ts +247 -0
- package/src/gateway/__tests__/sanitize.test.ts +193 -0
- package/src/gateway/__tests__/session-store.test.ts +235 -0
- package/src/gateway/channel-manager.ts +349 -0
- package/src/gateway/channels/botcord.ts +605 -0
- package/src/gateway/channels/index.ts +6 -0
- package/src/gateway/channels/sanitize.ts +68 -0
- package/src/gateway/dispatcher.ts +554 -0
- package/src/gateway/gateway.ts +211 -0
- package/src/gateway/index.ts +29 -0
- package/src/gateway/log.ts +30 -0
- package/src/gateway/router.ts +60 -0
- package/src/gateway/runtimes/claude-code.ts +180 -0
- package/src/gateway/runtimes/codex.ts +312 -0
- package/src/gateway/runtimes/gemini.ts +43 -0
- package/src/gateway/runtimes/ndjson-stream.ts +225 -0
- package/src/gateway/runtimes/probe.ts +73 -0
- package/src/gateway/runtimes/registry.ts +143 -0
- package/src/gateway/session-store.ts +157 -0
- package/src/gateway/types.ts +325 -0
- package/src/index.ts +961 -0
- package/src/log.ts +47 -0
- package/src/provision.ts +879 -0
- package/src/room-context-fetcher.ts +124 -0
- package/src/room-context.ts +167 -0
- package/src/sender-classify.ts +46 -0
- package/src/snapshot-writer.ts +103 -0
- package/src/status-render.ts +132 -0
- package/src/system-context.ts +162 -0
- package/src/turn-text.ts +93 -0
- package/src/user-auth.ts +295 -0
- package/src/working-memory.ts +352 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { DaemonConfig } from "../config.js";
|
|
3
|
+
import {
|
|
4
|
+
BOTCORD_CHANNEL_TYPE,
|
|
5
|
+
buildManagedRoutes,
|
|
6
|
+
toGatewayConfig,
|
|
7
|
+
} from "../daemon-config-map.js";
|
|
8
|
+
import { agentWorkspaceDir } from "../agent-workspace.js";
|
|
9
|
+
import type { GatewayRoute } from "../gateway/index.js";
|
|
10
|
+
|
|
11
|
+
function baseConfig(partial: Partial<DaemonConfig> = {}): DaemonConfig {
|
|
12
|
+
return {
|
|
13
|
+
agentId: "ag_daemon",
|
|
14
|
+
defaultRoute: { adapter: "claude-code", cwd: "/home/alice" },
|
|
15
|
+
routes: [],
|
|
16
|
+
streamBlocks: true,
|
|
17
|
+
...partial,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("toGatewayConfig", () => {
|
|
22
|
+
it("translates a minimal (legacy agentId) daemon config into a single-channel gateway config keyed by agentId", () => {
|
|
23
|
+
const gw = toGatewayConfig(baseConfig());
|
|
24
|
+
expect(gw.channels).toHaveLength(1);
|
|
25
|
+
expect(gw.channels[0]).toEqual({
|
|
26
|
+
id: "ag_daemon",
|
|
27
|
+
type: BOTCORD_CHANNEL_TYPE,
|
|
28
|
+
accountId: "ag_daemon",
|
|
29
|
+
agentId: "ag_daemon",
|
|
30
|
+
});
|
|
31
|
+
expect(gw.defaultRoute).toEqual({
|
|
32
|
+
runtime: "claude-code",
|
|
33
|
+
cwd: "/home/alice",
|
|
34
|
+
extraArgs: undefined,
|
|
35
|
+
trustLevel: undefined,
|
|
36
|
+
});
|
|
37
|
+
expect(gw.routes).toEqual([]);
|
|
38
|
+
expect(gw.streamBlocks).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("maps route.match.roomId to conversationId without auto-injecting channel", () => {
|
|
42
|
+
const cfg = baseConfig({
|
|
43
|
+
routes: [
|
|
44
|
+
{
|
|
45
|
+
match: { roomId: "rm_abc" },
|
|
46
|
+
adapter: "claude-code",
|
|
47
|
+
cwd: "/work",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
const gw = toGatewayConfig(cfg);
|
|
52
|
+
expect(gw.routes).toHaveLength(1);
|
|
53
|
+
expect(gw.routes![0].match).toEqual({
|
|
54
|
+
conversationId: "rm_abc",
|
|
55
|
+
});
|
|
56
|
+
expect(gw.routes![0].match?.channel).toBeUndefined();
|
|
57
|
+
expect(gw.routes![0].runtime).toBe("claude-code");
|
|
58
|
+
expect(gw.routes![0].cwd).toBe("/work");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("maps route.match.roomPrefix to conversationPrefix without auto-injecting channel", () => {
|
|
62
|
+
const cfg = baseConfig({
|
|
63
|
+
routes: [
|
|
64
|
+
{
|
|
65
|
+
match: { roomPrefix: "rm_oc_" },
|
|
66
|
+
adapter: "claude-code",
|
|
67
|
+
cwd: "/work",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
const gw = toGatewayConfig(cfg);
|
|
72
|
+
expect(gw.routes![0].match).toEqual({
|
|
73
|
+
conversationPrefix: "rm_oc_",
|
|
74
|
+
});
|
|
75
|
+
expect(gw.routes![0].match?.channel).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("does not inject queueMode for rm_oc_ prefix routes (dispatcher default wins via direct kind)", () => {
|
|
79
|
+
const cfg = baseConfig({
|
|
80
|
+
routes: [
|
|
81
|
+
{
|
|
82
|
+
match: { roomPrefix: "rm_oc_" },
|
|
83
|
+
adapter: "claude-code",
|
|
84
|
+
cwd: "/work",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
const gw = toGatewayConfig(cfg);
|
|
89
|
+
expect(gw.routes![0].queueMode).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("does not inject queueMode for a concrete rm_oc_* roomId route", () => {
|
|
93
|
+
const cfg = baseConfig({
|
|
94
|
+
routes: [
|
|
95
|
+
{
|
|
96
|
+
match: { roomId: "rm_oc_abc123" },
|
|
97
|
+
adapter: "claude-code",
|
|
98
|
+
cwd: "/work",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
const gw = toGatewayConfig(cfg);
|
|
103
|
+
expect(gw.routes![0].queueMode).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("leaves queueMode undefined for non-owner-chat routes (dispatcher default wins)", () => {
|
|
107
|
+
const cfg = baseConfig({
|
|
108
|
+
routes: [
|
|
109
|
+
{
|
|
110
|
+
match: { roomPrefix: "rm_share_" },
|
|
111
|
+
adapter: "claude-code",
|
|
112
|
+
cwd: "/work",
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
const gw = toGatewayConfig(cfg);
|
|
117
|
+
expect(gw.routes![0].queueMode).toBeUndefined();
|
|
118
|
+
expect(gw.defaultRoute.queueMode).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("maps trustLevel owner → owner and untrusted → public when present", () => {
|
|
122
|
+
const cfg = baseConfig({
|
|
123
|
+
defaultRoute: {
|
|
124
|
+
adapter: "claude-code",
|
|
125
|
+
cwd: "/home",
|
|
126
|
+
// Untyped extension — daemon config doesn't require trustLevel, but
|
|
127
|
+
// callers may pass it through. Cast to access the compat surface.
|
|
128
|
+
...({ trustLevel: "owner" } as Record<string, unknown>),
|
|
129
|
+
} as DaemonConfig["defaultRoute"],
|
|
130
|
+
routes: [
|
|
131
|
+
{
|
|
132
|
+
match: { roomId: "rm_peer" },
|
|
133
|
+
adapter: "claude-code",
|
|
134
|
+
cwd: "/x",
|
|
135
|
+
...({ trustLevel: "untrusted" } as Record<string, unknown>),
|
|
136
|
+
} as DaemonConfig["routes"][number],
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
const gw = toGatewayConfig(cfg);
|
|
140
|
+
expect(gw.defaultRoute.trustLevel).toBe("owner");
|
|
141
|
+
expect(gw.routes![0].trustLevel).toBe("public");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("propagates extraArgs verbatim on default and route entries", () => {
|
|
145
|
+
const cfg = baseConfig({
|
|
146
|
+
defaultRoute: {
|
|
147
|
+
adapter: "claude-code",
|
|
148
|
+
cwd: "/home",
|
|
149
|
+
extraArgs: ["--permission-mode", "plan"],
|
|
150
|
+
},
|
|
151
|
+
routes: [
|
|
152
|
+
{
|
|
153
|
+
match: { roomId: "rm_x" },
|
|
154
|
+
adapter: "codex",
|
|
155
|
+
cwd: "/proj",
|
|
156
|
+
extraArgs: ["--flag"],
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
const gw = toGatewayConfig(cfg);
|
|
161
|
+
expect(gw.defaultRoute.extraArgs).toEqual(["--permission-mode", "plan"]);
|
|
162
|
+
expect(gw.routes![0].extraArgs).toEqual(["--flag"]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("propagates streamBlocks verbatim (true and false)", () => {
|
|
166
|
+
expect(toGatewayConfig(baseConfig({ streamBlocks: true })).streamBlocks).toBe(true);
|
|
167
|
+
expect(toGatewayConfig(baseConfig({ streamBlocks: false })).streamBlocks).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("passes through new match fields (accountId, senderId, kind, mentioned, channel)", () => {
|
|
171
|
+
const cfg = baseConfig({
|
|
172
|
+
routes: [
|
|
173
|
+
{
|
|
174
|
+
match: {
|
|
175
|
+
channel: "botcord",
|
|
176
|
+
accountId: "ag_account",
|
|
177
|
+
senderId: "ag_sender",
|
|
178
|
+
conversationKind: "direct",
|
|
179
|
+
mentioned: true,
|
|
180
|
+
},
|
|
181
|
+
adapter: "claude-code",
|
|
182
|
+
cwd: "/work",
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
const gw = toGatewayConfig(cfg);
|
|
187
|
+
expect(gw.routes![0].match).toEqual({
|
|
188
|
+
channel: "botcord",
|
|
189
|
+
accountId: "ag_account",
|
|
190
|
+
senderId: "ag_sender",
|
|
191
|
+
conversationKind: "direct",
|
|
192
|
+
mentioned: true,
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("prefers conversationId over legacy roomId when both are present", () => {
|
|
197
|
+
const cfg = baseConfig({
|
|
198
|
+
routes: [
|
|
199
|
+
{
|
|
200
|
+
match: { roomId: "rm_legacy", conversationId: "rm_canonical" },
|
|
201
|
+
adapter: "claude-code",
|
|
202
|
+
cwd: "/work",
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
const gw = toGatewayConfig(cfg);
|
|
207
|
+
expect(gw.routes![0].match?.conversationId).toBe("rm_canonical");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("prefers conversationPrefix over legacy roomPrefix when both are present", () => {
|
|
211
|
+
const cfg = baseConfig({
|
|
212
|
+
routes: [
|
|
213
|
+
{
|
|
214
|
+
match: {
|
|
215
|
+
roomPrefix: "rm_oc_",
|
|
216
|
+
conversationPrefix: "rm_share_",
|
|
217
|
+
},
|
|
218
|
+
adapter: "claude-code",
|
|
219
|
+
cwd: "/work",
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
const gw = toGatewayConfig(cfg);
|
|
224
|
+
expect(gw.routes![0].match?.conversationPrefix).toBe("rm_share_");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("allows overriding the channel binding for multi-channel configs", () => {
|
|
228
|
+
const cfg = baseConfig({
|
|
229
|
+
routes: [
|
|
230
|
+
{
|
|
231
|
+
match: { channel: "telegram", conversationPrefix: "tg_" },
|
|
232
|
+
adapter: "claude-code",
|
|
233
|
+
cwd: "/work",
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
});
|
|
237
|
+
const gw = toGatewayConfig(cfg);
|
|
238
|
+
expect(gw.routes![0].match?.channel).toBe("telegram");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("emits one channel per id when `agents` carries multiple entries", () => {
|
|
242
|
+
// Drop the legacy agentId so `baseConfig` doesn't hand us both shapes.
|
|
243
|
+
const cfg = baseConfig({
|
|
244
|
+
agentId: undefined,
|
|
245
|
+
agents: ["ag_one", "ag_two"],
|
|
246
|
+
});
|
|
247
|
+
const gw = toGatewayConfig(cfg);
|
|
248
|
+
expect(gw.channels).toHaveLength(2);
|
|
249
|
+
expect(gw.channels[0]).toEqual({
|
|
250
|
+
id: "ag_one",
|
|
251
|
+
type: BOTCORD_CHANNEL_TYPE,
|
|
252
|
+
accountId: "ag_one",
|
|
253
|
+
agentId: "ag_one",
|
|
254
|
+
});
|
|
255
|
+
expect(gw.channels[1]).toEqual({
|
|
256
|
+
id: "ag_two",
|
|
257
|
+
type: BOTCORD_CHANNEL_TYPE,
|
|
258
|
+
accountId: "ag_two",
|
|
259
|
+
agentId: "ag_two",
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("prefers `agents` when both `agents` and legacy `agentId` are present", () => {
|
|
264
|
+
// agentId here isn't in agents — the resolver warns on stderr and
|
|
265
|
+
// proceeds with agents. We silence the warning to keep test output clean.
|
|
266
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
267
|
+
try {
|
|
268
|
+
const cfg = baseConfig({
|
|
269
|
+
agentId: "ag_legacy",
|
|
270
|
+
agents: ["ag_one", "ag_two"],
|
|
271
|
+
});
|
|
272
|
+
const gw = toGatewayConfig(cfg);
|
|
273
|
+
expect(gw.channels.map((c) => c.id)).toEqual(["ag_one", "ag_two"]);
|
|
274
|
+
expect(warn).toHaveBeenCalled();
|
|
275
|
+
} finally {
|
|
276
|
+
warn.mockRestore();
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("deduplicates repeated agent ids while preserving order", () => {
|
|
281
|
+
const cfg = baseConfig({
|
|
282
|
+
agentId: undefined,
|
|
283
|
+
agents: ["ag_one", "ag_two", "ag_one"],
|
|
284
|
+
});
|
|
285
|
+
const gw = toGatewayConfig(cfg);
|
|
286
|
+
expect(gw.channels.map((c) => c.id)).toEqual(["ag_one", "ag_two"]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("throws when neither `agents` nor `agentId` is present (and no explicit agentIds override)", () => {
|
|
290
|
+
const cfg = baseConfig({ agentId: undefined });
|
|
291
|
+
// baseConfig leaves `agents` undefined, so resolveAgentIds should reject
|
|
292
|
+
// when no opts.agentIds is supplied. (P1 callers pass boot agents in.)
|
|
293
|
+
expect(() => toGatewayConfig(cfg)).toThrow(/missing agents/);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("accepts an explicit agentIds override (P1: boot agents injected by daemon)", () => {
|
|
297
|
+
const cfg = baseConfig({ agentId: undefined });
|
|
298
|
+
const gw = toGatewayConfig(cfg, { agentIds: ["ag_discovered"] });
|
|
299
|
+
expect(gw.channels.map((c) => c.id)).toEqual(["ag_discovered"]);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("emits zero channels when the injected agentIds list is empty", () => {
|
|
303
|
+
// When credentials discovery finds nothing the daemon still boots with
|
|
304
|
+
// no channels — `toGatewayConfig` must not throw in that case.
|
|
305
|
+
const cfg = baseConfig({ agentId: undefined });
|
|
306
|
+
const gw = toGatewayConfig(cfg, { agentIds: [] });
|
|
307
|
+
expect(gw.channels).toEqual([]);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("routes user-authored cfg.routes[] through to GatewayConfig.routes unchanged", () => {
|
|
311
|
+
const cfg = baseConfig({
|
|
312
|
+
agentId: undefined,
|
|
313
|
+
agents: ["ag_one"],
|
|
314
|
+
routes: [
|
|
315
|
+
{ match: { roomPrefix: "rm_oc_" }, adapter: "claude-code", cwd: "/work" },
|
|
316
|
+
],
|
|
317
|
+
});
|
|
318
|
+
const gw = toGatewayConfig(cfg, {
|
|
319
|
+
agentIds: ["ag_one"],
|
|
320
|
+
agentRuntimes: { ag_one: { runtime: "codex", cwd: "/home/alice/ag_one" } },
|
|
321
|
+
});
|
|
322
|
+
// User route stays in `routes[]` exactly as translated from `cfg.routes`.
|
|
323
|
+
expect(gw.routes).toHaveLength(1);
|
|
324
|
+
expect(gw.routes![0].match).toEqual({ conversationPrefix: "rm_oc_" });
|
|
325
|
+
// Synthesized per-agent route lives in `managedRoutes`, not `routes`.
|
|
326
|
+
expect(gw.managedRoutes).toEqual([
|
|
327
|
+
{
|
|
328
|
+
match: { accountId: "ag_one" },
|
|
329
|
+
runtime: "codex",
|
|
330
|
+
cwd: "/home/alice/ag_one",
|
|
331
|
+
},
|
|
332
|
+
]);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("synthesizes managed routes into GatewayConfig.managedRoutes, not GatewayConfig.routes", () => {
|
|
336
|
+
const cfg = baseConfig({
|
|
337
|
+
agentId: undefined,
|
|
338
|
+
agents: ["ag_one", "ag_two"],
|
|
339
|
+
});
|
|
340
|
+
const gw = toGatewayConfig(cfg, {
|
|
341
|
+
agentIds: ["ag_one", "ag_two"],
|
|
342
|
+
agentRuntimes: {
|
|
343
|
+
ag_one: { runtime: "codex", cwd: "/home/alice/ag_one" },
|
|
344
|
+
// ag_two deliberately missing — should still get a managed route.
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
expect(gw.routes).toEqual([]);
|
|
348
|
+
expect(gw.managedRoutes).toHaveLength(2);
|
|
349
|
+
expect(gw.managedRoutes![0]).toEqual({
|
|
350
|
+
match: { accountId: "ag_one" },
|
|
351
|
+
runtime: "codex",
|
|
352
|
+
cwd: "/home/alice/ag_one",
|
|
353
|
+
});
|
|
354
|
+
expect(gw.managedRoutes![1]).toEqual({
|
|
355
|
+
match: { accountId: "ag_two" },
|
|
356
|
+
runtime: "claude-code",
|
|
357
|
+
cwd: agentWorkspaceDir("ag_two"),
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe("buildManagedRoutes", () => {
|
|
363
|
+
const defaultRoute: GatewayRoute = {
|
|
364
|
+
runtime: "claude-code",
|
|
365
|
+
cwd: "/home/default",
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
it("uses agentRuntimes[id].cwd when set", () => {
|
|
369
|
+
const map = buildManagedRoutes(
|
|
370
|
+
["ag_one"],
|
|
371
|
+
{ ag_one: { runtime: "codex", cwd: "/custom/ag_one" } },
|
|
372
|
+
defaultRoute,
|
|
373
|
+
);
|
|
374
|
+
expect(map.get("ag_one")).toEqual({
|
|
375
|
+
match: { accountId: "ag_one" },
|
|
376
|
+
runtime: "codex",
|
|
377
|
+
cwd: "/custom/ag_one",
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("falls back to agentWorkspaceDir(id) when meta has no cwd", () => {
|
|
382
|
+
const map = buildManagedRoutes(
|
|
383
|
+
["ag_one"],
|
|
384
|
+
{ ag_one: { runtime: "codex" } },
|
|
385
|
+
defaultRoute,
|
|
386
|
+
);
|
|
387
|
+
expect(map.get("ag_one")?.cwd).toBe(agentWorkspaceDir("ag_one"));
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("falls back to defaultRoute.runtime when meta has no runtime (behavior change from pre-plan guard)", () => {
|
|
391
|
+
// Previously the synthesized route was only emitted when meta.runtime
|
|
392
|
+
// was truthy; agents without a cached runtime were silently skipped.
|
|
393
|
+
// Plan §10 makes the synthesis universal so every agent lands in its
|
|
394
|
+
// own workspace by default.
|
|
395
|
+
const map = buildManagedRoutes(["ag_one"], {}, defaultRoute);
|
|
396
|
+
expect(map.get("ag_one")).toEqual({
|
|
397
|
+
match: { accountId: "ag_one" },
|
|
398
|
+
runtime: "claude-code",
|
|
399
|
+
cwd: agentWorkspaceDir("ag_one"),
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("preserves agentIds insertion order in the returned map", () => {
|
|
404
|
+
const map = buildManagedRoutes(
|
|
405
|
+
["ag_b", "ag_a", "ag_c"],
|
|
406
|
+
{},
|
|
407
|
+
defaultRoute,
|
|
408
|
+
);
|
|
409
|
+
expect(Array.from(map.keys())).toEqual(["ag_b", "ag_a", "ag_c"]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("emits an empty map when agentIds is empty", () => {
|
|
413
|
+
const map = buildManagedRoutes([], {}, defaultRoute);
|
|
414
|
+
expect(map.size).toBe(0);
|
|
415
|
+
});
|
|
416
|
+
});
|