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.
- jaclang/cli/cli.py +147 -25
- jaclang/cli/cmdreg.py +144 -8
- jaclang/compiler/__init__.py +6 -1
- jaclang/compiler/codeinfo.py +16 -1
- jaclang/compiler/constant.py +33 -13
- jaclang/compiler/jac.lark +130 -31
- jaclang/compiler/larkparse/jac_parser.py +2 -2
- jaclang/compiler/parser.py +567 -176
- jaclang/compiler/passes/__init__.py +2 -1
- jaclang/compiler/passes/ast_gen/__init__.py +5 -0
- jaclang/compiler/passes/ast_gen/base_ast_gen_pass.py +54 -0
- jaclang/compiler/passes/ast_gen/jsx_processor.py +344 -0
- jaclang/compiler/passes/ecmascript/__init__.py +25 -0
- jaclang/compiler/passes/ecmascript/es_unparse.py +576 -0
- jaclang/compiler/passes/ecmascript/esast_gen_pass.py +2068 -0
- jaclang/compiler/passes/ecmascript/estree.py +972 -0
- jaclang/compiler/passes/ecmascript/tests/__init__.py +1 -0
- jaclang/compiler/passes/ecmascript/tests/fixtures/advanced_language_features.jac +170 -0
- jaclang/compiler/passes/ecmascript/tests/fixtures/class_separate_impl.impl.jac +30 -0
- jaclang/compiler/passes/ecmascript/tests/fixtures/class_separate_impl.jac +14 -0
- jaclang/compiler/passes/ecmascript/tests/fixtures/client_jsx.jac +89 -0
- jaclang/compiler/passes/ecmascript/tests/fixtures/core_language_features.jac +195 -0
- jaclang/compiler/passes/ecmascript/tests/test_esast_gen_pass.py +167 -0
- jaclang/compiler/passes/ecmascript/tests/test_js_generation.py +239 -0
- jaclang/compiler/passes/main/__init__.py +0 -3
- jaclang/compiler/passes/main/annex_pass.py +23 -1
- jaclang/compiler/passes/main/pyast_gen_pass.py +324 -234
- jaclang/compiler/passes/main/pyast_load_pass.py +46 -11
- jaclang/compiler/passes/main/pyjac_ast_link_pass.py +2 -0
- jaclang/compiler/passes/main/sym_tab_build_pass.py +18 -1
- jaclang/compiler/passes/main/tests/fixtures/autoimpl.cl.jac +7 -0
- jaclang/compiler/passes/main/tests/fixtures/checker_arity.jac +3 -0
- jaclang/compiler/passes/main/tests/fixtures/checker_class_construct.jac +33 -0
- jaclang/compiler/passes/main/tests/fixtures/defuse_modpath.jac +7 -0
- jaclang/compiler/passes/main/tests/fixtures/member_access_type_resolve.jac +2 -1
- jaclang/compiler/passes/main/tests/test_checker_pass.py +31 -2
- jaclang/compiler/passes/main/tests/test_def_use_pass.py +12 -0
- jaclang/compiler/passes/main/tests/test_import_pass.py +23 -4
- jaclang/compiler/passes/main/tests/test_pyast_gen_pass.py +25 -0
- jaclang/compiler/passes/main/type_checker_pass.py +7 -0
- jaclang/compiler/passes/tool/doc_ir_gen_pass.py +115 -0
- jaclang/compiler/passes/tool/fuse_comments_pass.py +1 -10
- jaclang/compiler/passes/tool/tests/test_jac_format_pass.py +4 -1
- jaclang/compiler/passes/transform.py +9 -1
- jaclang/compiler/passes/uni_pass.py +5 -7
- jaclang/compiler/program.py +22 -25
- jaclang/compiler/tests/test_client_codegen.py +113 -0
- jaclang/compiler/tests/test_importer.py +12 -10
- jaclang/compiler/tests/test_parser.py +249 -3
- jaclang/compiler/type_system/type_evaluator.jac +169 -50
- jaclang/compiler/type_system/type_utils.py +1 -1
- jaclang/compiler/type_system/types.py +6 -0
- jaclang/compiler/unitree.py +430 -84
- jaclang/langserve/engine.jac +224 -288
- jaclang/langserve/sem_manager.jac +12 -8
- jaclang/langserve/server.jac +48 -48
- jaclang/langserve/tests/fixtures/greet.py +17 -0
- jaclang/langserve/tests/fixtures/md_path.jac +22 -0
- jaclang/langserve/tests/fixtures/user.jac +15 -0
- jaclang/langserve/tests/test_server.py +66 -371
- jaclang/lib.py +1 -1
- jaclang/runtimelib/client_bundle.py +169 -0
- jaclang/runtimelib/client_runtime.jac +586 -0
- jaclang/runtimelib/constructs.py +2 -0
- jaclang/runtimelib/machine.py +259 -100
- jaclang/runtimelib/meta_importer.py +111 -22
- jaclang/runtimelib/mtp.py +15 -0
- jaclang/runtimelib/server.py +1089 -0
- jaclang/runtimelib/tests/fixtures/client_app.jac +18 -0
- jaclang/runtimelib/tests/fixtures/custom_access_validation.jac +1 -1
- jaclang/runtimelib/tests/fixtures/savable_object.jac +4 -5
- jaclang/runtimelib/tests/fixtures/serve_api.jac +75 -0
- jaclang/runtimelib/tests/test_client_bundle.py +55 -0
- jaclang/runtimelib/tests/test_client_render.py +63 -0
- jaclang/runtimelib/tests/test_serve.py +1069 -0
- jaclang/settings.py +0 -2
- jaclang/tests/fixtures/iife_functions.jac +142 -0
- jaclang/tests/fixtures/iife_functions_client.jac +143 -0
- jaclang/tests/fixtures/multistatement_lambda.jac +116 -0
- jaclang/tests/fixtures/multistatement_lambda_client.jac +113 -0
- jaclang/tests/fixtures/needs_import_dup.jac +6 -4
- jaclang/tests/fixtures/py_run.py +7 -5
- jaclang/tests/fixtures/pyfunc_fstr.py +2 -2
- jaclang/tests/fixtures/simple_lambda_test.jac +12 -0
- jaclang/tests/test_cli.py +1 -1
- jaclang/tests/test_language.py +10 -39
- jaclang/tests/test_reference.py +17 -2
- jaclang/utils/NonGPT.py +375 -0
- jaclang/utils/helpers.py +44 -16
- jaclang/utils/lang_tools.py +31 -4
- jaclang/utils/tests/test_lang_tools.py +1 -1
- jaclang/utils/treeprinter.py +8 -3
- {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/METADATA +3 -3
- {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/RECORD +96 -66
- jaclang/compiler/passes/main/binder_pass.py +0 -594
- jaclang/compiler/passes/main/tests/fixtures/sym_binder.jac +0 -47
- jaclang/compiler/passes/main/tests/test_binder_pass.py +0 -111
- jaclang/langserve/tests/session.jac +0 -294
- jaclang/langserve/tests/test_dev_server.py +0 -80
- jaclang/runtimelib/importer.py +0 -351
- jaclang/tests/test_typecheck.py +0 -542
- {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/WHEEL +0 -0
- {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)
|