@lobu/gateway 3.0.9 → 3.0.12

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 (210) hide show
  1. package/dist/api/platform.d.ts.map +1 -1
  2. package/dist/api/platform.js +7 -26
  3. package/dist/api/platform.js.map +1 -1
  4. package/dist/auth/mcp/proxy.d.ts +14 -0
  5. package/dist/auth/mcp/proxy.d.ts.map +1 -1
  6. package/dist/auth/mcp/proxy.js +149 -13
  7. package/dist/auth/mcp/proxy.js.map +1 -1
  8. package/dist/cli/gateway.d.ts.map +1 -1
  9. package/dist/cli/gateway.js +29 -0
  10. package/dist/cli/gateway.js.map +1 -1
  11. package/dist/connections/chat-instance-manager.d.ts.map +1 -1
  12. package/dist/connections/chat-instance-manager.js +2 -1
  13. package/dist/connections/chat-instance-manager.js.map +1 -1
  14. package/dist/connections/interaction-bridge.d.ts +9 -2
  15. package/dist/connections/interaction-bridge.d.ts.map +1 -1
  16. package/dist/connections/interaction-bridge.js +121 -261
  17. package/dist/connections/interaction-bridge.js.map +1 -1
  18. package/dist/interactions.d.ts +9 -43
  19. package/dist/interactions.d.ts.map +1 -1
  20. package/dist/interactions.js +10 -52
  21. package/dist/interactions.js.map +1 -1
  22. package/dist/routes/public/agent.d.ts +4 -0
  23. package/dist/routes/public/agent.d.ts.map +1 -1
  24. package/dist/routes/public/agent.js +21 -0
  25. package/dist/routes/public/agent.js.map +1 -1
  26. package/dist/services/core-services.d.ts.map +1 -1
  27. package/dist/services/core-services.js +4 -0
  28. package/dist/services/core-services.js.map +1 -1
  29. package/package.json +9 -9
  30. package/src/__tests__/agent-config-routes.test.ts +0 -254
  31. package/src/__tests__/agent-history-routes.test.ts +0 -72
  32. package/src/__tests__/agent-routes.test.ts +0 -68
  33. package/src/__tests__/agent-schedules-routes.test.ts +0 -59
  34. package/src/__tests__/agent-settings-store.test.ts +0 -323
  35. package/src/__tests__/bedrock-model-catalog.test.ts +0 -40
  36. package/src/__tests__/bedrock-openai-service.test.ts +0 -157
  37. package/src/__tests__/bedrock-provider-module.test.ts +0 -56
  38. package/src/__tests__/chat-instance-manager-slack.test.ts +0 -204
  39. package/src/__tests__/chat-response-bridge.test.ts +0 -131
  40. package/src/__tests__/config-memory-plugins.test.ts +0 -92
  41. package/src/__tests__/config-request-store.test.ts +0 -127
  42. package/src/__tests__/connection-routes.test.ts +0 -144
  43. package/src/__tests__/core-services-store-selection.test.ts +0 -92
  44. package/src/__tests__/docker-deployment.test.ts +0 -1211
  45. package/src/__tests__/embedded-deployment.test.ts +0 -342
  46. package/src/__tests__/grant-store.test.ts +0 -148
  47. package/src/__tests__/http-proxy.test.ts +0 -281
  48. package/src/__tests__/instruction-service.test.ts +0 -37
  49. package/src/__tests__/link-buttons.test.ts +0 -112
  50. package/src/__tests__/lobu.test.ts +0 -32
  51. package/src/__tests__/mcp-config-service.test.ts +0 -347
  52. package/src/__tests__/mcp-proxy.test.ts +0 -694
  53. package/src/__tests__/message-handler-bridge.test.ts +0 -17
  54. package/src/__tests__/model-selection.test.ts +0 -172
  55. package/src/__tests__/oauth-templates.test.ts +0 -39
  56. package/src/__tests__/platform-adapter-slack-send.test.ts +0 -114
  57. package/src/__tests__/platform-helpers-model-resolution.test.ts +0 -253
  58. package/src/__tests__/provider-inheritance.test.ts +0 -212
  59. package/src/__tests__/routes/cli-auth.test.ts +0 -337
  60. package/src/__tests__/routes/interactions.test.ts +0 -121
  61. package/src/__tests__/secret-proxy.test.ts +0 -85
  62. package/src/__tests__/session-manager.test.ts +0 -572
  63. package/src/__tests__/setup.ts +0 -133
  64. package/src/__tests__/skill-and-mcp-registry.test.ts +0 -203
  65. package/src/__tests__/slack-routes.test.ts +0 -161
  66. package/src/__tests__/system-config-resolver.test.ts +0 -75
  67. package/src/__tests__/system-message-limiter.test.ts +0 -89
  68. package/src/__tests__/system-skills-service.test.ts +0 -362
  69. package/src/__tests__/transcription-service.test.ts +0 -222
  70. package/src/__tests__/utils/rate-limiter.test.ts +0 -102
  71. package/src/__tests__/worker-connection-manager.test.ts +0 -497
  72. package/src/__tests__/worker-job-router.test.ts +0 -722
  73. package/src/api/index.ts +0 -1
  74. package/src/api/platform.ts +0 -292
  75. package/src/api/response-renderer.ts +0 -157
  76. package/src/auth/agent-metadata-store.ts +0 -168
  77. package/src/auth/api-auth-middleware.ts +0 -69
  78. package/src/auth/api-key-provider-module.ts +0 -213
  79. package/src/auth/base-provider-module.ts +0 -201
  80. package/src/auth/bedrock/provider-module.ts +0 -110
  81. package/src/auth/chatgpt/chatgpt-oauth-module.ts +0 -185
  82. package/src/auth/chatgpt/device-code-client.ts +0 -218
  83. package/src/auth/chatgpt/index.ts +0 -1
  84. package/src/auth/claude/oauth-module.ts +0 -280
  85. package/src/auth/cli/token-service.ts +0 -249
  86. package/src/auth/external/client.ts +0 -560
  87. package/src/auth/external/device-code-client.ts +0 -235
  88. package/src/auth/mcp/config-service.ts +0 -420
  89. package/src/auth/mcp/proxy.ts +0 -1086
  90. package/src/auth/mcp/string-substitution.ts +0 -17
  91. package/src/auth/mcp/tool-cache.ts +0 -90
  92. package/src/auth/oauth/base-client.ts +0 -267
  93. package/src/auth/oauth/client.ts +0 -153
  94. package/src/auth/oauth/credentials.ts +0 -7
  95. package/src/auth/oauth/providers.ts +0 -69
  96. package/src/auth/oauth/state-store.ts +0 -150
  97. package/src/auth/oauth-templates.ts +0 -179
  98. package/src/auth/provider-catalog.ts +0 -220
  99. package/src/auth/provider-model-options.ts +0 -41
  100. package/src/auth/settings/agent-settings-store.ts +0 -565
  101. package/src/auth/settings/auth-profiles-manager.ts +0 -216
  102. package/src/auth/settings/index.ts +0 -12
  103. package/src/auth/settings/model-preference-store.ts +0 -52
  104. package/src/auth/settings/model-selection.ts +0 -135
  105. package/src/auth/settings/resolved-settings-view.ts +0 -298
  106. package/src/auth/settings/template-utils.ts +0 -44
  107. package/src/auth/settings/token-service.ts +0 -88
  108. package/src/auth/system-env-store.ts +0 -98
  109. package/src/auth/user-agents-store.ts +0 -68
  110. package/src/channels/binding-service.ts +0 -214
  111. package/src/channels/index.ts +0 -4
  112. package/src/cli/gateway.ts +0 -1312
  113. package/src/cli/index.ts +0 -74
  114. package/src/commands/built-in-commands.ts +0 -80
  115. package/src/commands/command-dispatcher.ts +0 -94
  116. package/src/commands/command-reply-adapters.ts +0 -27
  117. package/src/config/file-loader.ts +0 -618
  118. package/src/config/index.ts +0 -588
  119. package/src/config/network-allowlist.ts +0 -71
  120. package/src/connections/chat-instance-manager.ts +0 -1284
  121. package/src/connections/chat-response-bridge.ts +0 -618
  122. package/src/connections/index.ts +0 -7
  123. package/src/connections/interaction-bridge.ts +0 -831
  124. package/src/connections/message-handler-bridge.ts +0 -440
  125. package/src/connections/platform-auth-methods.ts +0 -15
  126. package/src/connections/types.ts +0 -84
  127. package/src/gateway/connection-manager.ts +0 -291
  128. package/src/gateway/index.ts +0 -698
  129. package/src/gateway/job-router.ts +0 -201
  130. package/src/gateway-main.ts +0 -200
  131. package/src/index.ts +0 -41
  132. package/src/infrastructure/queue/index.ts +0 -12
  133. package/src/infrastructure/queue/queue-producer.ts +0 -148
  134. package/src/infrastructure/queue/redis-queue.ts +0 -361
  135. package/src/infrastructure/queue/types.ts +0 -133
  136. package/src/infrastructure/redis/system-message-limiter.ts +0 -94
  137. package/src/interactions/config-request-store.ts +0 -198
  138. package/src/interactions.ts +0 -363
  139. package/src/lobu.ts +0 -311
  140. package/src/metrics/prometheus.ts +0 -159
  141. package/src/modules/module-system.ts +0 -179
  142. package/src/orchestration/base-deployment-manager.ts +0 -900
  143. package/src/orchestration/deployment-utils.ts +0 -98
  144. package/src/orchestration/impl/docker-deployment.ts +0 -620
  145. package/src/orchestration/impl/embedded-deployment.ts +0 -268
  146. package/src/orchestration/impl/index.ts +0 -8
  147. package/src/orchestration/impl/k8s/deployment.ts +0 -1061
  148. package/src/orchestration/impl/k8s/helpers.ts +0 -610
  149. package/src/orchestration/impl/k8s/index.ts +0 -1
  150. package/src/orchestration/index.ts +0 -333
  151. package/src/orchestration/message-consumer.ts +0 -584
  152. package/src/orchestration/scheduled-wakeup.ts +0 -704
  153. package/src/permissions/approval-policy.ts +0 -36
  154. package/src/permissions/grant-store.ts +0 -219
  155. package/src/platform/file-handler.ts +0 -66
  156. package/src/platform/link-buttons.ts +0 -57
  157. package/src/platform/renderer-utils.ts +0 -44
  158. package/src/platform/response-renderer.ts +0 -84
  159. package/src/platform/unified-thread-consumer.ts +0 -194
  160. package/src/platform.ts +0 -318
  161. package/src/proxy/http-proxy.ts +0 -752
  162. package/src/proxy/proxy-manager.ts +0 -81
  163. package/src/proxy/secret-proxy.ts +0 -402
  164. package/src/proxy/token-refresh-job.ts +0 -143
  165. package/src/routes/internal/audio.ts +0 -141
  166. package/src/routes/internal/device-auth.ts +0 -652
  167. package/src/routes/internal/files.ts +0 -226
  168. package/src/routes/internal/history.ts +0 -69
  169. package/src/routes/internal/images.ts +0 -127
  170. package/src/routes/internal/interactions.ts +0 -84
  171. package/src/routes/internal/middleware.ts +0 -23
  172. package/src/routes/internal/schedule.ts +0 -226
  173. package/src/routes/internal/types.ts +0 -22
  174. package/src/routes/openapi-auto.ts +0 -239
  175. package/src/routes/public/agent-access.ts +0 -23
  176. package/src/routes/public/agent-config.ts +0 -675
  177. package/src/routes/public/agent-history.ts +0 -422
  178. package/src/routes/public/agent-schedules.ts +0 -296
  179. package/src/routes/public/agent.ts +0 -1086
  180. package/src/routes/public/agents.ts +0 -373
  181. package/src/routes/public/channels.ts +0 -191
  182. package/src/routes/public/cli-auth.ts +0 -896
  183. package/src/routes/public/connections.ts +0 -574
  184. package/src/routes/public/landing.ts +0 -16
  185. package/src/routes/public/oauth.ts +0 -147
  186. package/src/routes/public/settings-auth.ts +0 -104
  187. package/src/routes/public/slack.ts +0 -173
  188. package/src/routes/shared/agent-ownership.ts +0 -101
  189. package/src/routes/shared/token-verifier.ts +0 -34
  190. package/src/services/bedrock-model-catalog.ts +0 -217
  191. package/src/services/bedrock-openai-service.ts +0 -658
  192. package/src/services/core-services.ts +0 -1072
  193. package/src/services/image-generation-service.ts +0 -257
  194. package/src/services/instruction-service.ts +0 -318
  195. package/src/services/mcp-registry.ts +0 -94
  196. package/src/services/platform-helpers.ts +0 -287
  197. package/src/services/session-manager.ts +0 -262
  198. package/src/services/settings-resolver.ts +0 -74
  199. package/src/services/system-config-resolver.ts +0 -89
  200. package/src/services/system-skills-service.ts +0 -229
  201. package/src/services/transcription-service.ts +0 -684
  202. package/src/session.ts +0 -110
  203. package/src/spaces/index.ts +0 -1
  204. package/src/spaces/space-resolver.ts +0 -17
  205. package/src/stores/in-memory-agent-store.ts +0 -403
  206. package/src/stores/redis-agent-store.ts +0 -279
  207. package/src/utils/public-url.ts +0 -44
  208. package/src/utils/rate-limiter.ts +0 -94
  209. package/tsconfig.json +0 -33
  210. package/tsconfig.tsbuildinfo +0 -1
