jac-scale 0.1.1__py3-none-any.whl

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 (57) hide show
  1. jac_scale/__init__.py +0 -0
  2. jac_scale/abstractions/config/app_config.jac +30 -0
  3. jac_scale/abstractions/config/base_config.jac +26 -0
  4. jac_scale/abstractions/database_provider.jac +51 -0
  5. jac_scale/abstractions/deployment_target.jac +64 -0
  6. jac_scale/abstractions/image_registry.jac +54 -0
  7. jac_scale/abstractions/logger.jac +20 -0
  8. jac_scale/abstractions/models/deployment_result.jac +27 -0
  9. jac_scale/abstractions/models/resource_status.jac +38 -0
  10. jac_scale/config_loader.jac +31 -0
  11. jac_scale/context.jac +14 -0
  12. jac_scale/factories/database_factory.jac +43 -0
  13. jac_scale/factories/deployment_factory.jac +43 -0
  14. jac_scale/factories/registry_factory.jac +32 -0
  15. jac_scale/factories/utility_factory.jac +34 -0
  16. jac_scale/impl/config_loader.impl.jac +131 -0
  17. jac_scale/impl/context.impl.jac +24 -0
  18. jac_scale/impl/memory_hierarchy.main.impl.jac +63 -0
  19. jac_scale/impl/memory_hierarchy.mongo.impl.jac +239 -0
  20. jac_scale/impl/memory_hierarchy.redis.impl.jac +186 -0
  21. jac_scale/impl/serve.impl.jac +1785 -0
  22. jac_scale/jserver/__init__.py +0 -0
  23. jac_scale/jserver/impl/jfast_api.impl.jac +731 -0
  24. jac_scale/jserver/impl/jserver.impl.jac +79 -0
  25. jac_scale/jserver/jfast_api.jac +162 -0
  26. jac_scale/jserver/jserver.jac +101 -0
  27. jac_scale/memory_hierarchy.jac +138 -0
  28. jac_scale/plugin.jac +218 -0
  29. jac_scale/plugin_config.jac +175 -0
  30. jac_scale/providers/database/kubernetes_mongo.jac +137 -0
  31. jac_scale/providers/database/kubernetes_redis.jac +110 -0
  32. jac_scale/providers/registry/dockerhub.jac +64 -0
  33. jac_scale/serve.jac +118 -0
  34. jac_scale/targets/kubernetes/kubernetes_config.jac +215 -0
  35. jac_scale/targets/kubernetes/kubernetes_target.jac +841 -0
  36. jac_scale/targets/kubernetes/utils/kubernetes_utils.impl.jac +519 -0
  37. jac_scale/targets/kubernetes/utils/kubernetes_utils.jac +85 -0
  38. jac_scale/tests/__init__.py +0 -0
  39. jac_scale/tests/conftest.py +29 -0
  40. jac_scale/tests/fixtures/test_api.jac +159 -0
  41. jac_scale/tests/fixtures/todo_app.jac +68 -0
  42. jac_scale/tests/test_abstractions.py +88 -0
  43. jac_scale/tests/test_deploy_k8s.py +265 -0
  44. jac_scale/tests/test_examples.py +484 -0
  45. jac_scale/tests/test_factories.py +149 -0
  46. jac_scale/tests/test_file_upload.py +444 -0
  47. jac_scale/tests/test_k8s_utils.py +156 -0
  48. jac_scale/tests/test_memory_hierarchy.py +247 -0
  49. jac_scale/tests/test_serve.py +1835 -0
  50. jac_scale/tests/test_sso.py +711 -0
  51. jac_scale/utilities/loggers/standard_logger.jac +40 -0
  52. jac_scale/utils.jac +16 -0
  53. jac_scale-0.1.1.dist-info/METADATA +658 -0
  54. jac_scale-0.1.1.dist-info/RECORD +57 -0
  55. jac_scale-0.1.1.dist-info/WHEEL +5 -0
  56. jac_scale-0.1.1.dist-info/entry_points.txt +3 -0
  57. jac_scale-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,484 @@
