jaclang 0.8.9__py3-none-any.whl → 0.8.10__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.

Potentially problematic release.


This version of jaclang might be problematic. Click here for more details.

Files changed (103) hide show
  1. jaclang/cli/cli.py +147 -25
  2. jaclang/cli/cmdreg.py +144 -8
  3. jaclang/compiler/__init__.py +6 -1
  4. jaclang/compiler/codeinfo.py +16 -1
  5. jaclang/compiler/constant.py +33 -13
  6. jaclang/compiler/jac.lark +130 -31
  7. jaclang/compiler/larkparse/jac_parser.py +2 -2
  8. jaclang/compiler/parser.py +567 -176
  9. jaclang/compiler/passes/__init__.py +2 -1
  10. jaclang/compiler/passes/ast_gen/__init__.py +5 -0
  11. jaclang/compiler/passes/ast_gen/base_ast_gen_pass.py +54 -0
  12. jaclang/compiler/passes/ast_gen/jsx_processor.py +344 -0
  13. jaclang/compiler/passes/ecmascript/__init__.py +25 -0
  14. jaclang/compiler/passes/ecmascript/es_unparse.py +576 -0
  15. jaclang/compiler/passes/ecmascript/esast_gen_pass.py +2068 -0
  16. jaclang/compiler/passes/ecmascript/estree.py +972 -0
  17. jaclang/compiler/passes/ecmascript/tests/__init__.py +1 -0
  18. jaclang/compiler/passes/ecmascript/tests/fixtures/advanced_language_features.jac +170 -0
  19. jaclang/compiler/passes/ecmascript/tests/fixtures/class_separate_impl.impl.jac +30 -0
  20. jaclang/compiler/passes/ecmascript/tests/fixtures/class_separate_impl.jac +14 -0
  21. jaclang/compiler/passes/ecmascript/tests/fixtures/client_jsx.jac +89 -0
  22. jaclang/compiler/passes/ecmascript/tests/fixtures/core_language_features.jac +195 -0
  23. jaclang/compiler/passes/ecmascript/tests/test_esast_gen_pass.py +167 -0
  24. jaclang/compiler/passes/ecmascript/tests/test_js_generation.py +239 -0
  25. jaclang/compiler/passes/main/__init__.py +0 -3
  26. jaclang/compiler/passes/main/annex_pass.py +23 -1
  27. jaclang/compiler/passes/main/pyast_gen_pass.py +324 -234
  28. jaclang/compiler/passes/main/pyast_load_pass.py +46 -11
  29. jaclang/compiler/passes/main/pyjac_ast_link_pass.py +2 -0
  30. jaclang/compiler/passes/main/sym_tab_build_pass.py +18 -1
  31. jaclang/compiler/passes/main/tests/fixtures/autoimpl.cl.jac +7 -0
  32. jaclang/compiler/passes/main/tests/fixtures/checker_arity.jac +3 -0
  33. jaclang/compiler/passes/main/tests/fixtures/checker_class_construct.jac +33 -0
  34. jaclang/compiler/passes/main/tests/fixtures/defuse_modpath.jac +7 -0
  35. jaclang/compiler/passes/main/tests/fixtures/member_access_type_resolve.jac +2 -1
  36. jaclang/compiler/passes/main/tests/test_checker_pass.py +31 -2
  37. jaclang/compiler/passes/main/tests/test_def_use_pass.py +12 -0
  38. jaclang/compiler/passes/main/tests/test_import_pass.py +23 -4
  39. jaclang/compiler/passes/main/tests/test_pyast_gen_pass.py +25 -0
  40. jaclang/compiler/passes/main/type_checker_pass.py +7 -0
  41. jaclang/compiler/passes/tool/doc_ir_gen_pass.py +115 -0
  42. jaclang/compiler/passes/tool/fuse_comments_pass.py +1 -10
  43. jaclang/compiler/passes/tool/tests/test_jac_format_pass.py +4 -1
  44. jaclang/compiler/passes/transform.py +9 -1
  45. jaclang/compiler/passes/uni_pass.py +5 -7
  46. jaclang/compiler/program.py +22 -25
  47. jaclang/compiler/tests/test_client_codegen.py +113 -0
  48. jaclang/compiler/tests/test_importer.py +12 -10
  49. jaclang/compiler/tests/test_parser.py +249 -3
  50. jaclang/compiler/type_system/type_evaluator.jac +169 -50
  51. jaclang/compiler/type_system/type_utils.py +1 -1
  52. jaclang/compiler/type_system/types.py +6 -0
  53. jaclang/compiler/unitree.py +430 -84
  54. jaclang/langserve/engine.jac +224 -288
  55. jaclang/langserve/sem_manager.jac +12 -8
  56. jaclang/langserve/server.jac +48 -48
  57. jaclang/langserve/tests/fixtures/greet.py +17 -0
  58. jaclang/langserve/tests/fixtures/md_path.jac +22 -0
  59. jaclang/langserve/tests/fixtures/user.jac +15 -0
  60. jaclang/langserve/tests/test_server.py +66 -371
  61. jaclang/lib.py +1 -1
  62. jaclang/runtimelib/client_bundle.py +169 -0
  63. jaclang/runtimelib/client_runtime.jac +586 -0
  64. jaclang/runtimelib/constructs.py +2 -0
  65. jaclang/runtimelib/machine.py +259 -100
  66. jaclang/runtimelib/meta_importer.py +111 -22
  67. jaclang/runtimelib/mtp.py +15 -0
  68. jaclang/runtimelib/server.py +1089 -0
  69. jaclang/runtimelib/tests/fixtures/client_app.jac +18 -0
  70. jaclang/runtimelib/tests/fixtures/custom_access_validation.jac +1 -1
  71. jaclang/runtimelib/tests/fixtures/savable_object.jac +4 -5
  72. jaclang/runtimelib/tests/fixtures/serve_api.jac +75 -0
  73. jaclang/runtimelib/tests/test_client_bundle.py +55 -0
  74. jaclang/runtimelib/tests/test_client_render.py +63 -0
  75. jaclang/runtimelib/tests/test_serve.py +1069 -0
  76. jaclang/settings.py +0 -2
  77. jaclang/tests/fixtures/iife_functions.jac +142 -0
  78. jaclang/tests/fixtures/iife_functions_client.jac +143 -0
  79. jaclang/tests/fixtures/multistatement_lambda.jac +116 -0
  80. jaclang/tests/fixtures/multistatement_lambda_client.jac +113 -0
  81. jaclang/tests/fixtures/needs_import_dup.jac +6 -4
  82. jaclang/tests/fixtures/py_run.py +7 -5
  83. jaclang/tests/fixtures/pyfunc_fstr.py +2 -2
  84. jaclang/tests/fixtures/simple_lambda_test.jac +12 -0
  85. jaclang/tests/test_cli.py +1 -1
  86. jaclang/tests/test_language.py +10 -39
  87. jaclang/tests/test_reference.py +17 -2
  88. jaclang/utils/NonGPT.py +375 -0
  89. jaclang/utils/helpers.py +44 -16
  90. jaclang/utils/lang_tools.py +31 -4
  91. jaclang/utils/tests/test_lang_tools.py +1 -1
  92. jaclang/utils/treeprinter.py +8 -3
  93. {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/METADATA +3 -3
  94. {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/RECORD +96 -66
  95. jaclang/compiler/passes/main/binder_pass.py +0 -594
  96. jaclang/compiler/passes/main/tests/fixtures/sym_binder.jac +0 -47
  97. jaclang/compiler/passes/main/tests/test_binder_pass.py +0 -111
  98. jaclang/langserve/tests/session.jac +0 -294
  99. jaclang/langserve/tests/test_dev_server.py +0 -80
  100. jaclang/runtimelib/importer.py +0 -351
  101. jaclang/tests/test_typecheck.py +0 -542
  102. {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/WHEEL +0 -0
  103. {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,1069 @@
1
+ """Test for jac serve command and REST API server."""
2
+
3
+ import json
4
+ import os
5
+ import socket
6
+ import threading
7
+ import time
8
+ from urllib.error import HTTPError
9
+ from urllib.request import Request, urlopen
10
+
11
+ from jaclang.cli import cli
12
+ from jaclang.runtimelib.machine import JacMachine as Jac
13
+ from jaclang.runtimelib.server import JacAPIServer, UserManager
14
+ from jaclang.utils.test import TestCase
15
+
16
+
17
+ def get_free_port() -> int:
18
+ """Get a free port by binding to port 0 and releasing it."""
19
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
20
+ s.bind(("", 0))
21
+ s.listen(1)
22
+ port = s.getsockname()[1]
23
+ return port
24
+
25
+
26
+ class TestServeCommand(TestCase):
27
+ """Test jac serve REST API functionality."""
28
+
29
+ def setUp(self) -> None:
30
+ """Set up test."""
31
+ super().setUp()
32
+ self.server = None
33
+ self.server_thread = None
34
+ self.httpd = None
35
+ # Use dynamically allocated free port for each test
36
+ try:
37
+ self.port = get_free_port()
38
+ except PermissionError:
39
+ self.skipTest("Socket operations are not permitted in this environment")
40
+ return
41
+ self.base_url = f"http://localhost:{self.port}"
42
+ # Use unique session file for each test
43
+ test_name = self._testMethodName
44
+ self.session_file = self.fixture_abs_path(f"test_serve_{test_name}.session")
45
+
46
+ def tearDown(self) -> None:
47
+ """Tear down test."""
48
+ # Close user manager if it exists
49
+ if self.server and hasattr(self.server, 'user_manager'):
50
+ try:
51
+ self.server.user_manager.close()
52
+ except Exception:
53
+ pass
54
+
55
+ # Stop server if running
56
+ if self.httpd:
57
+ try:
58
+ self.httpd.shutdown()
59
+ self.httpd.server_close()
60
+ except Exception:
61
+ pass
62
+
63
+ # Wait for thread to finish
64
+ if self.server_thread and self.server_thread.is_alive():
65
+ self.server_thread.join(timeout=2)
66
+
67
+ # Clean up session files
68
+ self._del_session(self.session_file)
69
+ super().tearDown()
70
+
71
+ def _del_session(self, session: str) -> None:
72
+ """Delete session files including user database files."""
73
+ path = os.path.dirname(session)
74
+ prefix = os.path.basename(session)
75
+ if os.path.exists(path):
76
+ for file in os.listdir(path):
77
+ # Clean up session files and user database files (.users)
78
+ if file.startswith(prefix):
79
+ try:
80
+ os.remove(f"{path}/{file}")
81
+ except Exception:
82
+ pass
83
+
84
+ def _start_server(self) -> None:
85
+ """Start the API server in a background thread."""
86
+ from http.server import HTTPServer
87
+
88
+ # Load the module
89
+ base, mod, mach = cli.proc_file_sess(
90
+ self.fixture_abs_path("serve_api.jac"), ""
91
+ )
92
+ Jac.set_base_path(base)
93
+ Jac.jac_import(
94
+ target=mod,
95
+ base_path=base,
96
+ override_name="__main__",
97
+ lng="jac",
98
+ )
99
+
100
+ # Create server
101
+ self.server = JacAPIServer(
102
+ module_name="__main__",
103
+ session_path=self.session_file,
104
+ port=self.port,
105
+ )
106
+
107
+ # Start server in thread
108
+ def run_server():
109
+ try:
110
+ self.server.load_module()
111
+ handler_class = self.server.create_handler()
112
+ self.httpd = HTTPServer(("127.0.0.1", self.port), handler_class)
113
+ self.httpd.serve_forever()
114
+ except Exception:
115
+ pass
116
+
117
+ self.server_thread = threading.Thread(target=run_server, daemon=True)
118
+ self.server_thread.start()
119
+
120
+ # Wait for server to be ready
121
+ max_attempts = 50
122
+ for _ in range(max_attempts):
123
+ try:
124
+ self._request("GET", "/", timeout=10)
125
+ break
126
+ except Exception:
127
+ time.sleep(0.1)
128
+
129
+ def _request(
130
+ self, method: str, path: str, data: dict = None, token: str = None, timeout: int = 5
131
+ ) -> dict:
132
+ """Make HTTP request to server."""
133
+ status, payload, _ = self._request_raw(method, path, data=data, token=token, timeout=timeout)
134
+ try:
135
+ return json.loads(payload)
136
+ except json.JSONDecodeError as exc: # pragma: no cover - sanity guard
137
+ raise AssertionError(f"Expected JSON response, got: {payload}") from exc
138
+
139
+ def _request_raw(
140
+ self, method: str, path: str, data: dict = None, token: str = None, timeout: int = 5
141
+ ) -> tuple[int, str, dict[str, str]]:
142
+ """Make an HTTP request and return status, body, and headers."""
143
+ url = f"{self.base_url}{path}"
144
+ headers = {"Content-Type": "application/json"}
145
+
146
+ if token:
147
+ headers["Authorization"] = f"Bearer {token}"
148
+
149
+ body = json.dumps(data).encode() if data else None
150
+ request = Request(url, data=body, headers=headers, method=method)
151
+
152
+ try:
153
+ with urlopen(request, timeout=timeout) as response:
154
+ payload = response.read().decode()
155
+ return response.status, payload, dict(response.headers)
156
+ except HTTPError as e:
157
+ payload = e.read().decode()
158
+ return e.code, payload, dict(e.headers)
159
+
160
+ def test_user_manager_creation(self) -> None:
161
+ """Test UserManager creates users with unique roots."""
162
+ user_mgr = UserManager(self.session_file)
163
+
164
+ # Create first user
165
+ result1 = user_mgr.create_user("user1", "pass1")
166
+ self.assertIn("token", result1)
167
+ self.assertIn("root_id", result1)
168
+ self.assertEqual(result1["username"], "user1")
169
+
170
+ # Create second user
171
+ result2 = user_mgr.create_user("user2", "pass2")
172
+ self.assertIn("token", result2)
173
+ self.assertIn("root_id", result2)
174
+
175
+ # Users should have different roots
176
+ self.assertNotEqual(result1["root_id"], result2["root_id"])
177
+
178
+ # Duplicate username should fail
179
+ result3 = user_mgr.create_user("user1", "pass3")
180
+ self.assertIn("error", result3)
181
+
182
+ def test_user_manager_authentication(self) -> None:
183
+ """Test UserManager authentication."""
184
+ user_mgr = UserManager(self.session_file)
185
+
186
+ # Create user
187
+ create_result = user_mgr.create_user("testuser", "testpass")
188
+ original_token = create_result["token"]
189
+
190
+ # Authenticate with correct credentials
191
+ auth_result = user_mgr.authenticate("testuser", "testpass")
192
+ self.assertIsNotNone(auth_result)
193
+ self.assertEqual(auth_result["username"], "testuser")
194
+ self.assertEqual(auth_result["token"], original_token)
195
+
196
+ # Wrong password
197
+ auth_fail = user_mgr.authenticate("testuser", "wrongpass")
198
+ self.assertIsNone(auth_fail)
199
+
200
+ # Nonexistent user
201
+ auth_fail2 = user_mgr.authenticate("nouser", "pass")
202
+ self.assertIsNone(auth_fail2)
203
+
204
+ def test_user_manager_token_validation(self) -> None:
205
+ """Test UserManager token validation."""
206
+ user_mgr = UserManager(self.session_file)
207
+
208
+ # Create user
209
+ result = user_mgr.create_user("validuser", "validpass")
210
+ token = result["token"]
211
+
212
+ # Valid token
213
+ username = user_mgr.validate_token(token)
214
+ self.assertEqual(username, "validuser")
215
+
216
+ # Invalid token
217
+ username = user_mgr.validate_token("invalid_token")
218
+ self.assertIsNone(username)
219
+
220
+ def test_server_user_creation(self) -> None:
221
+ """Test user creation endpoint."""
222
+ self._start_server()
223
+
224
+ # Create user
225
+ result = self._request(
226
+ "POST",
227
+ "/user/create",
228
+ {"username": "alice", "password": "secret123"}
229
+ )
230
+
231
+ self.assertIn("username", result)
232
+ self.assertIn("token", result)
233
+ self.assertIn("root_id", result)
234
+ self.assertEqual(result["username"], "alice")
235
+
236
+ def test_server_user_login(self) -> None:
237
+ """Test user login endpoint."""
238
+ self._start_server()
239
+
240
+ # Create user
241
+ create_result = self._request(
242
+ "POST",
243
+ "/user/create",
244
+ {"username": "bob", "password": "pass456"}
245
+ )
246
+
247
+ # Login with correct credentials
248
+ login_result = self._request(
249
+ "POST",
250
+ "/user/login",
251
+ {"username": "bob", "password": "pass456"}
252
+ )
253
+
254
+ self.assertIn("token", login_result)
255
+ self.assertEqual(login_result["username"], "bob")
256
+ self.assertEqual(login_result["root_id"], create_result["root_id"])
257
+
258
+ # Login with wrong password
259
+ login_fail = self._request(
260
+ "POST",
261
+ "/user/login",
262
+ {"username": "bob", "password": "wrongpass"}
263
+ )
264
+
265
+ self.assertIn("error", login_fail)
266
+
267
+ def test_server_authentication_required(self) -> None:
268
+ """Test that protected endpoints require authentication."""
269
+ self._start_server()
270
+
271
+ # Try to access protected endpoint without token
272
+ result = self._request("GET", "/functions")
273
+ self.assertIn("error", result)
274
+ self.assertIn("Unauthorized", result["error"])
275
+
276
+ def test_server_list_functions(self) -> None:
277
+ """Test listing functions endpoint."""
278
+ self._start_server()
279
+
280
+ # Create user and get token
281
+ create_result = self._request(
282
+ "POST",
283
+ "/user/create",
284
+ {"username": "funcuser", "password": "pass"}
285
+ )
286
+ token = create_result["token"]
287
+
288
+ # List functions
289
+ result = self._request("GET", "/functions", token=token)
290
+
291
+ self.assertIn("functions", result)
292
+ self.assertIn("add_numbers", result["functions"])
293
+ self.assertIn("greet", result["functions"])
294
+
295
+ def test_server_get_function_signature(self) -> None:
296
+ """Test getting function signature."""
297
+ self._start_server()
298
+
299
+ # Create user
300
+ create_result = self._request(
301
+ "POST",
302
+ "/user/create",
303
+ {"username": "siguser", "password": "pass"}
304
+ )
305
+ token = create_result["token"]
306
+
307
+ # Get signature
308
+ result = self._request("GET", "/function/add_numbers", token=token)
309
+
310
+ self.assertIn("signature", result)
311
+ sig = result["signature"]
312
+ self.assertIn("parameters", sig)
313
+ self.assertIn("a", sig["parameters"])
314
+ self.assertIn("b", sig["parameters"])
315
+ self.assertTrue(sig["parameters"]["a"]["required"])
316
+ self.assertTrue(sig["parameters"]["b"]["required"])
317
+
318
+ def test_server_call_function(self) -> None:
319
+ """Test calling a function endpoint."""
320
+ self._start_server()
321
+
322
+ # Create user
323
+ create_result = self._request(
324
+ "POST",
325
+ "/user/create",
326
+ {"username": "calluser", "password": "pass"}
327
+ )
328
+ token = create_result["token"]
329
+
330
+ # Call add_numbers
331
+ result = self._request(
332
+ "POST",
333
+ "/function/add_numbers",
334
+ {"args": {"a": 10, "b": 25}},
335
+ token=token
336
+ )
337
+
338
+ self.assertIn("result", result)
339
+ self.assertEqual(result["result"], 35)
340
+
341
+ # Call greet
342
+ result2 = self._request(
343
+ "POST",
344
+ "/function/greet",
345
+ {"args": {"name": "World"}},
346
+ token=token
347
+ )
348
+
349
+ self.assertIn("result", result2)
350
+ self.assertEqual(result2["result"], "Hello, World!")
351
+
352
+ def test_server_call_function_with_defaults(self) -> None:
353
+ """Test calling function with default parameters."""
354
+ self._start_server()
355
+
356
+ # Create user
357
+ create_result = self._request(
358
+ "POST",
359
+ "/user/create",
360
+ {"username": "defuser", "password": "pass"}
361
+ )
362
+ token = create_result["token"]
363
+
364
+ # Call greet without name (should use default)
365
+ result = self._request(
366
+ "POST",
367
+ "/function/greet",
368
+ {"args": {}},
369
+ token=token
370
+ )
371
+
372
+ self.assertIn("result", result)
373
+ self.assertEqual(result["result"], "Hello, World!")
374
+
375
+ def test_server_list_walkers(self) -> None:
376
+ """Test listing walkers endpoint."""
377
+ self._start_server()
378
+
379
+ # Create user
380
+ create_result = self._request(
381
+ "POST",
382
+ "/user/create",
383
+ {"username": "walkuser", "password": "pass"}
384
+ )
385
+ token = create_result["token"]
386
+
387
+ # List walkers
388
+ result = self._request("GET", "/walkers", token=token)
389
+
390
+ self.assertIn("walkers", result)
391
+ self.assertIn("CreateTask", result["walkers"])
392
+ self.assertIn("ListTasks", result["walkers"])
393
+ self.assertIn("CompleteTask", result["walkers"])
394
+
395
+ def test_server_get_walker_info(self) -> None:
396
+ """Test getting walker information."""
397
+ self._start_server()
398
+
399
+ # Create user
400
+ create_result = self._request(
401
+ "POST",
402
+ "/user/create",
403
+ {"username": "infouser", "password": "pass"}
404
+ )
405
+ token = create_result["token"]
406
+
407
+ # Get walker info
408
+ result = self._request("GET", "/walker/CreateTask", token=token)
409
+
410
+ self.assertIn("info", result)
411
+ info = result["info"]
412
+ self.assertIn("fields", info)
413
+ self.assertIn("title", info["fields"])
414
+ self.assertIn("priority", info["fields"])
415
+ self.assertIn("_jac_spawn_node", info["fields"])
416
+
417
+ # Check that priority has a default
418
+ self.assertFalse(info["fields"]["priority"]["required"])
419
+ self.assertIsNotNone(info["fields"]["priority"]["default"])
420
+
421
+ def test_server_spawn_walker(self) -> None:
422
+ """Test spawning a walker."""
423
+ self._start_server()
424
+
425
+ # Create user
426
+ create_result = self._request(
427
+ "POST",
428
+ "/user/create",
429
+ {"username": "spawnuser", "password": "pass"}
430
+ )
431
+ token = create_result["token"]
432
+
433
+ # Spawn CreateTask walker
434
+ result = self._request(
435
+ "POST",
436
+ "/walker/CreateTask",
437
+ {"fields": {"title": "Test Task", "priority": 2}},
438
+ token=token
439
+ )
440
+
441
+ # If error, print for debugging
442
+ if "error" in result:
443
+ print(f"\nWalker spawn error: {result['error']}")
444
+ if "traceback" in result:
445
+ print(f"Traceback:\n{result['traceback']}")
446
+
447
+ self.assertIn("result", result)
448
+ self.assertIn("reports", result)
449
+
450
+ # Spawn ListTasks walker to verify task was created
451
+ result2 = self._request(
452
+ "POST",
453
+ "/walker/ListTasks",
454
+ {"fields": {}},
455
+ token=token
456
+ )
457
+
458
+ self.assertIn("result", result2)
459
+
460
+ def test_server_user_isolation(self) -> None:
461
+ """Test that users have isolated graph spaces."""
462
+ self._start_server()
463
+
464
+ # Create two users
465
+ user1 = self._request(
466
+ "POST",
467
+ "/user/create",
468
+ {"username": "user1", "password": "pass1"}
469
+ )
470
+ user2 = self._request(
471
+ "POST",
472
+ "/user/create",
473
+ {"username": "user2", "password": "pass2"}
474
+ )
475
+
476
+ token1 = user1["token"]
477
+ token2 = user2["token"]
478
+
479
+ # User1 creates a task
480
+ self._request(
481
+ "POST",
482
+ "/walker/CreateTask",
483
+ {"fields": {"title": "User1 Task", "priority": 1}},
484
+ token=token1
485
+ )
486
+
487
+ # User2 creates a different task
488
+ self._request(
489
+ "POST",
490
+ "/walker/CreateTask",
491
+ {"fields": {"title": "User2 Task", "priority": 2}},
492
+ token=token2
493
+ )
494
+
495
+ # Both users should have different root IDs
496
+ self.assertNotEqual(user1["root_id"], user2["root_id"])
497
+
498
+ def test_server_invalid_function(self) -> None:
499
+ """Test calling nonexistent function."""
500
+ self._start_server()
501
+
502
+ # Create user
503
+ create_result = self._request(
504
+ "POST",
505
+ "/user/create",
506
+ {"username": "invaliduser", "password": "pass"}
507
+ )
508
+ token = create_result["token"]
509
+
510
+ # Try to call nonexistent function
511
+ result = self._request(
512
+ "POST",
513
+ "/function/nonexistent",
514
+ {"args": {}},
515
+ token=token
516
+ )
517
+
518
+ self.assertIn("error", result)
519
+
520
+ def test_server_invalid_walker(self) -> None:
521
+ """Test spawning nonexistent walker."""
522
+ self._start_server()
523
+
524
+ # Create user
525
+ create_result = self._request(
526
+ "POST",
527
+ "/user/create",
528
+ {"username": "invalidwalk", "password": "pass"}
529
+ )
530
+ token = create_result["token"]
531
+
532
+ # Try to spawn nonexistent walker
533
+ result = self._request(
534
+ "POST",
535
+ "/walker/NonExistentWalker",
536
+ {"fields": {}},
537
+ token=token
538
+ )
539
+
540
+ self.assertIn("error", result)
541
+
542
+ def test_client_page_and_bundle_endpoints(self) -> None:
543
+ """Render a client page and fetch the bundled JavaScript."""
544
+ self._start_server()
545
+
546
+ create_result = self._request(
547
+ "POST",
548
+ "/user/create",
549
+ {"username": "pageuser", "password": "pass"}
550
+ )
551
+ token = create_result["token"]
552
+
553
+ # Use longer timeout for page requests (they trigger bundle building)
554
+ status, html_body, headers = self._request_raw(
555
+ "GET",
556
+ "/page/client_page",
557
+ token=token,
558
+ timeout=15
559
+ )
560
+
561
+ self.assertEqual(status, 200)
562
+ self.assertIn("text/html", headers.get("Content-Type", ""))
563
+ self.assertIn("<div id=\"__jac_root\">", html_body)
564
+ self.assertIn("Runtime Test", html_body)
565
+ self.assertIn("/static/client.js?hash=", html_body)
566
+
567
+ # Bundle should be cached from page request, but use longer timeout for CI safety
568
+ status_js, js_body, js_headers = self._request_raw("GET", "/static/client.js", timeout=15)
569
+ self.assertEqual(status_js, 200)
570
+ self.assertIn("application/javascript", js_headers.get("Content-Type", ""))
571
+ self.assertIn("function __jacJsx", js_body)
572
+
573
+ def test_server_root_endpoint(self) -> None:
574
+ """Test root endpoint returns API information."""
575
+ self._start_server()
576
+
577
+ result = self._request("GET", "/")
578
+
579
+ self.assertIn("message", result)
580
+ self.assertIn("endpoints", result)
581
+ self.assertIn("POST /user/create", result["endpoints"])
582
+ self.assertIn("GET /functions", result["endpoints"])
583
+ self.assertIn("GET /walkers", result["endpoints"])
584
+
585
+ def test_module_loading_and_introspection(self) -> None:
586
+ """Test that module loads correctly and introspection works."""
587
+ # Load module
588
+ base, mod, mach = cli.proc_file_sess(
589
+ self.fixture_abs_path("serve_api.jac"), ""
590
+ )
591
+ Jac.set_base_path(base)
592
+ Jac.jac_import(
593
+ target=mod,
594
+ base_path=base,
595
+ override_name="__main__",
596
+ lng="jac",
597
+ )
598
+
599
+ # Create server
600
+ server = JacAPIServer(
601
+ module_name="__main__",
602
+ session_path=self.session_file,
603
+ port=9999, # Different port, won't actually start
604
+ )
605
+ server.load_module()
606
+
607
+ # Check module loaded
608
+ self.assertIsNotNone(server.module)
609
+
610
+ # Check functions discovered
611
+ functions = server.get_functions()
612
+ self.assertIn("add_numbers", functions)
613
+ self.assertIn("greet", functions)
614
+
615
+ # Check walkers discovered
616
+ walkers = server.get_walkers()
617
+ self.assertIn("CreateTask", walkers)
618
+ self.assertIn("ListTasks", walkers)
619
+ self.assertIn("CompleteTask", walkers)
620
+
621
+ # Check introspection
622
+ sig = server.introspect_callable(functions["add_numbers"])
623
+ self.assertIn("parameters", sig)
624
+ self.assertIn("a", sig["parameters"])
625
+ self.assertIn("b", sig["parameters"])
626
+
627
+ # Check walker introspection
628
+ walker_info = server.introspect_walker(walkers["CreateTask"])
629
+ self.assertIn("fields", walker_info)
630
+ self.assertIn("title", walker_info["fields"])
631
+ self.assertIn("priority", walker_info["fields"])
632
+
633
+ mach.close()
634
+
635
+ def test_csr_mode_empty_root(self) -> None:
636
+ """Test CSR mode returns empty __jac_root for client-side rendering."""
637
+ self._start_server()
638
+
639
+ # Create user
640
+ create_result = self._request(
641
+ "POST",
642
+ "/user/create",
643
+ {"username": "csruser", "password": "pass"}
644
+ )
645
+ token = create_result["token"]
646
+
647
+ # Request page in CSR mode using query parameter (longer timeout for bundle building)
648
+ status, html_body, headers = self._request_raw(
649
+ "GET",
650
+ "/page/client_page?mode=csr",
651
+ token=token,
652
+ timeout=15
653
+ )
654
+
655
+ self.assertEqual(status, 200)
656
+ self.assertIn("text/html", headers.get("Content-Type", ""))
657
+
658
+ # In CSR mode, __jac_root should be empty (no SSR)
659
+ self.assertIn('<div id="__jac_root"></div>', html_body)
660
+
661
+ # But __jac_init__ and client.js should still be present
662
+ self.assertIn('<script id="__jac_init__" type="application/json">', html_body)
663
+ self.assertIn("/static/client.js?hash=", html_body)
664
+
665
+ # __jac_init__ should still contain the function name and args
666
+ self.assertIn('"function": "client_page"', html_body)
667
+
668
+ def test_default_page_is_csr(self) -> None:
669
+ """Requesting a page without mode parameter returns empty CSR shell."""
670
+ self._start_server()
671
+
672
+ # Create user
673
+ create_result = self._request(
674
+ "POST",
675
+ "/user/create",
676
+ {"username": "ssruser", "password": "pass"}
677
+ )
678
+ token = create_result["token"]
679
+
680
+ # Request page without specifying mode (CSR-only, longer timeout for bundle building)
681
+ status, html_body, headers = self._request_raw(
682
+ "GET",
683
+ "/page/client_page",
684
+ token=token,
685
+ timeout=15
686
+ )
687
+
688
+ self.assertEqual(status, 200)
689
+ self.assertIn("text/html", headers.get("Content-Type", ""))
690
+
691
+ # CSR shell should be empty; client renders later
692
+ self.assertIn('<div id="__jac_root"></div>', html_body)
693
+
694
+ # __jac_init__ and client.js should still be present for hydration
695
+ self.assertIn('<script id="__jac_init__" type="application/json">', html_body)
696
+ self.assertIn("/static/client.js?hash=", html_body)
697
+
698
+ def test_csr_mode_with_server_default(self) -> None:
699
+ """render_client_page returns an empty shell when called directly."""
700
+ # Load module
701
+ base, mod, mach = cli.proc_file_sess(
702
+ self.fixture_abs_path("serve_api.jac"), ""
703
+ )
704
+ Jac.set_base_path(base)
705
+ Jac.jac_import(
706
+ target=mod,
707
+ base_path=base,
708
+ override_name="__main__",
709
+ lng="jac",
710
+ )
711
+
712
+ # Create server
713
+ server = JacAPIServer(
714
+ module_name="__main__",
715
+ session_path=self.session_file,
716
+ port=9998,
717
+ )
718
+ server.load_module()
719
+
720
+ # Create a test user
721
+ server.user_manager.create_user("testuser", "testpass")
722
+
723
+ # Call render_client_page (always CSR)
724
+ result = server.render_client_page(
725
+ function_name="client_page",
726
+ args={},
727
+ username="testuser",
728
+ )
729
+
730
+ # Should have empty HTML body (CSR mode)
731
+ self.assertIn("html", result)
732
+ html_content = result["html"]
733
+ self.assertIn('<div id="__jac_root"></div>', html_content)
734
+
735
+ mach.close()
736
+
737
+ def test_root_data_persistence_across_server_restarts(self) -> None:
738
+ """Test that user data and graph persist across server restarts.
739
+
740
+ This test verifies that both user credentials and graph data (nodes and
741
+ edges attached to a root) are properly persisted to the session file and
742
+ can be accessed after a server restart.
743
+ """
744
+ # Start first server instance
745
+ self._start_server()
746
+
747
+ # Create user and get token
748
+ create_result = self._request(
749
+ "POST",
750
+ "/user/create",
751
+ {"username": "persistuser", "password": "testpass123"}
752
+ )
753
+ token = create_result["token"]
754
+ root_id = create_result["root_id"]
755
+
756
+ # Create multiple tasks on the root node
757
+ task1_result = self._request(
758
+ "POST",
759
+ "/walker/CreateTask",
760
+ {"fields": {"title": "Persistent Task 1", "priority": 1}},
761
+ token=token
762
+ )
763
+ self.assertIn("result", task1_result)
764
+
765
+ task2_result = self._request(
766
+ "POST",
767
+ "/walker/CreateTask",
768
+ {"fields": {"title": "Persistent Task 2", "priority": 2}},
769
+ token=token
770
+ )
771
+ self.assertIn("result", task2_result)
772
+
773
+ task3_result = self._request(
774
+ "POST",
775
+ "/walker/CreateTask",
776
+ {"fields": {"title": "Persistent Task 3", "priority": 3}},
777
+ token=token
778
+ )
779
+ self.assertIn("result", task3_result)
780
+
781
+ # List tasks to verify they were created
782
+ list_before = self._request(
783
+ "POST",
784
+ "/walker/ListTasks",
785
+ {"fields": {}},
786
+ token=token
787
+ )
788
+ self.assertIn("result", list_before)
789
+
790
+ # Shutdown first server instance
791
+ # Close user manager first to release the shelf lock
792
+ if self.server and hasattr(self.server, 'user_manager'):
793
+ self.server.user_manager.close()
794
+
795
+ if self.httpd:
796
+ self.httpd.shutdown()
797
+ self.httpd.server_close()
798
+ self.httpd = None
799
+
800
+ if self.server_thread and self.server_thread.is_alive():
801
+ self.server_thread.join(timeout=2)
802
+
803
+ # Wait a moment to ensure server is fully stopped
804
+ time.sleep(0.5)
805
+
806
+ # Start second server instance with the same session file
807
+ self._start_server()
808
+
809
+ # Login with the same credentials
810
+ login_result = self._request(
811
+ "POST",
812
+ "/user/login",
813
+ {"username": "persistuser", "password": "testpass123"}
814
+ )
815
+
816
+ # User should be able to log in successfully
817
+ self.assertIn("token", login_result)
818
+ self.assertNotIn("error", login_result)
819
+
820
+ new_token = login_result["token"]
821
+ new_root_id = login_result["root_id"]
822
+
823
+ # Root ID should be the same (same user, same root)
824
+ self.assertEqual(new_root_id, root_id)
825
+
826
+ # Token should be the same (persisted from before)
827
+ self.assertEqual(new_token, token)
828
+
829
+ # List tasks again to verify they persisted
830
+ list_after = self._request(
831
+ "POST",
832
+ "/walker/ListTasks",
833
+ {"fields": {}},
834
+ token=new_token
835
+ )
836
+
837
+ # The ListTasks walker should successfully run
838
+ self.assertIn("result", list_after)
839
+
840
+ # Complete one of the tasks to verify we can still interact with persisted data
841
+ complete_result = self._request(
842
+ "POST",
843
+ "/walker/CompleteTask",
844
+ {"fields": {"title": "Persistent Task 2"}},
845
+ token=new_token
846
+ )
847
+ self.assertIn("result", complete_result)
848
+
849
+ def test_client_bundle_has_object_get_polyfill(self) -> None:
850
+ """Test that client bundle includes Object.prototype.get polyfill."""
851
+ self._start_server()
852
+
853
+ # Pre-warm the bundle by requesting a page first (triggers bundle build)
854
+ # This ensures the bundle is cached before we test it directly
855
+ try:
856
+ self._request("GET", "/")
857
+ except Exception:
858
+ pass # Ignore errors, we just want to trigger bundle building
859
+
860
+ # Fetch the client bundle with longer timeout for CI environments
861
+ # Bundle building can be slow on CI runners with limited resources
862
+ status, js_body, headers = self._request_raw("GET", "/static/client.js", timeout=15)
863
+
864
+ self.assertEqual(status, 200)
865
+ self.assertIn("application/javascript", headers.get("Content-Type", ""))
866
+
867
+ # Verify the polyfill function is in the runtime (now part of client_runtime.jac)
868
+ self.assertIn("__jacEnsureObjectGetPolyfill", js_body)
869
+ self.assertIn("Object.defineProperty", js_body)
870
+
871
+ # Verify core runtime functions are present
872
+ self.assertIn("__jacJsx", js_body)
873
+ self.assertIn("__jacRegisterClientModule", js_body)
874
+
875
+ def test_login_form_renders_with_correct_elements(self) -> None:
876
+ """Test that client page renders with correct HTML elements via HTTP endpoint."""
877
+ self._start_server()
878
+
879
+ # Create user
880
+ create_result = self._request(
881
+ "POST",
882
+ "/user/create",
883
+ {"username": "formuser", "password": "pass"}
884
+ )
885
+ token = create_result["token"]
886
+
887
+ # Request the client_page endpoint (longer timeout for bundle building)
888
+ status, html_body, headers = self._request_raw(
889
+ "GET",
890
+ "/page/client_page",
891
+ token=token,
892
+ timeout=15
893
+ )
894
+
895
+ self.assertEqual(status, 200)
896
+ self.assertIn("text/html", headers.get("Content-Type", ""))
897
+
898
+ # Check basic HTML structure
899
+ self.assertIn("<!DOCTYPE html>", html_body)
900
+ self.assertIn('<div id="__jac_root">', html_body)
901
+ self.assertIn('<script id="__jac_init__"', html_body)
902
+ self.assertIn("/static/client.js?hash=", html_body)
903
+
904
+ # Verify __jac_init__ contains the right function and global
905
+ self.assertIn('"function": "client_page"', html_body)
906
+ self.assertIn('"WELCOME_TITLE": "Runtime Test"', html_body) # Global variable
907
+
908
+ # Fetch and verify the bundle (should be cached from page request, but use longer timeout for CI)
909
+ status_js, js_body, _ = self._request_raw("GET", "/static/client.js", timeout=15)
910
+ self.assertEqual(status_js, 200)
911
+
912
+ # Verify the bundle has the polyfill setup function (now part of client_runtime.jac)
913
+ self.assertIn("__jacEnsureObjectGetPolyfill", js_body)
914
+
915
+ # Verify the function is in the bundle
916
+ self.assertIn("function client_page", js_body)
917
+
918
+ def test_default_page_is_csr(self) -> None:
919
+ """Test that the default page response is CSR (client-side rendering)."""
920
+ self._start_server()
921
+
922
+ # Create user
923
+ create_result = self._request(
924
+ "POST",
925
+ "/user/create",
926
+ {"username": "csrdefaultuser", "password": "pass"}
927
+ )
928
+ token = create_result["token"]
929
+
930
+ # Request page WITHOUT specifying mode (should use default, longer timeout for bundle building)
931
+ status, html_body, headers = self._request_raw(
932
+ "GET",
933
+ "/page/client_page",
934
+ token=token,
935
+ timeout=15
936
+ )
937
+
938
+ self.assertEqual(status, 200)
939
+ self.assertIn("text/html", headers.get("Content-Type", ""))
940
+
941
+ # In CSR mode (default), __jac_root should be empty
942
+ self.assertIn('<div id="__jac_root"></div>', html_body)
943
+
944
+ # Should NOT contain pre-rendered content
945
+ # (The content will be rendered on the client side)
946
+ # Note: We check that the root div is completely empty
947
+ import re
948
+ root_match = re.search(r'<div id="__jac_root">(.*?)</div>', html_body)
949
+ self.assertIsNotNone(root_match)
950
+ root_content = root_match.group(1)
951
+ self.assertEqual(root_content, "") # Should be empty string
952
+
953
+ # Verify that explicitly requesting SSR mode is ignored (still CSR, longer timeout for bundle building)
954
+ status_ssr, html_ssr, _ = self._request_raw(
955
+ "GET",
956
+ "/page/client_page?mode=ssr",
957
+ token=token,
958
+ timeout=15
959
+ )
960
+ self.assertEqual(status_ssr, 200)
961
+
962
+ self.assertIn('<div id="__jac_root"></div>', html_ssr)
963
+
964
+ def test_faux_flag_prints_endpoint_docs(self) -> None:
965
+ """Test that --faux flag prints endpoint documentation without starting server."""
966
+ import io
967
+ import sys
968
+ from contextlib import redirect_stdout
969
+
970
+ # Capture stdout
971
+ captured_output = io.StringIO()
972
+
973
+ try:
974
+ with redirect_stdout(captured_output):
975
+ # Call serve with faux=True
976
+ cli.serve(
977
+ filename=self.fixture_abs_path("serve_api.jac"),
978
+ session=self.session_file,
979
+ port=self.port,
980
+ main=True,
981
+ faux=True
982
+ )
983
+ except SystemExit:
984
+ pass # serve() may call exit() in some error cases
985
+
986
+ output = captured_output.getvalue()
987
+
988
+ # Verify function endpoints are documented
989
+ self.assertIn("FUNCTIONS", output)
990
+ self.assertIn("/function/add_numbers", output)
991
+ self.assertIn("/function/greet", output)
992
+
993
+ # Verify walker endpoints are documented
994
+ self.assertIn("WALKERS", output)
995
+ self.assertIn("/walker/CreateTask", output)
996
+ self.assertIn("/walker/ListTasks", output)
997
+ self.assertIn("/walker/CompleteTask", output)
998
+
999
+ # Verify client page endpoints section is documented
1000
+ self.assertIn("CLIENT PAGES", output)
1001
+ self.assertIn("client_page", output)
1002
+
1003
+ # Verify summary is present
1004
+ self.assertIn("TOTAL:", output)
1005
+ self.assertIn("2 functions", output)
1006
+ self.assertIn("3 walkers", output)
1007
+ self.assertIn("16 endpoints", output)
1008
+
1009
+ # Verify parameter details are included
1010
+ self.assertIn("required", output)
1011
+ self.assertIn("optional", output)
1012
+ self.assertIn("Bearer token", output)
1013
+
1014
+ def test_faux_flag_with_littlex_example(self) -> None:
1015
+ """Test that --faux flag correctly identifies functions, walkers, and endpoints in littleX example."""
1016
+ import io
1017
+ from contextlib import redirect_stdout
1018
+
1019
+ # Get the absolute path to littleX file
1020
+ import os
1021
+ littlex_path = os.path.abspath(
1022
+ os.path.join(
1023
+ os.path.dirname(__file__),
1024
+ "../../../examples/littleX/littleX_single_nodeps.jac"
1025
+ )
1026
+ )
1027
+
1028
+ # Skip test if file doesn't exist
1029
+ if not os.path.exists(littlex_path):
1030
+ self.skipTest(f"LittleX example not found at {littlex_path}")
1031
+
1032
+ # Capture stdout
1033
+ captured_output = io.StringIO()
1034
+
1035
+ try:
1036
+ with redirect_stdout(captured_output):
1037
+ # Call serve with faux=True on littleX example
1038
+ cli.serve(
1039
+ filename=littlex_path,
1040
+ session=self.session_file,
1041
+ port=self.port,
1042
+ main=True,
1043
+ faux=True
1044
+ )
1045
+ except SystemExit:
1046
+ pass # serve() may call exit() in some error cases
1047
+
1048
+ output = captured_output.getvalue()
1049
+
1050
+
1051
+ self.assertIn("littleX_single_nodeps", output)
1052
+ self.assertIn("0 functions", output)
1053
+ self.assertIn("15 walkers", output)
1054
+ self.assertIn("36 endpoints", output)
1055
+
1056
+ # Verify some specific walker endpoints are documented
1057
+ self.assertIn("/walker/visit_profile", output)
1058
+ self.assertIn("/walker/create_tweet", output)
1059
+ self.assertIn("/walker/load_feed", output)
1060
+ self.assertIn("/walker/update_profile", output)
1061
+
1062
+ # Verify authentication and introspection endpoints are still present
1063
+ self.assertIn("/user/create", output)
1064
+ self.assertIn("Available", output)
1065
+ self.assertIn("27", output) # 27 client functions
1066
+ # Verify some client functions are listed
1067
+ self.assertIn("App", output)
1068
+ self.assertIn("FeedView", output)
1069
+ self.assertIn("/page/", output)