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