@@ -1,1211 +0,0 @@
1
- import {
2
- afterEach,
3
- beforeEach,
4
- describe,
5
- expect,
6
- mock,
7
- spyOn,
8
- test,
9
- } from "bun:test";
10
- import fs from "node:fs";
11
- import type {
12
- MessagePayload,
13
- OrchestratorConfig,
14
- } from "../orchestration/base-deployment-manager";
15
-
16
- // ---------------------------------------------------------------------------
17
- // Mock dockerode
18
- // ---------------------------------------------------------------------------
19
-
20
- const mockContainer = {
21
- id: "container-id-123",
22
- start: mock(async () => {
23
- /* noop */
24
- }),
25
- stop: mock(async () => {
26
- /* noop */
27
- }),
28
- remove: mock(async () => {
29
- /* noop */
30
- }),
31
- inspect: mock(async () => ({ State: { Running: true } })),
32
- wait: mock(async () => {
33
- /* noop */
34
- }),
35
- };
36
-
37
- const mockVolume = {
38
- inspect: mock(async () => ({})),
39
- };
40
-
41
- const mockNetwork = {
42
- inspect: mock(async () => ({ Internal: true })),
43
- connect: mock(async () => {
44
- /* noop */
45
- }),
46
- };
47
-
48
- const mockDocker = {
49
- info: mock(async () => ({ Runtimes: {} })),
50
- createContainer: mock(async () => mockContainer),
51
- getContainer: mock(() => mockContainer),
52
- createVolume: mock(async () => mockVolume),
53
- getVolume: mock(() => mockVolume),
54
- createNetwork: mock(async () => mockNetwork),
55
- getNetwork: mock(() => mockNetwork),
56
- listContainers: mock(async () => []),
57
- getImage: mock(() => ({ inspect: mock(async () => ({})) })),
58
- pull: mock((_name: string, cb: Function) =>
59
- cb(null, {
60
- on: () => {
61
- /* noop */
62
- },
63
- })
64
- ),
65
- modem: { followProgress: (_stream: any, cb: Function) => cb(null) },
66
- };
67
-
68
- mock.module("dockerode", () => ({
69
- default: class MockDocker {
70
- constructor() {
71
- Object.assign(this, mockDocker);
72
- }
73
- },
74
- }));
75
-
76
- // Must import after mocks are set up
77
- const { DockerDeploymentManager } = await import(
78
- "../orchestration/impl/docker-deployment"
79
- );
80
-
81
- // ---------------------------------------------------------------------------
82
- // Constants & helpers
83
- // ---------------------------------------------------------------------------
84
-
85
- const TEST_ENCRYPTION_KEY =
86
- "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
87
-
88
- const TEST_CONFIG: OrchestratorConfig = {
89
- queues: {
90
- connectionString: "redis://localhost:6379",
91
- retryLimit: 3,
92
- retryDelay: 5,
93
- expireInSeconds: 300,
94
- },
95
- worker: {
96
- image: {
97
- repository: "lobu-worker",
98
- tag: "latest",
99
- pullPolicy: "IfNotPresent",
100
- },
101
- resources: {
102
- requests: { cpu: "100m", memory: "128Mi" },
103
- limits: { cpu: "500m", memory: "512Mi" },
104
- },
105
- idleCleanupMinutes: 30,
106
- maxDeployments: 10,
107
- },
108
- kubernetes: { namespace: "default" },
109
- cleanup: { initialDelayMs: 5000, intervalMs: 60000, veryOldDays: 7 },
110
- };
111
-
112
- function createTestMessagePayload(
113
- overrides?: Partial<MessagePayload>
114
- ): MessagePayload {
115
- return {
116
- userId: "user-1",
117
- conversationId: "conv-1",
118
- channelId: "ch-1",
119
- messageId: "msg-1",
120
- teamId: "team-1",
121
- agentId: "test-agent",
122
- botId: "bot-1",
123
- platform: "slack",
124
- messageText: "hello",
125
- platformMetadata: {},
126
- agentOptions: {},
127
- ...overrides,
128
- } as MessagePayload;
129
- }
130
-
131
- const MOCK_DEFAULTS: Array<
132
- [{ mockReset: Function; mockImplementation: Function }, Function]
133
- > = [
134
- [
135
- mockContainer.start,
136
- async () => {
137
- /* noop */
138
- },
139
- ],
140
- [
141
- mockContainer.stop,
142
- async () => {
143
- /* noop */
144
- },
145
- ],
146
- [
147
- mockContainer.remove,
148
- async () => {
149
- /* noop */
150
- },
151
- ],
152
- [mockContainer.inspect, async () => ({ State: { Running: true } })],
153
- [mockDocker.info, async () => ({ Runtimes: {} })],
154
- [mockDocker.createContainer, async () => mockContainer],
155
- [mockDocker.getContainer, () => mockContainer],
156
- [mockDocker.createVolume, async () => mockVolume],
157
- [mockDocker.getVolume, () => mockVolume],
158
- [mockDocker.createNetwork, async () => mockNetwork],
159
- [mockDocker.getNetwork, () => mockNetwork],
160
- [mockDocker.listContainers, async () => []],
161
- [mockDocker.getImage, () => ({ inspect: mock(async () => ({})) })],
162
- [
163
- mockDocker.pull,
164
- (_name: string, cb: Function) =>
165
- cb(null, {
166
- on: () => {
167
- /* noop */
168
- },
169
- }),
170
- ],
171
- [mockVolume.inspect, async () => ({})],
172
- [mockNetwork.inspect, async () => ({ Internal: true })],
173
- [
174
- mockNetwork.connect,
175
- async () => {
176
- /* noop */
177
- },
178
- ],
179
- ];
180
-
181
- function resetAllMocks() {
182
- for (const [mockFn, impl] of MOCK_DEFAULTS) {
183
- mockFn.mockReset();
184
- mockFn.mockImplementation(impl);
185
- }
186
- }
187
-
188
- /** Extract the main container creation options (skips init containers like alpine). */
189
- function getMainCreateOpts(): any {
190
- const call = mockDocker.createContainer.mock.calls.find(
191
- (c: any) => c[0]?.name === "test-deploy"
192
- );
193
- return call?.[0];
194
- }
195
-
196
- // ---------------------------------------------------------------------------
197
- // Test suite
198
- // ---------------------------------------------------------------------------
199
-
200
- describe("DockerDeploymentManager", () => {
201
- let savedEnv: NodeJS.ProcessEnv;
202
-
203
- beforeEach(() => {
204
- savedEnv = { ...process.env };
205
- process.env.ENCRYPTION_KEY = TEST_ENCRYPTION_KEY;
206
- resetAllMocks();
207
- });
208
-
209
- afterEach(() => {
210
- process.env = savedEnv;
211
- });
212
-
213
- function createManager(configOverrides?: Partial<OrchestratorConfig>) {
214
- const config = { ...TEST_CONFIG, ...configOverrides };
215
- return new DockerDeploymentManager(config);
216
- }
217
-
218
- // =========================================================================
219
- // ResourceParser (tested indirectly via createContainer args)
220
- // =========================================================================
221
-
222
- describe("ResourceParser (via createDeployment)", () => {
223
- test("parseMemory: 512Mi -> 512 * 1024 * 1024", async () => {
224
- const manager = createManager({
225
- worker: {
226
- ...TEST_CONFIG.worker,
227
- resources: {
228
- requests: { cpu: "100m", memory: "128Mi" },
229
- limits: { cpu: "500m", memory: "512Mi" },
230
- },
231
- },
232
- });
233
-
234
- await manager.createDeployment(
235
- "test-deploy",
236
- "user",
237
- "user-id",
238
- createTestMessagePayload()
239
- );
240
-
241
- const opts = mockDocker.createContainer.mock.calls[0]?.[0] as any;
242
- expect(opts.HostConfig.Memory).toBe(512 * 1024 * 1024);
243
- });
244
-
245
- test("parseMemory: 1Gi -> 1024 * 1024 * 1024", async () => {
246
- const manager = createManager({
247
- worker: {
248
- ...TEST_CONFIG.worker,
249
- resources: {
250
- requests: { cpu: "100m", memory: "128Mi" },
251
- limits: { cpu: "1", memory: "1Gi" },
252
- },
253
- },
254
- });
255
-
256
- await manager.createDeployment(
257
- "test-deploy",
258
- "user",
259
- "user-id",
260
- createTestMessagePayload()
261
- );
262
-
263
- const opts = mockDocker.createContainer.mock.calls[0]?.[0] as any;
264
- expect(opts.HostConfig.Memory).toBe(1024 * 1024 * 1024);
265
- });
266
-
267
- test("parseCpu: 500m -> 500_000_000 nanocpus", async () => {
268
- const manager = createManager();
269
-
270
- await manager.createDeployment(
271
- "test-deploy",
272
- "user",
273
- "user-id",
274
- createTestMessagePayload()
275
- );
276
-
277
- const opts = mockDocker.createContainer.mock.calls[0]?.[0] as any;
278
- expect(opts.HostConfig.NanoCpus).toBe(500_000_000);
279
- });
280
-
281
- test("parseCpu: 1 core -> 1_000_000_000 nanocpus", async () => {
282
- const manager = createManager({
283
- worker: {
284
- ...TEST_CONFIG.worker,
285
- resources: {
286
- requests: { cpu: "100m", memory: "128Mi" },
287
- limits: { cpu: "1", memory: "512Mi" },
288
- },
289
- },
290
- });
291
-
292
- await manager.createDeployment(
293
- "test-deploy",
294
- "user",
295
- "user-id",
296
- createTestMessagePayload()
297
- );
298
-
299
- const opts = mockDocker.createContainer.mock.calls[0]?.[0] as any;
300
- expect(opts.HostConfig.NanoCpus).toBe(1_000_000_000);
301
- });
302
- });
303
-
304
- // =========================================================================
305
- // Container creation & security
306
- // =========================================================================
307
-
308
- describe("createDeployment", () => {
309
- test("calls docker.createContainer with correct image", async () => {
310
- const manager = createManager();
311
- await manager.createDeployment(
312
- "test-deploy",
313
- "user",
314
- "user-id",
315
- createTestMessagePayload()
316
- );
317
-
318
- const opts = getMainCreateOpts();
319
- expect(opts).toBeDefined();
320
- expect(opts.Image).toBe("lobu-worker:latest");
321
- });
322
-
323
- test("drops all capabilities: CapDrop=['ALL']", async () => {
324
- const manager = createManager();
325
- await manager.createDeployment(
326
- "test-deploy",
327
- "user",
328
- "user-id",
329
- createTestMessagePayload()
330
- );
331
- expect(getMainCreateOpts().HostConfig.CapDrop).toEqual(["ALL"]);
332
- });
333
-
334
- test("adds configurable capabilities via WORKER_CAPABILITIES env var", async () => {
335
- process.env.WORKER_CAPABILITIES = "NET_BIND_SERVICE,SYS_PTRACE";
336
- const manager = createManager();
337
- await manager.createDeployment(
338
- "test-deploy",
339
- "user",
340
- "user-id",
341
- createTestMessagePayload()
342
- );
343
- expect(getMainCreateOpts().HostConfig.CapAdd).toEqual([
344
- "NET_BIND_SERVICE",
345
- "SYS_PTRACE",
346
- ]);
347
- });
348
-
349
- test("empty CapAdd when WORKER_CAPABILITIES not set", async () => {
350
- delete process.env.WORKER_CAPABILITIES;
351
- const manager = createManager();
352
- await manager.createDeployment(
353
- "test-deploy",
354
- "user",
355
- "user-id",
356
- createTestMessagePayload()
357
- );
358
- expect(getMainCreateOpts().HostConfig.CapAdd).toEqual([]);
359
- });
360
-
361
- test("enables no-new-privileges security option", async () => {
362
- const manager = createManager();
363
- await manager.createDeployment(
364
- "test-deploy",
365
- "user",
366
- "user-id",
367
- createTestMessagePayload()
368
- );
369
- expect(getMainCreateOpts().HostConfig.SecurityOpt).toContain(
370
- "no-new-privileges:true"
371
- );
372
- });
373
-
374
- test("uses readonly rootfs by default", async () => {
375
- const manager = createManager();
376
- await manager.createDeployment(
377
- "test-deploy",
378
- "user",
379
- "user-id",
380
- createTestMessagePayload()
381
- );
382
- expect(getMainCreateOpts().HostConfig.ReadonlyRootfs).toBe(true);
383
- });
384
-
385
- test("disables readonly rootfs when Nix packages configured", async () => {
386
- const manager = createManager();
387
- await manager.createDeployment(
388
- "test-deploy",
389
- "user",
390
- "user-id",
391
- createTestMessagePayload({ nixConfig: { packages: ["nodejs"] } })
392
- );
393
- expect(getMainCreateOpts().HostConfig.ReadonlyRootfs).toBe(false);
394
- });
395
-
396
- test("disables readonly rootfs when Nix flakeUrl configured", async () => {
397
- const manager = createManager();
398
- await manager.createDeployment(
399
- "test-deploy",
400
- "user",
401
- "user-id",
402
- createTestMessagePayload({
403
- nixConfig: { flakeUrl: "github:owner/repo" },
404
- })
405
- );
406
- expect(getMainCreateOpts().HostConfig.ReadonlyRootfs).toBe(false);
407
- });
408
-
409
- test("adds tmpfs for /tmp when readonly rootfs enabled", async () => {
410
- const manager = createManager();
411
- await manager.createDeployment(
412
- "test-deploy",
413
- "user",
414
- "user-id",
415
- createTestMessagePayload()
416
- );
417
- expect(getMainCreateOpts().HostConfig.Tmpfs).toEqual({
418
- "/tmp": "rw,noexec,nosuid,size=100m",
419
- });
420
- });
421
-
422
- test("does not add tmpfs when Nix packages configured", async () => {
423
- const manager = createManager();
424
- await manager.createDeployment(
425
- "test-deploy",
426
- "user",
427
- "user-id",
428
- createTestMessagePayload({ nixConfig: { packages: ["nodejs"] } })
429
- );
430
- expect(getMainCreateOpts().HostConfig.Tmpfs).toBeUndefined();
431
- });
432
-
433
- test("sets ShmSize to 256MB (268435456)", async () => {
434
- const manager = createManager();
435
- await manager.createDeployment(
436
- "test-deploy",
437
- "user",
438
- "user-id",
439
- createTestMessagePayload()
440
- );
441
- expect(getMainCreateOpts().HostConfig.ShmSize).toBe(268435456);
442
- });
443
-
444
- test("uses gvisor runtime when available", async () => {
445
- mockDocker.info.mockImplementation(async () => ({
446
- Runtimes: { runsc: {} },
447
- }));
448
- const manager = createManager();
449
- await new Promise((r) => setTimeout(r, 50));
450
-
451
- await manager.createDeployment(
452
- "test-deploy",
453
- "user",
454
- "user-id",
455
- createTestMessagePayload()
456
- );
457
- expect(getMainCreateOpts().HostConfig.Runtime).toBe("runsc");
458
- });
459
-
460
- test("uses default runtime when gvisor unavailable", async () => {
461
- const manager = createManager();
462
- await new Promise((r) => setTimeout(r, 50));
463
-
464
- await manager.createDeployment(
465
- "test-deploy",
466
- "user",
467
- "user-id",
468
- createTestMessagePayload()
469
- );
470
- expect(getMainCreateOpts().HostConfig.Runtime).toBeUndefined();
471
- });
472
-
473
- test("starts container after creation", async () => {
474
- const manager = createManager();
475
- await manager.createDeployment(
476
- "test-deploy",
477
- "user",
478
- "user-id",
479
- createTestMessagePayload()
480
- );
481
- expect(mockContainer.start).toHaveBeenCalled();
482
- });
483
-
484
- test("removes container if start fails", async () => {
485
- const removeMock = mock(async () => {
486
- /* noop */
487
- });
488
- mockDocker.createContainer.mockImplementation(async (opts: any) => {
489
- if (opts?.name === "test-deploy") {
490
- return {
491
- ...mockContainer,
492
- id: "failed-container",
493
- start: mock(async () => {
494
- throw new Error("start failed");
495
- }),
496
- remove: removeMock,
497
- };
498
- }
499
- return mockContainer;
500
- });
501
-
502
- const manager = createManager();
503
-
504
- await expect(
505
- manager.createDeployment(
506
- "test-deploy",
507
- "user",
508
- "user-id",
509
- createTestMessagePayload()
510
- )
511
- ).rejects.toThrow();
512
-
513
- expect(removeMock).toHaveBeenCalledWith({ force: true });
514
- });
515
-
516
- test("sets WorkingDir to /workspace", async () => {
517
- const manager = createManager();
518
- await manager.createDeployment(
519
- "test-deploy",
520
- "user",
521
- "user-id",
522
- createTestMessagePayload()
523
- );
524
- expect(getMainCreateOpts().WorkingDir).toBe("/workspace");
525
- });
526
- });
527
-
528
- // =========================================================================
529
- // Docker volume management
530
- // =========================================================================
531
-
532
- describe("volume management", () => {
533
- test("volume created as lobu-workspace-{agentId}", async () => {
534
- // Make getVolume throw so ensureVolume creates a new one
535
- mockDocker.getVolume.mockImplementation(() => ({
536
- inspect: mock(async () => {
537
- throw new Error("no such volume");
538
- }),
539
- }));
540
-
541
- const manager = createManager();
542
-
543
- await manager.createDeployment(
544
- "test-deploy",
545
- "user",
546
- "user-id",
547
- createTestMessagePayload({ agentId: "my-agent" })
548
- );
549
-
550
- expect(mockDocker.createVolume).toHaveBeenCalledWith(
551
- expect.objectContaining({
552
- Name: "lobu-workspace-my-agent",
553
- })
554
- );
555
- });
556
-
557
- test("volume shared across threads with same agentId", async () => {
558
- const manager = createManager();
559
-
560
- // First deployment
561
- await manager.createDeployment(
562
- "deploy-1",
563
- "user",
564
- "user-id",
565
- createTestMessagePayload({ agentId: "shared-agent" })
566
- );
567
-
568
- // Second deployment with same agentId
569
- await manager.createDeployment(
570
- "deploy-2",
571
- "user",
572
- "user-id",
573
- createTestMessagePayload({ agentId: "shared-agent" })
574
- );
575
-
576
- // Both main container calls should reference the same volume name
577
- const mainCalls = mockDocker.createContainer.mock.calls.filter(
578
- (call: any) =>
579
- call[0]?.name === "deploy-1" || call[0]?.name === "deploy-2"
580
- );
581
- expect(mainCalls.length).toBe(2);
582
-
583
- // In production mode (non-development), uses Mounts with volume
584
- for (const call of mainCalls) {
585
- const opts = call[0] as any;
586
- if (opts.HostConfig.Mounts) {
587
- expect(opts.HostConfig.Mounts[0].Source).toBe(
588
- "lobu-workspace-shared-agent"
589
- );
590
- }
591
- }
592
- });
593
-
594
- test("handles race condition on concurrent volume creation (409 conflict)", async () => {
595
- mockDocker.getVolume.mockImplementation(() => ({
596
- inspect: mock(async () => {
597
- throw new Error("no such volume");
598
- }),
599
- }));
600
- mockDocker.createVolume.mockImplementation(async () => {
601
- const err: any = new Error("already exists");
602
- err.statusCode = 409;
603
- throw err;
604
- });
605
-
606
- const manager = createManager();
607
-
608
- // Should not throw despite 409
609
- await expect(
610
- manager.createDeployment(
611
- "test-deploy",
612
- "user",
613
- "user-id",
614
- createTestMessagePayload()
615
- )
616
- ).resolves.toBeUndefined();
617
- });
618
-
619
- test("development mode uses bind mounts", async () => {
620
- process.env.NODE_ENV = "development";
621
- process.env.DEPLOYMENT_MODE = "docker";
622
- process.env.LOBU_DEV_PROJECT_PATH = "/app";
623
-
624
- const manager = createManager();
625
-
626
- await manager.createDeployment(
627
- "test-deploy",
628
- "user",
629
- "user-id",
630
- createTestMessagePayload({ agentId: "dev-agent" })
631
- );
632
-
633
- const mainCall = mockDocker.createContainer.mock.calls.find(
634
- (call: any) => call[0]?.name === "test-deploy"
635
- );
636
- const opts = mainCall![0] as any;
637
- expect(opts.HostConfig.Binds).toBeDefined();
638
- expect(opts.HostConfig.Binds[0]).toContain(
639
- "/app/workspaces/dev-agent:/workspace"
640
- );
641
- });
642
- });
643
-
644
- // =========================================================================
645
- // Docker network
646
- // =========================================================================
647
-
648
- describe("network management", () => {
649
- test("internal network created/checked with Internal flag", async () => {
650
- // The constructor calls ensureInternalNetwork
651
- delete process.env.WORKER_NETWORK;
652
- mockNetwork.inspect.mockImplementation(async () => ({ Internal: true }));
653
-
654
- createManager();
655
- // ensureInternalNetwork is fire-and-forget, give it time
656
- await new Promise((r) => setTimeout(r, 50));
657
-
658
- expect(mockDocker.getNetwork).toHaveBeenCalled();
659
- });
660
-
661
- test("WORKER_NETWORK env var overrides network name", async () => {
662
- process.env.WORKER_NETWORK = "custom-network";
663
- const manager = createManager();
664
- await manager.createDeployment(
665
- "test-deploy",
666
- "user",
667
- "user-id",
668
- createTestMessagePayload()
669
- );
670
- expect(getMainCreateOpts().HostConfig.NetworkMode).toBe("custom-network");
671
- });
672
-
673
- test("uses compose project name for default network", async () => {
674
- delete process.env.WORKER_NETWORK;
675
- process.env.COMPOSE_PROJECT_NAME = "myproject";
676
- const manager = createManager();
677
- await manager.createDeployment(
678
- "test-deploy",
679
- "user",
680
- "user-id",
681
- createTestMessagePayload()
682
- );
683
- expect(getMainCreateOpts().HostConfig.NetworkMode).toBe(
684
- "myproject_lobu-internal"
685
- );
686
- });
687
-
688
- test("host mode connects to public network too", async () => {
689
- delete process.env.WORKER_NETWORK;
690
- // Simulate running on host (not in container)
691
- delete process.env.CONTAINER;
692
- const existsSpy = spyOn(fs, "existsSync").mockReturnValue(false);
693
-
694
- const manager = createManager();
695
- await new Promise((r) => setTimeout(r, 50));
696
-
697
- await manager.createDeployment(
698
- "test-deploy",
699
- "user",
700
- "user-id",
701
- createTestMessagePayload()
702
- );
703
-
704
- // Should attempt to connect to public network
705
- expect(mockNetwork.connect).toHaveBeenCalled();
706
-
707
- existsSpy.mockRestore();
708
- });
709
- });
710
-
711
- // =========================================================================
712
- // Dispatcher host
713
- // =========================================================================
714
-
715
- describe("dispatcher host", () => {
716
- function getDispatcherUrlEnv() {
717
- return (getMainCreateOpts().Env as string[]).find((e: string) =>
718
- e.startsWith("DISPATCHER_URL=")
719
- );
720
- }
721
-
722
- test('returns "gateway" when running in container (/.dockerenv exists)', async () => {
723
- const existsSpy = spyOn(fs, "existsSync").mockImplementation(
724
- (p: any) => String(p) === "/.dockerenv"
725
- );
726
- const manager = createManager();
727
- await manager.createDeployment(
728
- "test-deploy",
729
- "user",
730
- "user-id",
731
- createTestMessagePayload()
732
- );
733
- expect(getDispatcherUrlEnv()).toContain("gateway");
734
- existsSpy.mockRestore();
735
- });
736
-
737
- test('returns "gateway" when CONTAINER=true', async () => {
738
- process.env.CONTAINER = "true";
739
- const existsSpy = spyOn(fs, "existsSync").mockReturnValue(false);
740
- const manager = createManager();
741
- await manager.createDeployment(
742
- "test-deploy",
743
- "user",
744
- "user-id",
745
- createTestMessagePayload()
746
- );
747
- expect(getDispatcherUrlEnv()).toContain("gateway");
748
- existsSpy.mockRestore();
749
- });
750
-
751
- test('returns "host.docker.internal" when running on host', async () => {
752
- delete process.env.CONTAINER;
753
- const existsSpy = spyOn(fs, "existsSync").mockReturnValue(false);
754
- const manager = createManager();
755
- await manager.createDeployment(
756
- "test-deploy",
757
- "user",
758
- "user-id",
759
- createTestMessagePayload()
760
- );
761
- expect(getDispatcherUrlEnv()).toContain("host.docker.internal");
762
- existsSpy.mockRestore();
763
- });
764
- });
765
-
766
- // =========================================================================
767
- // Docker image reference
768
- // =========================================================================
769
-
770
- describe("image reference", () => {
771
- test("uses digest reference when configured: repo@sha256:abc123", async () => {
772
- const manager = createManager({
773
- worker: {
774
- ...TEST_CONFIG.worker,
775
- image: {
776
- repository: "lobu-worker",
777
- tag: "latest",
778
- digest: "abc123def456",
779
- pullPolicy: "IfNotPresent",
780
- },
781
- },
782
- });
783
- await manager.createDeployment(
784
- "test-deploy",
785
- "user",
786
- "user-id",
787
- createTestMessagePayload()
788
- );
789
- expect(getMainCreateOpts().Image).toBe("lobu-worker@sha256:abc123def456");
790
- });
791
-
792
- test("uses tag reference when no digest: repo:tag", async () => {
793
- const manager = createManager();
794
- await manager.createDeployment(
795
- "test-deploy",
796
- "user",
797
- "user-id",
798
- createTestMessagePayload()
799
- );
800
- expect(getMainCreateOpts().Image).toBe("lobu-worker:latest");
801
- });
802
-
803
- test("handles digest that already has sha256: prefix", async () => {
804
- const manager = createManager({
805
- worker: {
806
- ...TEST_CONFIG.worker,
807
- image: {
808
- repository: "lobu-worker",
809
- tag: "latest",
810
- digest: "sha256:abc123def456",
811
- pullPolicy: "IfNotPresent",
812
- },
813
- },
814
- });
815
- await manager.createDeployment(
816
- "test-deploy",
817
- "user",
818
- "user-id",
819
- createTestMessagePayload()
820
- );
821
- expect(getMainCreateOpts().Image).toBe("lobu-worker@sha256:abc123def456");
822
- });
823
- });
824
-
825
- // =========================================================================
826
- // deleteDeployment
827
- // =========================================================================
828
-
829
- describe("deleteDeployment", () => {
830
- test("calls stop + remove on container", async () => {
831
- const manager = createManager();
832
-
833
- await manager.deleteDeployment("test-deploy");
834
-
835
- expect(mockContainer.stop).toHaveBeenCalled();
836
- expect(mockContainer.remove).toHaveBeenCalled();
837
- });
838
-
839
- test("handles 404 (container already deleted) gracefully", async () => {
840
- const stopMock = mock(async () => {
841
- /* noop */
842
- });
843
- const removeMock = mock(async () => {
844
- const err: any = new Error("not found");
845
- err.statusCode = 404;
846
- throw err;
847
- });
848
- mockDocker.getContainer.mockImplementation(() => ({
849
- stop: stopMock,
850
- remove: removeMock,
851
- }));
852
-
853
- const manager = createManager();
854
-
855
- // Should not throw on 404
856
- await expect(
857
- manager.deleteDeployment("nonexistent")
858
- ).resolves.toBeUndefined();
859
- });
860
-
861
- test("handles already-stopped container gracefully", async () => {
862
- mockContainer.stop.mockImplementation(async () => {
863
- throw new Error("container already stopped");
864
- });
865
-
866
- const manager = createManager();
867
-
868
- // Should not throw - stop failure is caught
869
- await expect(
870
- manager.deleteDeployment("test-deploy")
871
- ).resolves.toBeUndefined();
872
- expect(mockContainer.remove).toHaveBeenCalled();
873
- });
874
- });
875
-
876
- // =========================================================================
877
- // scaleDeployment
878
- // =========================================================================
879
-
880
- describe("scaleDeployment", () => {
881
- test("scaleDeployment(0) stops running container", async () => {
882
- mockContainer.inspect.mockImplementation(async () => ({
883
- State: { Running: true },
884
- }));
885
-
886
- const manager = createManager();
887
- await manager.scaleDeployment("test-deploy", 0);
888
-
889
- expect(mockContainer.stop).toHaveBeenCalled();
890
- });
891
-
892
- test("scaleDeployment(1) starts stopped container", async () => {
893
- mockContainer.inspect.mockImplementation(async () => ({
894
- State: { Running: false },
895
- }));
896
-
897
- const manager = createManager();
898
- await manager.scaleDeployment("test-deploy", 1);
899
-
900
- expect(mockContainer.start).toHaveBeenCalled();
901
- });
902
-
903
- test("scaleDeployment(0) is a no-op if container already stopped", async () => {
904
- mockContainer.inspect.mockImplementation(async () => ({
905
- State: { Running: false },
906
- }));
907
-
908
- const manager = createManager();
909
- await manager.scaleDeployment("test-deploy", 0);
910
-
911
- expect(mockContainer.stop).not.toHaveBeenCalled();
912
- });
913
-
914
- test("scaleDeployment(1) is a no-op if container already running", async () => {
915
- mockContainer.inspect.mockImplementation(async () => ({
916
- State: { Running: true },
917
- }));
918
-
919
- const manager = createManager();
920
- await manager.scaleDeployment("test-deploy", 1);
921
-
922
- expect(mockContainer.start).not.toHaveBeenCalled();
923
- });
924
- });
925
-
926
- // =========================================================================
927
- // listDeployments
928
- // =========================================================================
929
-
930
- describe("listDeployments", () => {
931
- test("with no containers returns empty", async () => {
932
- mockDocker.listContainers.mockImplementation(async () => []);
933
-
934
- const manager = createManager();
935
- const result = await manager.listDeployments();
936
-
937
- expect(result).toEqual([]);
938
- });
939
-
940
- test("with containers returns DeploymentInfo entries", async () => {
941
- const now = Math.floor(Date.now() / 1000);
942
- mockDocker.listContainers.mockImplementation(async () => [
943
- {
944
- Names: ["/lobu-worker-test-123"],
945
- State: "running",
946
- Created: now - 60, // 60 seconds ago
947
- Labels: {
948
- "app.kubernetes.io/component": "worker",
949
- "lobu.io/created": new Date((now - 60) * 1000).toISOString(),
950
- },
951
- },
952
- {
953
- Names: ["/lobu-worker-test-456"],
954
- State: "exited",
955
- Created: now - 3600, // 1 hour ago
956
- Labels: {
957
- "app.kubernetes.io/component": "worker",
958
- },
959
- },
960
- ]);
961
-
962
- const manager = createManager();
963
- const result = await manager.listDeployments();
964
-
965
- expect(result).toHaveLength(2);
966
- expect(result[0].deploymentName).toBe("lobu-worker-test-123");
967
- expect(result[0].replicas).toBe(1); // running
968
- expect(result[1].deploymentName).toBe("lobu-worker-test-456");
969
- expect(result[1].replicas).toBe(0); // exited
970
- });
971
-
972
- test("filters by worker label", async () => {
973
- const manager = createManager();
974
- await manager.listDeployments();
975
-
976
- expect(mockDocker.listContainers).toHaveBeenCalledWith({
977
- all: true,
978
- filters: {
979
- label: ["app.kubernetes.io/component=worker"],
980
- },
981
- });
982
- });
983
- });
984
-
985
- // =========================================================================
986
- // Activity tracking
987
- // =========================================================================
988
-
989
- describe("activity tracking", () => {
990
- test("updateDeploymentActivity stores timestamp", async () => {
991
- const manager = createManager();
992
-
993
- await manager.updateDeploymentActivity("test-deploy");
994
-
995
- // Verify by listing deployments with a container matching the name
996
- const now = Math.floor(Date.now() / 1000);
997
- mockDocker.listContainers.mockImplementation(async () => [
998
- {
999
- Names: ["/test-deploy"],
1000
- State: "running",
1001
- Created: now - 86400, // 1 day ago
1002
- Labels: {
1003
- "app.kubernetes.io/component": "worker",
1004
- "lobu.io/created": new Date((now - 86400) * 1000).toISOString(),
1005
- },
1006
- },
1007
- ]);
1008
-
1009
- const deployments = await manager.listDeployments();
1010
- // The tracked activity should be very recent (just now), not 1 day ago
1011
- expect(deployments[0].minutesIdle).toBeLessThan(1);
1012
- });
1013
-
1014
- test("listDeployments uses most recent of tracked vs label activity", async () => {
1015
- const manager = createManager();
1016
- const now = Math.floor(Date.now() / 1000);
1017
-
1018
- // Set up a container with an old label timestamp
1019
- mockDocker.listContainers.mockImplementation(async () => [
1020
- {
1021
- Names: ["/tracked-deploy"],
1022
- State: "running",
1023
- Created: now - 7200, // 2 hours ago
1024
- Labels: {
1025
- "app.kubernetes.io/component": "worker",
1026
- "lobu.io/last-activity": new Date(
1027
- (now - 7200) * 1000
1028
- ).toISOString(),
1029
- },
1030
- },
1031
- ]);
1032
-
1033
- // Update activity in-memory (very recent)
1034
- await manager.updateDeploymentActivity("tracked-deploy");
1035
-
1036
- const deployments = await manager.listDeployments();
1037
- // Should use tracked (recent) not label (2 hours ago)
1038
- expect(deployments[0].minutesIdle).toBeLessThan(1);
1039
- });
1040
- });
1041
-
1042
- // =========================================================================
1043
- // validateWorkerImage
1044
- // =========================================================================
1045
-
1046
- describe("validateWorkerImage", () => {
1047
- test("succeeds when image exists locally", async () => {
1048
- const manager = createManager();
1049
- await expect(manager.validateWorkerImage()).resolves.toBeUndefined();
1050
- });
1051
-
1052
- test("pulls image when not found locally", async () => {
1053
- mockDocker.getImage.mockImplementation(() => ({
1054
- inspect: mock(async () => {
1055
- throw new Error("No such image");
1056
- }),
1057
- }));
1058
-
1059
- const manager = createManager();
1060
- await expect(manager.validateWorkerImage()).resolves.toBeUndefined();
1061
- expect(mockDocker.pull).toHaveBeenCalled();
1062
- });
1063
-
1064
- test("throws when image not found and pull fails", async () => {
1065
- mockDocker.getImage.mockImplementation(() => ({
1066
- inspect: mock(async () => {
1067
- throw new Error("No such image");
1068
- }),
1069
- }));
1070
- mockDocker.pull.mockImplementation((_name: string, cb: Function) =>
1071
- cb(new Error("pull failed"))
1072
- );
1073
-
1074
- const manager = createManager();
1075
- await expect(manager.validateWorkerImage()).rejects.toThrow(
1076
- "does not exist locally and pull failed"
1077
- );
1078
- });
1079
- });
1080
-
1081
- // =========================================================================
1082
- // Labels & compose integration
1083
- // =========================================================================
1084
-
1085
- describe("labels", () => {
1086
- test("sets base worker labels", async () => {
1087
- const manager = createManager();
1088
- await manager.createDeployment(
1089
- "test-deploy",
1090
- "user",
1091
- "user-id",
1092
- createTestMessagePayload()
1093
- );
1094
- const labels = getMainCreateOpts().Labels;
1095
- expect(labels["app.kubernetes.io/name"]).toBe("lobu");
1096
- expect(labels["app.kubernetes.io/component"]).toBe("worker");
1097
- expect(labels["lobu/managed-by"]).toBe("orchestrator");
1098
- });
1099
-
1100
- test("sets Docker Compose project labels", async () => {
1101
- process.env.COMPOSE_PROJECT_NAME = "myproject";
1102
- const manager = createManager();
1103
- await manager.createDeployment(
1104
- "test-deploy",
1105
- "user",
1106
- "user-id",
1107
- createTestMessagePayload()
1108
- );
1109
- const labels = getMainCreateOpts().Labels;
1110
- expect(labels["com.docker.compose.project"]).toBe("myproject");
1111
- expect(labels["com.docker.compose.service"]).toBe("test-deploy");
1112
- });
1113
-
1114
- test("sets agent-id label", async () => {
1115
- const manager = createManager();
1116
- await manager.createDeployment(
1117
- "test-deploy",
1118
- "user",
1119
- "user-id",
1120
- createTestMessagePayload({ agentId: "my-agent-123" })
1121
- );
1122
- expect(getMainCreateOpts().Labels["lobu.io/agent-id"]).toBe(
1123
- "my-agent-123"
1124
- );
1125
- });
1126
- });
1127
-
1128
- // =========================================================================
1129
- // Extra hosts & host mode
1130
- // =========================================================================
1131
-
1132
- describe("extra hosts", () => {
1133
- test("adds ExtraHosts when running on host (not in container)", async () => {
1134
- delete process.env.CONTAINER;
1135
- const existsSpy = spyOn(fs, "existsSync").mockReturnValue(false);
1136
- const manager = createManager();
1137
- await manager.createDeployment(
1138
- "test-deploy",
1139
- "user",
1140
- "user-id",
1141
- createTestMessagePayload()
1142
- );
1143
- expect(getMainCreateOpts().HostConfig.ExtraHosts).toEqual([
1144
- "host.docker.internal:host-gateway",
1145
- ]);
1146
- existsSpy.mockRestore();
1147
- });
1148
-
1149
- test("does not add ExtraHosts when running in container", async () => {
1150
- process.env.CONTAINER = "true";
1151
- const existsSpy = spyOn(fs, "existsSync").mockReturnValue(false);
1152
- const manager = createManager();
1153
- await manager.createDeployment(
1154
- "test-deploy",
1155
- "user",
1156
- "user-id",
1157
- createTestMessagePayload()
1158
- );
1159
- expect(getMainCreateOpts().HostConfig.ExtraHosts).toBeUndefined();
1160
- existsSpy.mockRestore();
1161
- });
1162
- });
1163
-
1164
- // =========================================================================
1165
- // Security options
1166
- // =========================================================================
1167
-
1168
- describe("advanced security options", () => {
1169
- test("adds seccomp profile when WORKER_SECCOMP_PROFILE set", async () => {
1170
- process.env.WORKER_SECCOMP_PROFILE = "/path/to/seccomp.json";
1171
- const manager = createManager();
1172
- await manager.createDeployment(
1173
- "test-deploy",
1174
- "user",
1175
- "user-id",
1176
- createTestMessagePayload()
1177
- );
1178
- expect(getMainCreateOpts().HostConfig.SecurityOpt).toContain(
1179
- "seccomp=/path/to/seccomp.json"
1180
- );
1181
- });
1182
-
1183
- test("adds apparmor profile when WORKER_APPARMOR_PROFILE set", async () => {
1184
- process.env.WORKER_APPARMOR_PROFILE = "docker-custom";
1185
- const manager = createManager();
1186
- await manager.createDeployment(
1187
- "test-deploy",
1188
- "user",
1189
- "user-id",
1190
- createTestMessagePayload()
1191
- );
1192
- expect(getMainCreateOpts().HostConfig.SecurityOpt).toContain(
1193
- "apparmor=docker-custom"
1194
- );
1195
- });
1196
-
1197
- test("disables readonly rootfs when WORKER_READONLY_ROOTFS=false", async () => {
1198
- process.env.WORKER_READONLY_ROOTFS = "false";
1199
- const manager = createManager();
1200
- await manager.createDeployment(
1201
- "test-deploy",
1202
- "user",
1203
- "user-id",
1204
- createTestMessagePayload()
1205
- );
1206
- const hc = getMainCreateOpts().HostConfig;
1207
- expect(hc.ReadonlyRootfs).toBe(false);
1208
- expect(hc.Tmpfs).toBeUndefined();
1209
- });
1210
- });
1211
- });