@lobu/gateway 3.0.9 → 3.0.13

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 (212) 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/gateway/index.js +1 -1
  19. package/dist/gateway/index.js.map +1 -1
  20. package/dist/interactions.d.ts +9 -43
  21. package/dist/interactions.d.ts.map +1 -1
  22. package/dist/interactions.js +10 -52
  23. package/dist/interactions.js.map +1 -1
  24. package/dist/routes/public/agent.d.ts +4 -0
  25. package/dist/routes/public/agent.d.ts.map +1 -1
  26. package/dist/routes/public/agent.js +21 -0
  27. package/dist/routes/public/agent.js.map +1 -1
  28. package/dist/services/core-services.d.ts.map +1 -1
  29. package/dist/services/core-services.js +4 -0
  30. package/dist/services/core-services.js.map +1 -1
  31. package/package.json +9 -9
  32. package/src/__tests__/agent-config-routes.test.ts +0 -254
  33. package/src/__tests__/agent-history-routes.test.ts +0 -72
  34. package/src/__tests__/agent-routes.test.ts +0 -68
  35. package/src/__tests__/agent-schedules-routes.test.ts +0 -59
  36. package/src/__tests__/agent-settings-store.test.ts +0 -323
  37. package/src/__tests__/bedrock-model-catalog.test.ts +0 -40
  38. package/src/__tests__/bedrock-openai-service.test.ts +0 -157
  39. package/src/__tests__/bedrock-provider-module.test.ts +0 -56
  40. package/src/__tests__/chat-instance-manager-slack.test.ts +0 -204
  41. package/src/__tests__/chat-response-bridge.test.ts +0 -131
  42. package/src/__tests__/config-memory-plugins.test.ts +0 -92
  43. package/src/__tests__/config-request-store.test.ts +0 -127
  44. package/src/__tests__/connection-routes.test.ts +0 -144
  45. package/src/__tests__/core-services-store-selection.test.ts +0 -92
  46. package/src/__tests__/docker-deployment.test.ts +0 -1211
  47. package/src/__tests__/embedded-deployment.test.ts +0 -342
  48. package/src/__tests__/grant-store.test.ts +0 -148
  49. package/src/__tests__/http-proxy.test.ts +0 -281
  50. package/src/__tests__/instruction-service.test.ts +0 -37
  51. package/src/__tests__/link-buttons.test.ts +0 -112
  52. package/src/__tests__/lobu.test.ts +0 -32
  53. package/src/__tests__/mcp-config-service.test.ts +0 -347
  54. package/src/__tests__/mcp-proxy.test.ts +0 -694
  55. package/src/__tests__/message-handler-bridge.test.ts +0 -17
  56. package/src/__tests__/model-selection.test.ts +0 -172
  57. package/src/__tests__/oauth-templates.test.ts +0 -39
  58. package/src/__tests__/platform-adapter-slack-send.test.ts +0 -114
  59. package/src/__tests__/platform-helpers-model-resolution.test.ts +0 -253
  60. package/src/__tests__/provider-inheritance.test.ts +0 -212
  61. package/src/__tests__/routes/cli-auth.test.ts +0 -337
  62. package/src/__tests__/routes/interactions.test.ts +0 -121
  63. package/src/__tests__/secret-proxy.test.ts +0 -85
  64. package/src/__tests__/session-manager.test.ts +0 -572
  65. package/src/__tests__/setup.ts +0 -133
  66. package/src/__tests__/skill-and-mcp-registry.test.ts +0 -203
  67. package/src/__tests__/slack-routes.test.ts +0 -161
  68. package/src/__tests__/system-config-resolver.test.ts +0 -75
  69. package/src/__tests__/system-message-limiter.test.ts +0 -89
  70. package/src/__tests__/system-skills-service.test.ts +0 -362
  71. package/src/__tests__/transcription-service.test.ts +0 -222
  72. package/src/__tests__/utils/rate-limiter.test.ts +0 -102
  73. package/src/__tests__/worker-connection-manager.test.ts +0 -497
  74. package/src/__tests__/worker-job-router.test.ts +0 -722
  75. package/src/api/index.ts +0 -1
  76. package/src/api/platform.ts +0 -292
  77. package/src/api/response-renderer.ts +0 -157
  78. package/src/auth/agent-metadata-store.ts +0 -168
  79. package/src/auth/api-auth-middleware.ts +0 -69
  80. package/src/auth/api-key-provider-module.ts +0 -213
  81. package/src/auth/base-provider-module.ts +0 -201
  82. package/src/auth/bedrock/provider-module.ts +0 -110
  83. package/src/auth/chatgpt/chatgpt-oauth-module.ts +0 -185
  84. package/src/auth/chatgpt/device-code-client.ts +0 -218
  85. package/src/auth/chatgpt/index.ts +0 -1
  86. package/src/auth/claude/oauth-module.ts +0 -280
  87. package/src/auth/cli/token-service.ts +0 -249
  88. package/src/auth/external/client.ts +0 -560
  89. package/src/auth/external/device-code-client.ts +0 -235
  90. package/src/auth/mcp/config-service.ts +0 -420
  91. package/src/auth/mcp/proxy.ts +0 -1086
  92. package/src/auth/mcp/string-substitution.ts +0 -17
  93. package/src/auth/mcp/tool-cache.ts +0 -90
  94. package/src/auth/oauth/base-client.ts +0 -267
  95. package/src/auth/oauth/client.ts +0 -153
  96. package/src/auth/oauth/credentials.ts +0 -7
  97. package/src/auth/oauth/providers.ts +0 -69
  98. package/src/auth/oauth/state-store.ts +0 -150
  99. package/src/auth/oauth-templates.ts +0 -179
  100. package/src/auth/provider-catalog.ts +0 -220
  101. package/src/auth/provider-model-options.ts +0 -41
  102. package/src/auth/settings/agent-settings-store.ts +0 -565
  103. package/src/auth/settings/auth-profiles-manager.ts +0 -216
  104. package/src/auth/settings/index.ts +0 -12
  105. package/src/auth/settings/model-preference-store.ts +0 -52
  106. package/src/auth/settings/model-selection.ts +0 -135
  107. package/src/auth/settings/resolved-settings-view.ts +0 -298
  108. package/src/auth/settings/template-utils.ts +0 -44
  109. package/src/auth/settings/token-service.ts +0 -88
  110. package/src/auth/system-env-store.ts +0 -98
  111. package/src/auth/user-agents-store.ts +0 -68
  112. package/src/channels/binding-service.ts +0 -214
  113. package/src/channels/index.ts +0 -4
  114. package/src/cli/gateway.ts +0 -1312
  115. package/src/cli/index.ts +0 -74
  116. package/src/commands/built-in-commands.ts +0 -80
  117. package/src/commands/command-dispatcher.ts +0 -94
  118. package/src/commands/command-reply-adapters.ts +0 -27
  119. package/src/config/file-loader.ts +0 -618
  120. package/src/config/index.ts +0 -588
  121. package/src/config/network-allowlist.ts +0 -71
  122. package/src/connections/chat-instance-manager.ts +0 -1284
  123. package/src/connections/chat-response-bridge.ts +0 -618
  124. package/src/connections/index.ts +0 -7
  125. package/src/connections/interaction-bridge.ts +0 -831
  126. package/src/connections/message-handler-bridge.ts +0 -440
  127. package/src/connections/platform-auth-methods.ts +0 -15
  128. package/src/connections/types.ts +0 -84
  129. package/src/gateway/connection-manager.ts +0 -291
  130. package/src/gateway/index.ts +0 -698
  131. package/src/gateway/job-router.ts +0 -201
  132. package/src/gateway-main.ts +0 -200
  133. package/src/index.ts +0 -41
  134. package/src/infrastructure/queue/index.ts +0 -12
  135. package/src/infrastructure/queue/queue-producer.ts +0 -148
  136. package/src/infrastructure/queue/redis-queue.ts +0 -361
  137. package/src/infrastructure/queue/types.ts +0 -133
  138. package/src/infrastructure/redis/system-message-limiter.ts +0 -94
  139. package/src/interactions/config-request-store.ts +0 -198
  140. package/src/interactions.ts +0 -363
  141. package/src/lobu.ts +0 -311
  142. package/src/metrics/prometheus.ts +0 -159
  143. package/src/modules/module-system.ts +0 -179
  144. package/src/orchestration/base-deployment-manager.ts +0 -900
  145. package/src/orchestration/deployment-utils.ts +0 -98
  146. package/src/orchestration/impl/docker-deployment.ts +0 -620
  147. package/src/orchestration/impl/embedded-deployment.ts +0 -268
  148. package/src/orchestration/impl/index.ts +0 -8
  149. package/src/orchestration/impl/k8s/deployment.ts +0 -1061
  150. package/src/orchestration/impl/k8s/helpers.ts +0 -610
  151. package/src/orchestration/impl/k8s/index.ts +0 -1
  152. package/src/orchestration/index.ts +0 -333
  153. package/src/orchestration/message-consumer.ts +0 -584
  154. package/src/orchestration/scheduled-wakeup.ts +0 -704
  155. package/src/permissions/approval-policy.ts +0 -36
  156. package/src/permissions/grant-store.ts +0 -219
  157. package/src/platform/file-handler.ts +0 -66
  158. package/src/platform/link-buttons.ts +0 -57
  159. package/src/platform/renderer-utils.ts +0 -44
  160. package/src/platform/response-renderer.ts +0 -84
  161. package/src/platform/unified-thread-consumer.ts +0 -194
  162. package/src/platform.ts +0 -318
  163. package/src/proxy/http-proxy.ts +0 -752
  164. package/src/proxy/proxy-manager.ts +0 -81
  165. package/src/proxy/secret-proxy.ts +0 -402
  166. package/src/proxy/token-refresh-job.ts +0 -143
  167. package/src/routes/internal/audio.ts +0 -141
  168. package/src/routes/internal/device-auth.ts +0 -652
  169. package/src/routes/internal/files.ts +0 -226
  170. package/src/routes/internal/history.ts +0 -69
  171. package/src/routes/internal/images.ts +0 -127
  172. package/src/routes/internal/interactions.ts +0 -84
  173. package/src/routes/internal/middleware.ts +0 -23
  174. package/src/routes/internal/schedule.ts +0 -226
  175. package/src/routes/internal/types.ts +0 -22
  176. package/src/routes/openapi-auto.ts +0 -239
  177. package/src/routes/public/agent-access.ts +0 -23
  178. package/src/routes/public/agent-config.ts +0 -675
  179. package/src/routes/public/agent-history.ts +0 -422
  180. package/src/routes/public/agent-schedules.ts +0 -296
  181. package/src/routes/public/agent.ts +0 -1086
  182. package/src/routes/public/agents.ts +0 -373
  183. package/src/routes/public/channels.ts +0 -191
  184. package/src/routes/public/cli-auth.ts +0 -896
  185. package/src/routes/public/connections.ts +0 -574
  186. package/src/routes/public/landing.ts +0 -16
  187. package/src/routes/public/oauth.ts +0 -147
  188. package/src/routes/public/settings-auth.ts +0 -104
  189. package/src/routes/public/slack.ts +0 -173
  190. package/src/routes/shared/agent-ownership.ts +0 -101
  191. package/src/routes/shared/token-verifier.ts +0 -34
  192. package/src/services/bedrock-model-catalog.ts +0 -217
  193. package/src/services/bedrock-openai-service.ts +0 -658
  194. package/src/services/core-services.ts +0 -1072
  195. package/src/services/image-generation-service.ts +0 -257
  196. package/src/services/instruction-service.ts +0 -318
  197. package/src/services/mcp-registry.ts +0 -94
  198. package/src/services/platform-helpers.ts +0 -287
  199. package/src/services/session-manager.ts +0 -262
  200. package/src/services/settings-resolver.ts +0 -74
  201. package/src/services/system-config-resolver.ts +0 -89
  202. package/src/services/system-skills-service.ts +0 -229
  203. package/src/services/transcription-service.ts +0 -684
  204. package/src/session.ts +0 -110
  205. package/src/spaces/index.ts +0 -1
  206. package/src/spaces/space-resolver.ts +0 -17
  207. package/src/stores/in-memory-agent-store.ts +0 -403
  208. package/src/stores/redis-agent-store.ts +0 -279
  209. package/src/utils/public-url.ts +0 -44
  210. package/src/utils/rate-limiter.ts +0 -94
  211. package/tsconfig.json +0 -33
  212. 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
- });