1
+ """Test for running jac-scale examples and testing their APIs."""
2
+
3
+ import contextlib
4
+ import gc
5
+ import socket
6
+ import subprocess
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import requests
12
+
13
+ from jaclang.project.config import find_project_root
14
+
15
+ JacClientExamples = (
16
+ Path(__file__).parent.parent.parent.parent
17
+ / "jac-client"
18
+ / "jac_client"
19
+ / "examples"
20
+ )
21
+
22
+
23
+ def get_free_port() -> int:
24
+ """Get a free port by binding to port 0 and releasing it."""
25
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
26
+ s.bind(("", 0))
27
+ s.listen(1)
28
+ port = s.getsockname()[1]
29
+ return port
30
+
31
+
32
+ class JacScaleTestRunner:
33
+ """Helper class to run jac-scale examples and test their APIs."""
34
+
35
+ def __init__(
36
+ self, example_file: Path, session_name: str = "test", setup_npm: bool = True
37
+ ):
38
+ """Initialize the test runner.
39
+
40
+ Args:
41
+ example_file: Path to the .jac file to serve
42
+ session_name: Name for the session file (default: "test")
43
+ setup_npm: Whether to run npm install and setup src directory (default: True)
44
+ """
45
+ self.example_file = example_file
46
+ self.port = get_free_port()
47
+ self.base_url = f"http://localhost:{self.port}"
48
+ self.session_file = example_file.parent / f"{session_name}_{self.port}.session"
49
+ self.server_process: subprocess.Popen[str] | None = None
50
+ self.token: str | None = None
51
+ self.root_id: str | None = None
52
+ self.setup_npm = setup_npm
53
+
54
+ def start_server(self, timeout: int = 30) -> None:
55
+ """Start the jac-scale server.
56
+
57
+ Args:
58
+ timeout: Maximum time to wait for server to start (in seconds)
59
+ """
60
+ # Find project root (where jac.toml is) using jaclang's find_project_root
61
+ project_root_result = find_project_root(self.example_file.parent)
62
+ if project_root_result:
63
+ example_dir, _ = project_root_result
64
+ else:
65
+ example_dir = self.example_file.parent
66
+
67
+ # Clean up directories before starting (don't clean src - it contains source files)
68
+ dirs_to_clean = ["build", "dist", "node_modules", ".jac"]
69
+ for dir_name in dirs_to_clean:
70
+ dir_path = example_dir / dir_name
71
+ if dir_path.exists():
72
+ subprocess.run(
73
+ ["rm", "-rf", dir_name],
74
+ cwd=example_dir,
75
+ check=False,
76
+ )
77
+
78
+ # Setup npm dependencies if needed
79
+ if self.setup_npm:
80
+ print(f"Setting up example directory: {example_dir}")
81
+
82
+ # Run npm install
83
+ npm_install = subprocess.run(
84
+ ["jac", "add", "--npm"],
85
+ cwd=example_dir,
86
+ capture_output=True,
87
+ text=True,
88
+ )
89
+ if npm_install.returncode != 0:
90
+ print(f"npm install warning: {npm_install.stderr}")
91
+
92
+ print("Example directory setup complete")
93
+
94
+ # Get the jac executable from the same directory as the current Python interpreter
95
+ import sys
96
+ from pathlib import Path
97
+
98
+ jac_executable = Path(sys.executable).parent / "jac"
99
+
100
+ cmd = [
101
+ str(jac_executable),
102
+ "start",
103
+ str(self.example_file),
104
+ # "--session",
105
+ # str(self.session_file),
106
+ "--port",
107
+ str(self.port),
108
+ ]
109
+
110
+ self.server_process = subprocess.Popen(
111
+ cmd,
112
+ stdout=subprocess.PIPE,
113
+ stderr=subprocess.PIPE,
114
+ text=True,
115
+ cwd=example_dir, # Run from example directory
116
+ )
117
+
118
+ # Wait for server to be ready
119
+ max_attempts = timeout * 5 # Check every 0.2 seconds
120
+ server_ready = False
121
+
122
+ for _ in range(max_attempts):
123
+ # Check if process has died
124
+ if self.server_process.poll() is not None:
125
+ stdout, stderr = self.server_process.communicate()
126
+ raise RuntimeError(
127
+ f"Server process terminated unexpectedly.\n"
128
+ f"STDOUT: {stdout}\nSTDERR: {stderr}"
129
+ )
130
+
131
+ try:
132
+ response = requests.get(f"{self.base_url}/docs", timeout=2)
133
+ if response.status_code in (200, 404):
134
+ print(f"Server started on port {self.port}")
135
+ server_ready = True
136
+ break
137
+ except (requests.ConnectionError, requests.Timeout):
138
+ time.sleep(0.2)
139
+
140
+ if not server_ready:
141
+ stdout, stderr = self.server_process.communicate(timeout=5)
142
+ raise RuntimeError(
143
+ f"Server failed to become ready.\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}"
144
+ )
145
+
146
+ def stop_server(self) -> None:
147
+ """Stop the jac-scale server and clean up session files."""
148
+ if self.server_process:
149
+ self.server_process.terminate()
150
+ try:
151
+ self.server_process.wait(timeout=5)
152
+ except subprocess.TimeoutExpired:
153
+ self.server_process.kill()
154
+ self.server_process.wait()
155
+
156
+ # Close the pipes to avoid ResourceWarning
157
+ if self.server_process.stdout:
158
+ self.server_process.stdout.close()
159
+ if self.server_process.stderr:
160
+ self.server_process.stderr.close()
161
+
162
+ # Run garbage collection to clean up lingering socket objects
163
+ gc.collect()
164
+
165
+ # Clean up session files
166
+ if self.session_file.exists():
167
+ session_dir = self.session_file.parent
168
+ prefix = self.session_file.name
169
+
170
+ for file in session_dir.iterdir():
171
+ if file.name.startswith(prefix):
172
+ with contextlib.suppress(Exception):
173
+ file.unlink()
174
+
175
+ # Clean up directories after stopping (don't clean src - it contains source files)
176
+ project_root_result = find_project_root(self.example_file.parent)
177
+ if project_root_result:
178
+ example_dir, _ = project_root_result
179
+ else:
180
+ example_dir = self.example_file.parent
181
+ dirs_to_clean = [
182
+ "build",
183
+ "dist",
184
+ "node_modules",
185
+ ".jac",
186
+ "package-lock.json",
187
+ ]
188
+ for dir_name in dirs_to_clean:
189
+ dir_path = example_dir / dir_name
190
+ if dir_path.exists():
191
+ subprocess.run(
192
+ ["rm", "-rf", dir_name],
193
+ cwd=example_dir,
194
+ check=False,
195
+ )
196
+
197
+ def create_user(self, username: str, password: str) -> dict[str, Any]:
198
+ """Create a new user and store the token.
199
+
200
+ Args:
201
+ username: Username for the new user
202
+ password: Password for the new user
203
+
204
+ Returns:
205
+ User creation response
206
+ """
207
+ result = self.request(
208
+ "POST", "/user/create", data={"username": username, "password": password}
209
+ )
210
+ self.token = result.get("token")
211
+ self.root_id = result.get("root_id")
212
+ return result
213
+
214
+ def login(self, username: str, password: str) -> dict[str, Any]:
215
+ """Login as an existing user and store the token.
216
+
217
+ Args:
218
+ username: Username
219
+ password: Password
220
+
221
+ Returns:
222
+ Login response
223
+ """
224
+ result = self.request(
225
+ "POST", "/user/login", data={"username": username, "password": password}
226
+ )
227
+ self.token = result.get("token")
228
+ self.root_id = result.get("root_id")
229
+ return result
230
+
231
+ def request(
232
+ self,
233
+ method: str,
234
+ path: str,
235
+ data: dict[str, Any] | None = None,
236
+ use_token: bool = False,
237
+ timeout: int = 5,
238
+ max_retries: int = 60,
239
+ retry_interval: float = 2.0,
240
+ ) -> dict[str, Any]:
241
+ """Make an HTTP request to the server.
242
+
243
+ Retries on 503 Service Unavailable responses.
244
+
245
+ Args:
246
+ method: HTTP method (GET, POST, etc.)
247
+ path: API path (e.g., "/walker/CreateTask")
248
+ data: Request body data
249
+ use_token: Whether to include authentication token
250
+ timeout: Request timeout in seconds
251
+ max_retries: Maximum number of retries for 503 responses
252
+ retry_interval: Time to wait between retries in seconds
253
+
254
+ Returns:
255
+ Response JSON data
256
+ """
257
+ url = f"{self.base_url}{path}"
258
+ headers = {"Content-Type": "application/json"}
259
+
260
+ if use_token and self.token:
261
+ headers["Authorization"] = f"Bearer {self.token}"
262
+
263
+ response = None
264
+ for attempt in range(max_retries):
265
+ response = requests.request(
266
+ method=method,
267
+ url=url,
268
+ json=data,
269
+ headers=headers,
270
+ timeout=timeout,
271
+ )
272
+
273
+ if response.status_code == 503:
274
+ print(
275
+ f"[DEBUG] {path} returned 503, retrying ({attempt + 1}/{max_retries})..."
276
+ )
277
+ time.sleep(retry_interval)
278
+ continue
279
+
280
+ break
281
+
282
+ assert response is not None, "No response received"
283
+ json_response: Any = response.json()
284
+
285
+ # Handle jac-scale's tuple response format [status, body]
286
+ if isinstance(json_response, list) and len(json_response) == 2:
287
+ json_response = json_response[1]
288
+
289
+ # Handle TransportResponse envelope format
290
+ if (
291
+ isinstance(json_response, dict)
292
+ and "ok" in json_response
293
+ and "data" in json_response
294
+ ):
295
+ if json_response.get("ok") and json_response.get("data") is not None:
296
+ # Success case: return the data field
297
+ return json_response["data"]
298
+ elif not json_response.get("ok") and json_response.get("error"):
299
+ # Error case: return error info
300
+ error_info = json_response["error"]
301
+ result: dict[str, Any] = {
302
+ "error": error_info.get("message", "Unknown error")
303
+ }
304
+ if "code" in error_info:
305
+ result["error_code"] = error_info["code"]
306
+ if "details" in error_info:
307
+ result["error_details"] = error_info["details"]
308
+ return result
309
+
310
+ # FastAPI validation errors (422) have "detail" field - return as-is
311
+ return json_response # type: ignore[return-value]
312
+
313
+ def request_raw(
314
+ self,
315
+ method: str,
316
+ path: str,
317
+ data: dict[str, Any] | None = None,
318
+ use_token: bool = False,
319
+ timeout: int = 120,
320
+ max_retries: int = 60,
321
+ retry_interval: float = 2.0,
322
+ ) -> str:
323
+ """Make a raw HTTP request to the server.
324
+
325
+ Args:
326
+ method: HTTP method (GET, POST, etc.)
327
+ path: API path (e.g., "/walker/CreateTask")
328
+ data: Request body data
329
+ use_token: Whether to include authentication token
330
+ timeout: Request timeout in seconds
331
+ max_retries: Maximum number of retries for 503 responses and timeouts
332
+ retry_interval: Time to wait between retries in seconds
333
+
334
+ Returns:
335
+ Response text
336
+ """
337
+ url = f"{self.base_url}{path}"
338
+ headers = {"Content-Type": "application/json"}
339
+
340
+ if use_token and self.token:
341
+ headers["Authorization"] = f"Bearer {self.token}"
342
+
343
+ response = None
344
+ for attempt in range(max_retries):
345
+ try:
346
+ response = requests.request(
347
+ method=method,
348
+ url=url,
349
+ json=data,
350
+ headers=headers,
351
+ timeout=timeout,
352
+ )
353
+
354
+ if response.status_code == 503:
355
+ print(
356
+ f"[DEBUG] {path} returned 503, retrying ({attempt + 1}/{max_retries})..."
357
+ )
358
+ time.sleep(retry_interval)
359
+ continue
360
+
361
+ return response.text
362
+ except requests.exceptions.Timeout:
363
+ print(
364
+ f"[DEBUG] {path} timed out, retrying ({attempt + 1}/{max_retries})..."
365
+ )
366
+ time.sleep(retry_interval)
367
+ continue
368
+
369
+ # Return last response text even if it was 503, or error message if all timeouts
370
+ if response is not None:
371
+ return response.text
372
+ return f"Request failed after {max_retries} retries (all timeouts)"
373
+
374
+ def spawn_walker(
375
+ self, walker_name: str, **kwargs: dict[str, Any]
376
+ ) -> dict[str, Any]:
377
+ """Spawn a walker with the given parameters.
378
+
379
+ Args:
380
+ walker_name: Name of the walker to spawn
381
+ **kwargs: Walker field values
382
+
383
+ Returns:
384
+ Walker execution response
385
+ """
386
+ return self.request(
387
+ "POST", f"/walker/{walker_name}", data=kwargs, use_token=True
388
+ )
389
+
390
+ def call_function(
391
+ self, function_name: str, **kwargs: dict[str, Any]
392
+ ) -> dict[str, Any]:
393
+ """Call a function with the given parameters.
394
+
395
+ Args:
396
+ function_name: Name of the function to call
397
+ **kwargs: Function parameter values
398
+
399
+ Returns:
400
+ Function result
401
+ """
402
+ # Build query string from kwargs
403
+ query_params = "&".join(f"{k}={v}" for k, v in kwargs.items())
404
+ path = f"/function/{function_name}"
405
+ if query_params:
406
+ path += f"?{query_params}"
407
+
408
+ return self.request("GET", path, use_token=True)
409
+
410
+ def __enter__(self) -> "JacScaleTestRunner":
411
+ """Context manager entry."""
412
+ self.start_server()
413
+ return self
414
+
415
+ def __exit__(
416
+ self,
417
+ exc_type: type[BaseException] | None,
418
+ exc_val: BaseException | None,
419
+ exc_tb: object | None,
420
+ ) -> None:
421
+ """Context manager exit."""
422
+ self.stop_server()
423
+
424
+
425
+ class TestJacClientExamples:
426
+ """Template for testing custom examples."""
427
+
428
+ def test_all_in_one(self) -> None:
429
+ """Test a custom example file."""
430
+ # Point to your example file
431
+ example_file = JacClientExamples / "all-in-one" / "main.jac"
432
+ with JacScaleTestRunner(
433
+ example_file, session_name="custom_test", setup_npm=True
434
+ ) as runner:
435
+ assert "background-image" in runner.request_raw("GET", "/styles/styles.css")
436
+ assert "PNG" in runner.request_raw("GET", "/static/assets/burger.png")
437
+ assert "/static/client.js" in runner.request_raw("GET", "/cl/app")
438
+ assert (
439
+ runner.request_raw("GET", "/static/client.js")
440
+ != "Static file not found"
441
+ )
442
+ assert (
443
+ runner.request_raw("GET", "/static/client.jss")
444
+ == "Static file not found"
445
+ )
446
+
447
+ def test_js_styling(self) -> None:
448
+ """Test JS and styling example file."""
449
+ # Point to your example file
450
+ example_file = JacClientExamples / "css-styling" / "js-styling" / "main.jac"
451
+ with JacScaleTestRunner(
452
+ example_file, session_name="js_styling_test", setup_npm=True
453
+ ) as runner:
454
+ assert "const countDisplay" in runner.request_raw("GET", "/styles.js")
455
+ assert "/static/client.js" in runner.request_raw("GET", "/cl/app")
456
+
457
+ def test_material_ui(self) -> None:
458
+ """Test Material-UI styling example."""
459
+ example_file = JacClientExamples / "css-styling" / "material-ui" / "main.jac"
460
+ with JacScaleTestRunner(
461
+ example_file, session_name="material_ui_test", setup_npm=True
462
+ ) as runner:
463
+ assert "/static/client.js" in runner.request_raw("GET", "/cl/app")
464
+
465
+ def test_pure_css(self) -> None:
466
+ """Test Pure CSS example."""
467
+ example_file = JacClientExamples / "css-styling" / "pure-css" / "main.jac"
468
+ with JacScaleTestRunner(
469
+ example_file, session_name="pure_css_test", setup_npm=True
470
+ ) as runner:
471
+ page_content = runner.request_raw("GET", "/cl/app")
472
+ assert "/static/client.js" in page_content
473
+ assert ".container {" in runner.request_raw("GET", "/styles.css")
474
+
475
+ def test_styled_components(self) -> None:
476
+ """Test Styled Components example."""
477
+ example_file = (
478
+ JacClientExamples / "css-styling" / "styled-components" / "main.jac"
479
+ )
480
+ with JacScaleTestRunner(
481
+ example_file, session_name="styled_components_test", setup_npm=True
482
+ ) as runner:
483
+ assert "/static/client.js" in runner.request_raw("GET", "/cl/app")
484
+ assert "import styled from" in runner.request_raw("GET", "/styled.js")
@@ -0,0 +1,149 @@
1
+ """Tests for the new factory-based architecture."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ # Note: These imports will work once the Jac files are compiled to Python
8
+ # For now, we'll use relative imports that match the structure
9
+ try:
10
+ from ..abstractions.config.app_config import AppConfig
11
+ from ..factories.deployment_factory import DeploymentTargetFactory
12
+ from ..factories.registry_factory import ImageRegistryFactory
13
+ from ..factories.utility_factory import UtilityFactory
14
+ from ..targets.kubernetes.kubernetes_config import KubernetesConfig
15
+ except ImportError:
16
+ # If Jac files aren't compiled yet, skip these tests
17
+ pytest.skip("Jac modules not compiled", allow_module_level=True)
18
+
19
+
20
+ def test_deployment_target_factory_creates_kubernetes_target():
21
+ """Test that factory creates KubernetesTarget for 'kubernetes' type."""
22
+ config = {
23
+ "app_name": "test-app",
24
+ "namespace": "default",
25
+ "container_port": 8000,
26
+ "node_port": 30001,
27
+ }
28
+
29
+ target = DeploymentTargetFactory.create("kubernetes", config)
30
+
31
+ assert target is not None
32
+ assert hasattr(target, "deploy")
33
+ assert hasattr(target, "destroy")
34
+ assert hasattr(target, "get_status")
35
+ assert hasattr(target, "scale")
36
+ assert target.k8s_config.app_name == "test-app"
37
+
38
+
39
+ def test_deployment_target_factory_raises_for_unsupported_target():
40
+ """Test that factory raises ValueError for unsupported target."""
41
+ config = {"app_name": "test-app"}
42
+
43
+ with pytest.raises(ValueError, match="Unsupported deployment target"):
44
+ DeploymentTargetFactory.create("unsupported", config)
45
+
46
+
47
+ def test_deployment_target_factory_raises_for_not_implemented_target():
48
+ """Test that factory raises NotImplementedError for future targets."""
49
+ config = {"app_name": "test-app"}
50
+
51
+ with pytest.raises(NotImplementedError, match="not yet implemented"):
52
+ DeploymentTargetFactory.create("aws", config)
53
+
54
+
55
+ def test_image_registry_factory_creates_dockerhub():
56
+ """Test that factory creates DockerHubRegistry for 'dockerhub' type."""
57
+ config = {
58
+ "app_name": "test-app",
59
+ "docker_username": "testuser",
60
+ "docker_password": "testpass",
61
+ }
62
+
63
+ registry = ImageRegistryFactory.create("dockerhub", config)
64
+
65
+ assert registry is not None
66
+ assert hasattr(registry, "build_image")
67
+ assert hasattr(registry, "push_image")
68
+ assert hasattr(registry, "get_image_url")
69
+ assert registry.docker_username == "testuser"
70
+
71
+
72
+ def test_image_registry_factory_raises_for_unsupported_registry():
73
+ """Test that factory raises ValueError for unsupported registry."""
74
+ config = {"app_name": "test-app"}
75
+
76
+ with pytest.raises(ValueError, match="Unsupported image registry"):
77
+ ImageRegistryFactory.create("unsupported", config)
78
+
79
+
80
+ def test_utility_factory_creates_standard_logger():
81
+ """Test that factory creates StandardLogger for 'standard' type."""
82
+ logger = UtilityFactory.create_logger("standard")
83
+
84
+ assert logger is not None
85
+ assert hasattr(logger, "info")
86
+ assert hasattr(logger, "error")
87
+ assert hasattr(logger, "warn")
88
+ assert hasattr(logger, "debug")
89
+
90
+
91
+ def test_utility_factory_defaults_to_standard_logger():
92
+ """Test that factory defaults to standard logger when no type specified."""
93
+ logger = UtilityFactory.create_logger()
94
+
95
+ assert logger is not None
96
+ assert hasattr(logger, "info")
97
+
98
+
99
+ def test_kubernetes_config_from_dict():
100
+ """Test KubernetesConfig creation from dictionary."""
101
+ config_dict = {
102
+ "app_name": "my-app",
103
+ "namespace": "test-ns",
104
+ "container_port": 9000,
105
+ "node_port": 30002,
106
+ "mongodb_enabled": False,
107
+ "redis_enabled": False,
108
+ }
109
+
110
+ config = KubernetesConfig.from_dict(config_dict)
111
+
112
+ assert config.app_name == "my-app"
113
+ assert config.namespace == "test-ns"
114
+ assert config.container_port == 9000
115
+ assert config.node_port == 30002
116
+ assert config.mongodb_enabled is False
117
+ assert config.redis_enabled is False
118
+
119
+
120
+ def test_app_config_creation():
121
+ """Test AppConfig creation."""
122
+ app_config = AppConfig(
123
+ code_folder="/path/to/code",
124
+ file_name="main.jac",
125
+ build=True,
126
+ )
127
+
128
+ assert app_config.code_folder == "/path/to/code"
129
+ assert app_config.file_name == "main.jac"
130
+ assert app_config.build is True
131
+ assert app_config.testing is False
132
+
133
+
134
+ @patch("jac_scale.factories.deployment_factory.KubernetesTarget")
135
+ def test_deployment_target_with_logger(mock_kubernetes_target: MagicMock):
136
+ """Test that deployment target is created with logger."""
137
+ mock_target = MagicMock()
138
+ mock_kubernetes_target.return_value = mock_target
139
+
140
+ config = {"app_name": "test-app", "namespace": "default"}
141
+ logger = UtilityFactory.create_logger("standard")
142
+
143
+ DeploymentTargetFactory.create("kubernetes", config, logger)
144
+
145
+ # Verify logger was passed to target
146
+ mock_kubernetes_target.assert_called_once()
147
+ call_kwargs = mock_kubernetes_target.call_args[1]
148
+ assert "logger" in call_kwargs
149
+ assert call_kwargs["logger"] == logger