jac-scale 0.1.1__py3-none-any.whl → 0.1.4__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.
- jac_scale/abstractions/config/app_config.jac +5 -2
- jac_scale/config_loader.jac +2 -1
- jac_scale/context.jac +2 -1
- jac_scale/factories/storage_factory.jac +75 -0
- jac_scale/google_sso_provider.jac +85 -0
- jac_scale/impl/config_loader.impl.jac +28 -3
- jac_scale/impl/context.impl.jac +1 -0
- jac_scale/impl/serve.impl.jac +749 -266
- jac_scale/impl/user_manager.impl.jac +349 -0
- jac_scale/impl/webhook.impl.jac +212 -0
- jac_scale/jserver/impl/jfast_api.impl.jac +4 -0
- jac_scale/memory_hierarchy.jac +3 -1
- jac_scale/plugin.jac +46 -3
- jac_scale/plugin_config.jac +28 -1
- jac_scale/serve.jac +33 -16
- jac_scale/sso_provider.jac +72 -0
- jac_scale/targets/kubernetes/kubernetes_config.jac +9 -15
- jac_scale/targets/kubernetes/kubernetes_target.jac +174 -15
- jac_scale/tests/fixtures/scale-feats/components/Button.cl.jac +32 -0
- jac_scale/tests/fixtures/scale-feats/main.jac +147 -0
- jac_scale/tests/fixtures/test_api.jac +89 -0
- jac_scale/tests/fixtures/test_restspec.jac +88 -0
- jac_scale/tests/test_deploy_k8s.py +2 -1
- jac_scale/tests/test_examples.py +180 -5
- jac_scale/tests/test_hooks.py +39 -0
- jac_scale/tests/test_restspec.py +289 -0
- jac_scale/tests/test_serve.py +411 -4
- jac_scale/tests/test_sso.py +273 -284
- jac_scale/tests/test_storage.py +274 -0
- jac_scale/user_manager.jac +49 -0
- jac_scale/webhook.jac +93 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/METADATA +11 -4
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/RECORD +36 -23
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/WHEEL +1 -1
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/entry_points.txt +0 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""jac-scale features test fixture.
|
|
2
|
+
|
|
3
|
+
Demonstrates jac-scale specific features that require FastAPI:
|
|
4
|
+
- Storage abstraction with file uploads
|
|
5
|
+
- UploadFile handling
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import from fastapi { UploadFile }
|
|
9
|
+
import from uuid { uuid4 }
|
|
10
|
+
|
|
11
|
+
# Initialize storage with custom base path
|
|
12
|
+
glob storage = store(base_path="./uploads");
|
|
13
|
+
|
|
14
|
+
# ============================================================================
|
|
15
|
+
# Storage API Walkers
|
|
16
|
+
# ============================================================================
|
|
17
|
+
|
|
18
|
+
"""Upload a file to storage."""
|
|
19
|
+
walker:pub upload_file {
|
|
20
|
+
has file: UploadFile;
|
|
21
|
+
has folder: str = "documents";
|
|
22
|
+
|
|
23
|
+
can process with `root entry {
|
|
24
|
+
# Generate unique filename
|
|
25
|
+
ext = self.file.filename.rsplit(".", 1)[-1] if "." in self.file.filename else "";
|
|
26
|
+
unique_name = f"{uuid4()}.{ext}" if ext else str(uuid4());
|
|
27
|
+
path = f"{self.folder}/{unique_name}";
|
|
28
|
+
|
|
29
|
+
# Upload file to storage
|
|
30
|
+
storage.upload(self.file.file, path);
|
|
31
|
+
|
|
32
|
+
# Get metadata
|
|
33
|
+
metadata = storage.get_metadata(path);
|
|
34
|
+
|
|
35
|
+
report {
|
|
36
|
+
"success": True,
|
|
37
|
+
"original_filename": self.file.filename,
|
|
38
|
+
"storage_path": path,
|
|
39
|
+
"size": metadata["size"],
|
|
40
|
+
"content_type": self.file.content_type
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
"""List files in storage."""
|
|
46
|
+
walker:pub list_files {
|
|
47
|
+
has folder: str = "";
|
|
48
|
+
has recursive: bool = False;
|
|
49
|
+
|
|
50
|
+
can process with `root entry {
|
|
51
|
+
files = [];
|
|
52
|
+
for path in storage.list_files(self.folder, self.recursive) {
|
|
53
|
+
metadata = storage.get_metadata(path);
|
|
54
|
+
files.append({
|
|
55
|
+
"path": path,
|
|
56
|
+
"size": metadata["size"],
|
|
57
|
+
"modified": str(metadata["modified"])
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
report {"files": files, "count": len(files)};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
"""Delete a file from storage."""
|
|
65
|
+
walker:pub delete_file {
|
|
66
|
+
has path: str;
|
|
67
|
+
|
|
68
|
+
can process with `root entry {
|
|
69
|
+
if not storage.exists(self.path) {
|
|
70
|
+
report {"success": False, "error": "File not found"};
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
deleted = storage.delete(self.path);
|
|
74
|
+
report {"success": deleted, "path": self.path};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
"""Download a file from storage."""
|
|
79
|
+
walker:pub download_file {
|
|
80
|
+
has path: str;
|
|
81
|
+
|
|
82
|
+
can process with `root entry {
|
|
83
|
+
if not storage.exists(self.path) {
|
|
84
|
+
report {"success": False, "error": "File not found"};
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
content = storage.download(self.path);
|
|
88
|
+
metadata = storage.get_metadata(self.path);
|
|
89
|
+
report {
|
|
90
|
+
"success": True,
|
|
91
|
+
"path": self.path,
|
|
92
|
+
"size": metadata["size"],
|
|
93
|
+
"content_length": len(content) if content else 0
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
"""Check if a file exists in storage."""
|
|
99
|
+
walker:pub file_exists {
|
|
100
|
+
has path: str;
|
|
101
|
+
|
|
102
|
+
can process with `root entry {
|
|
103
|
+
exists = storage.exists(self.path);
|
|
104
|
+
report {"path": self.path, "exists": exists};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
"""Copy a file within storage."""
|
|
109
|
+
walker:pub copy_file {
|
|
110
|
+
has source: str;
|
|
111
|
+
has destination: str;
|
|
112
|
+
|
|
113
|
+
can process with `root entry {
|
|
114
|
+
if not storage.exists(self.source) {
|
|
115
|
+
report {"success": False, "error": "Source file not found"};
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
result = storage.copy(self.source, self.destination);
|
|
119
|
+
report {"success": result, "source": self.source, "destination": self.destination};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
"""Move a file within storage."""
|
|
124
|
+
walker:pub move_file {
|
|
125
|
+
has source: str;
|
|
126
|
+
has destination: str;
|
|
127
|
+
|
|
128
|
+
can process with `root entry {
|
|
129
|
+
if not storage.exists(self.source) {
|
|
130
|
+
report {"success": False, "error": "Source file not found"};
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
result = storage.move(self.source, self.destination);
|
|
134
|
+
report {"success": result, "source": self.source, "destination": self.destination};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# ============================================================================
|
|
139
|
+
# Health Check
|
|
140
|
+
# ============================================================================
|
|
141
|
+
|
|
142
|
+
"""Simple health check walker."""
|
|
143
|
+
walker:pub health {
|
|
144
|
+
can check with `root entry {
|
|
145
|
+
report {"status": "ok", "features": ["storage", "file_upload"]};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -89,6 +89,35 @@ walker : pub PublicInfo {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
# Streaming Walker Test
|
|
93
|
+
walker : pub WalkerStream {
|
|
94
|
+
has count: int = 3;
|
|
95
|
+
|
|
96
|
+
can stream_reports with `root entry {
|
|
97
|
+
def stream -> str {
|
|
98
|
+
import time;
|
|
99
|
+
for i in range(self.count) {
|
|
100
|
+
time.sleep(0.5);
|
|
101
|
+
yield f"Report {i}";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
report stream();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
import from typing { Generator }
|
|
109
|
+
# Streaming function test
|
|
110
|
+
def : pub FunctionStream(count: int = 3) -> Generator {
|
|
111
|
+
import time;
|
|
112
|
+
def stream -> Generator {
|
|
113
|
+
for i in range(count) {
|
|
114
|
+
time.sleep(0.5);
|
|
115
|
+
yield f"Func {i}";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
report stream();
|
|
119
|
+
}
|
|
120
|
+
|
|
92
121
|
# Async Walker Test
|
|
93
122
|
import asyncio;
|
|
94
123
|
|
|
@@ -157,3 +186,63 @@ walker : pub PublicFileUpload {
|
|
|
157
186
|
};
|
|
158
187
|
}
|
|
159
188
|
}
|
|
189
|
+
|
|
190
|
+
# ============================================================================
|
|
191
|
+
# Webhook Walkers
|
|
192
|
+
# ============================================================================
|
|
193
|
+
"""Test 1: Webhook walker with multiple fields using @restspec(webhook=True).
|
|
194
|
+
This walker should be accessible ONLY via /webhook/PaymentReceived endpoint."""
|
|
195
|
+
@restspec(webhook=True)
|
|
196
|
+
walker PaymentReceived {
|
|
197
|
+
has payment_id: str,
|
|
198
|
+
order_id: str,
|
|
199
|
+
amount: float,
|
|
200
|
+
currency: str = 'USD';
|
|
201
|
+
|
|
202
|
+
can process with `root entry {
|
|
203
|
+
response = {
|
|
204
|
+
"status": "success",
|
|
205
|
+
"message": f"Payment {self.payment_id} processed successfully.",
|
|
206
|
+
"payment_id": self.payment_id,
|
|
207
|
+
"order_id": self.order_id,
|
|
208
|
+
"amount": self.amount,
|
|
209
|
+
"currency": self.currency
|
|
210
|
+
};
|
|
211
|
+
report response;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
"""Test 2: Normal walker without @restspec(webhook=True) (defaults to HTTP).
|
|
216
|
+
This walker should be accessible via /walker/NormalPayment endpoint, NOT /webhook/."""
|
|
217
|
+
walker NormalPayment {
|
|
218
|
+
has payment_id: str,
|
|
219
|
+
order_id: str,
|
|
220
|
+
amount: float,
|
|
221
|
+
currency: str = 'USD';
|
|
222
|
+
|
|
223
|
+
can process with `root entry {
|
|
224
|
+
response = {
|
|
225
|
+
"status": "success",
|
|
226
|
+
"message": f"Normal payment {self.payment_id} processed.",
|
|
227
|
+
"payment_id": self.payment_id,
|
|
228
|
+
"order_id": self.order_id,
|
|
229
|
+
"amount": self.amount,
|
|
230
|
+
"currency": self.currency,
|
|
231
|
+
"transport": "http"
|
|
232
|
+
};
|
|
233
|
+
report response;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
"""Test 3: Minimal webhook walker with ONLY @restspec(webhook=True).
|
|
238
|
+
This walker should be accessible ONLY via /webhook/MinimalWebhook endpoint."""
|
|
239
|
+
@restspec(webhook=True)
|
|
240
|
+
walker MinimalWebhook {
|
|
241
|
+
can process with `root entry {
|
|
242
|
+
report {
|
|
243
|
+
"status": "received",
|
|
244
|
+
"message": "Minimal webhook executed successfully",
|
|
245
|
+
"transport": "webhook"
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
|
|
2
|
+
"""Test API fixture for restspec decorator tests."""
|
|
3
|
+
|
|
4
|
+
import from jaclang.runtimelib.builtin { restspec }
|
|
5
|
+
import from http { HTTPMethod }
|
|
6
|
+
|
|
7
|
+
# ============================================================================
|
|
8
|
+
# RestSpec Decorator Tests
|
|
9
|
+
# ============================================================================
|
|
10
|
+
|
|
11
|
+
"""Walker with GET method via restspec decorator."""
|
|
12
|
+
@restspec(method=HTTPMethod.GET)
|
|
13
|
+
walker : pub GetWalker {
|
|
14
|
+
can get_data with `root entry {
|
|
15
|
+
report {"message": "GetWalker executed", "method": "GET"};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
"""Walker with custom path."""
|
|
20
|
+
@restspec(method=HTTPMethod.GET, path="/custom/walker")
|
|
21
|
+
walker : pub CustomPathWalker {
|
|
22
|
+
can get_data with `root entry {
|
|
23
|
+
report {"message": "CustomPathWalker executed", "path": "/custom/walker"};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
"""Function with GET method via restspec decorator."""
|
|
28
|
+
@restspec(method=HTTPMethod.GET)
|
|
29
|
+
def get_func() -> dict {
|
|
30
|
+
return {"message": "get_func executed", "method": "GET"};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
"""Function with custom path."""
|
|
34
|
+
@restspec(method=HTTPMethod.GET, path="/custom/func")
|
|
35
|
+
def custom_path_func() -> dict {
|
|
36
|
+
return {"message": "custom_path_func executed", "path": "/custom/func"};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
"""Walker with POST method via restspec decorator."""
|
|
40
|
+
@restspec(method=HTTPMethod.POST)
|
|
41
|
+
walker : pub PostWalker {
|
|
42
|
+
can post_data with `root entry {
|
|
43
|
+
report {"message": "PostWalker executed", "method": "POST"};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
"""Walker with default method (POST)."""
|
|
48
|
+
walker : pub DefaultWalker {
|
|
49
|
+
can post_data with `root entry {
|
|
50
|
+
report {"message": "DefaultWalker executed", "method": "DEFAULT"};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
"""Function with POST method via restspec decorator."""
|
|
55
|
+
@restspec(method=HTTPMethod.POST)
|
|
56
|
+
def post_func() -> dict {
|
|
57
|
+
return {"message": "post_func executed", "method": "POST"};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
"""Function with default method (POST)."""
|
|
61
|
+
def default_func() -> dict {
|
|
62
|
+
return {"message": "default_func executed", "method": "DEFAULT"};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
"""Walker with GET method and parameters."""
|
|
66
|
+
@restspec(method=HTTPMethod.GET)
|
|
67
|
+
walker : pub GetWalkerWithParams {
|
|
68
|
+
has name: str;
|
|
69
|
+
has age: int;
|
|
70
|
+
|
|
71
|
+
can get_data with `root entry {
|
|
72
|
+
report {
|
|
73
|
+
"message": "GetWalkerWithParams executed",
|
|
74
|
+
"name": self.name,
|
|
75
|
+
"age": self.age
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
"""Function with GET method and parameters."""
|
|
81
|
+
@restspec(method=HTTPMethod.GET)
|
|
82
|
+
def get_func_with_params(name: str, age: int) -> dict {
|
|
83
|
+
return {
|
|
84
|
+
"message": "get_func_with_params executed",
|
|
85
|
+
"name": name,
|
|
86
|
+
"age": age
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -97,10 +97,12 @@ def test_deploy_all_in_one():
|
|
|
97
97
|
)
|
|
98
98
|
|
|
99
99
|
# Create app config
|
|
100
|
+
# Use experimental=True to install from repo (PyPI packages may not be available)
|
|
100
101
|
app_config = AppConfig(
|
|
101
102
|
code_folder=todo_app_path,
|
|
102
103
|
file_name="main.jac",
|
|
103
104
|
build=False,
|
|
105
|
+
experimental=True,
|
|
104
106
|
)
|
|
105
107
|
|
|
106
108
|
# Deploy using new architecture
|
|
@@ -172,7 +174,6 @@ def test_deploy_all_in_one():
|
|
|
172
174
|
|
|
173
175
|
# Cleanup using new architecture
|
|
174
176
|
deployment_target.destroy(app_name)
|
|
175
|
-
time.sleep(60) # Wait for deletion to propagate
|
|
176
177
|
|
|
177
178
|
# Verify cleanup - resources should no longer exist
|
|
178
179
|
try:
|
jac_scale/tests/test_examples.py
CHANGED
|
@@ -19,6 +19,8 @@ JacClientExamples = (
|
|
|
19
19
|
/ "examples"
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
+
JacScaleFixtures = Path(__file__).parent / "fixtures"
|
|
23
|
+
|
|
22
24
|
|
|
23
25
|
def get_free_port() -> int:
|
|
24
26
|
"""Get a free port by binding to port 0 and releasing it."""
|
|
@@ -184,6 +186,7 @@ class JacScaleTestRunner:
|
|
|
184
186
|
"node_modules",
|
|
185
187
|
".jac",
|
|
186
188
|
"package-lock.json",
|
|
189
|
+
"uploads", # Storage API test uploads
|
|
187
190
|
]
|
|
188
191
|
for dir_name in dirs_to_clean:
|
|
189
192
|
dir_path = example_dir / dir_name
|
|
@@ -371,9 +374,7 @@ class JacScaleTestRunner:
|
|
|
371
374
|
return response.text
|
|
372
375
|
return f"Request failed after {max_retries} retries (all timeouts)"
|
|
373
376
|
|
|
374
|
-
def spawn_walker(
|
|
375
|
-
self, walker_name: str, **kwargs: dict[str, Any]
|
|
376
|
-
) -> dict[str, Any]:
|
|
377
|
+
def spawn_walker(self, walker_name: str, **kwargs: object) -> dict[str, Any]:
|
|
377
378
|
"""Spawn a walker with the given parameters.
|
|
378
379
|
|
|
379
380
|
Args:
|
|
@@ -387,9 +388,73 @@ class JacScaleTestRunner:
|
|
|
387
388
|
"POST", f"/walker/{walker_name}", data=kwargs, use_token=True
|
|
388
389
|
)
|
|
389
390
|
|
|
390
|
-
def
|
|
391
|
-
self,
|
|
391
|
+
def upload_file(
|
|
392
|
+
self,
|
|
393
|
+
walker_name: str,
|
|
394
|
+
file_field: str,
|
|
395
|
+
filename: str,
|
|
396
|
+
content: bytes,
|
|
397
|
+
content_type: str = "text/plain",
|
|
398
|
+
extra_data: dict[str, Any] | None = None,
|
|
399
|
+
timeout: int = 10,
|
|
392
400
|
) -> dict[str, Any]:
|
|
401
|
+
"""Upload a file to a walker endpoint using multipart/form-data.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
walker_name: Name of the walker to call
|
|
405
|
+
file_field: Name of the file field in the walker
|
|
406
|
+
filename: Name of the file being uploaded
|
|
407
|
+
content: File content as bytes
|
|
408
|
+
content_type: MIME type of the file
|
|
409
|
+
extra_data: Additional form data fields
|
|
410
|
+
timeout: Request timeout in seconds
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Walker execution response
|
|
414
|
+
"""
|
|
415
|
+
import io
|
|
416
|
+
|
|
417
|
+
url = f"{self.base_url}/walker/{walker_name}"
|
|
418
|
+
headers = {}
|
|
419
|
+
|
|
420
|
+
if self.token:
|
|
421
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
422
|
+
|
|
423
|
+
files = {file_field: (filename, io.BytesIO(content), content_type)}
|
|
424
|
+
data = extra_data or {}
|
|
425
|
+
|
|
426
|
+
response = requests.post(
|
|
427
|
+
url, headers=headers, files=files, data=data, timeout=timeout
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
json_response: Any = response.json()
|
|
431
|
+
|
|
432
|
+
# Handle jac-scale's tuple response format [status, body]
|
|
433
|
+
if isinstance(json_response, list) and len(json_response) == 2:
|
|
434
|
+
json_response = json_response[1]
|
|
435
|
+
|
|
436
|
+
# Handle TransportResponse envelope format
|
|
437
|
+
if (
|
|
438
|
+
isinstance(json_response, dict)
|
|
439
|
+
and "ok" in json_response
|
|
440
|
+
and "data" in json_response
|
|
441
|
+
):
|
|
442
|
+
if json_response.get("ok") and json_response.get("data") is not None:
|
|
443
|
+
return json_response["data"]
|
|
444
|
+
elif not json_response.get("ok") and json_response.get("error"):
|
|
445
|
+
error_info = json_response["error"]
|
|
446
|
+
result: dict[str, Any] = {
|
|
447
|
+
"error": error_info.get("message", "Unknown error")
|
|
448
|
+
}
|
|
449
|
+
if "code" in error_info:
|
|
450
|
+
result["error_code"] = error_info["code"]
|
|
451
|
+
if "details" in error_info:
|
|
452
|
+
result["error_details"] = error_info["details"]
|
|
453
|
+
return result
|
|
454
|
+
|
|
455
|
+
return json_response # type: ignore[return-value]
|
|
456
|
+
|
|
457
|
+
def call_function(self, function_name: str, **kwargs: object) -> dict[str, Any]:
|
|
393
458
|
"""Call a function with the given parameters.
|
|
394
459
|
|
|
395
460
|
Args:
|
|
@@ -482,3 +547,113 @@ class TestJacClientExamples:
|
|
|
482
547
|
) as runner:
|
|
483
548
|
assert "/static/client.js" in runner.request_raw("GET", "/cl/app")
|
|
484
549
|
assert "import styled from" in runner.request_raw("GET", "/styled.js")
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
class TestJacScaleFeatures:
|
|
553
|
+
"""Test jac-scale specific features using dedicated fixtures."""
|
|
554
|
+
|
|
555
|
+
def test_storage_api(self) -> None:
|
|
556
|
+
"""Test Storage API with file upload, list, download, copy, move, delete."""
|
|
557
|
+
example_file = JacScaleFixtures / "scale-feats" / "main.jac"
|
|
558
|
+
with JacScaleTestRunner(
|
|
559
|
+
example_file, session_name="storage_api_test", setup_npm=False
|
|
560
|
+
) as runner:
|
|
561
|
+
# Health check
|
|
562
|
+
health = runner.spawn_walker("health")
|
|
563
|
+
assert health.get("reports", [[]])[0].get("status") == "ok"
|
|
564
|
+
|
|
565
|
+
# Register user for auth
|
|
566
|
+
runner.create_user("storage_user", "testpass123")
|
|
567
|
+
|
|
568
|
+
# Test file upload
|
|
569
|
+
test_content = b"Test content for storage API"
|
|
570
|
+
upload_result = runner.upload_file(
|
|
571
|
+
walker_name="upload_file",
|
|
572
|
+
file_field="file",
|
|
573
|
+
filename="test_file.txt",
|
|
574
|
+
content=test_content,
|
|
575
|
+
content_type="text/plain",
|
|
576
|
+
extra_data={"folder": "test_docs"},
|
|
577
|
+
)
|
|
578
|
+
reports = upload_result.get("reports", [])
|
|
579
|
+
assert len(reports) > 0, "Upload should return reports"
|
|
580
|
+
file_info = reports[0]
|
|
581
|
+
assert file_info["success"] is True
|
|
582
|
+
assert file_info["original_filename"] == "test_file.txt"
|
|
583
|
+
assert "storage_path" in file_info
|
|
584
|
+
uploaded_path = file_info["storage_path"]
|
|
585
|
+
assert uploaded_path.startswith("test_docs/")
|
|
586
|
+
assert file_info["size"] == len(test_content)
|
|
587
|
+
|
|
588
|
+
# Test file exists
|
|
589
|
+
exists_result = runner.spawn_walker("file_exists", path=uploaded_path)
|
|
590
|
+
reports = exists_result.get("reports", [])
|
|
591
|
+
assert len(reports) > 0
|
|
592
|
+
assert reports[0]["exists"] is True
|
|
593
|
+
|
|
594
|
+
# Test list files
|
|
595
|
+
list_result = runner.spawn_walker("list_files", folder="", recursive=True)
|
|
596
|
+
reports = list_result.get("reports", [])
|
|
597
|
+
assert len(reports) > 0
|
|
598
|
+
list_info = reports[0]
|
|
599
|
+
assert "files" in list_info
|
|
600
|
+
assert list_info["count"] >= 1
|
|
601
|
+
file_paths = [f["path"] for f in list_info["files"]]
|
|
602
|
+
assert uploaded_path in file_paths
|
|
603
|
+
|
|
604
|
+
# Test download file
|
|
605
|
+
download_result = runner.spawn_walker("download_file", path=uploaded_path)
|
|
606
|
+
reports = download_result.get("reports", [])
|
|
607
|
+
assert len(reports) > 0
|
|
608
|
+
assert reports[0]["success"] is True
|
|
609
|
+
assert reports[0]["content_length"] == len(test_content)
|
|
610
|
+
|
|
611
|
+
# Test copy file
|
|
612
|
+
copy_dest = "test_docs/copied_file.txt"
|
|
613
|
+
copy_result = runner.spawn_walker(
|
|
614
|
+
"copy_file", source=uploaded_path, destination=copy_dest
|
|
615
|
+
)
|
|
616
|
+
reports = copy_result.get("reports", [])
|
|
617
|
+
assert len(reports) > 0
|
|
618
|
+
assert reports[0]["success"] is True
|
|
619
|
+
|
|
620
|
+
# Verify copy exists
|
|
621
|
+
exists_result = runner.spawn_walker("file_exists", path=copy_dest)
|
|
622
|
+
assert exists_result.get("reports", [[]])[0]["exists"] is True
|
|
623
|
+
|
|
624
|
+
# Test move file
|
|
625
|
+
move_dest = "test_docs/moved_file.txt"
|
|
626
|
+
move_result = runner.spawn_walker(
|
|
627
|
+
"move_file", source=copy_dest, destination=move_dest
|
|
628
|
+
)
|
|
629
|
+
reports = move_result.get("reports", [])
|
|
630
|
+
assert len(reports) > 0
|
|
631
|
+
assert reports[0]["success"] is True
|
|
632
|
+
|
|
633
|
+
# Verify move: source gone, dest exists
|
|
634
|
+
exists_result = runner.spawn_walker("file_exists", path=copy_dest)
|
|
635
|
+
assert exists_result.get("reports", [[]])[0]["exists"] is False
|
|
636
|
+
exists_result = runner.spawn_walker("file_exists", path=move_dest)
|
|
637
|
+
assert exists_result.get("reports", [[]])[0]["exists"] is True
|
|
638
|
+
|
|
639
|
+
# Test delete file
|
|
640
|
+
delete_result = runner.spawn_walker("delete_file", path=uploaded_path)
|
|
641
|
+
reports = delete_result.get("reports", [])
|
|
642
|
+
assert len(reports) > 0
|
|
643
|
+
assert reports[0]["success"] is True
|
|
644
|
+
|
|
645
|
+
# Verify deleted
|
|
646
|
+
exists_result = runner.spawn_walker("file_exists", path=uploaded_path)
|
|
647
|
+
assert exists_result.get("reports", [[]])[0]["exists"] is False
|
|
648
|
+
|
|
649
|
+
# Test delete nonexistent file
|
|
650
|
+
delete_result = runner.spawn_walker(
|
|
651
|
+
"delete_file", path="nonexistent/file.txt"
|
|
652
|
+
)
|
|
653
|
+
reports = delete_result.get("reports", [])
|
|
654
|
+
assert len(reports) > 0
|
|
655
|
+
assert reports[0]["success"] is False
|
|
656
|
+
assert reports[0]["error"] == "File not found"
|
|
657
|
+
|
|
658
|
+
# Cleanup: delete moved file
|
|
659
|
+
runner.spawn_walker("delete_file", path=move_dest)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Test hook registration for JacScale."""
|
|
2
|
+
|
|
3
|
+
from jac_scale.plugin import JacScalePlugin
|
|
4
|
+
from jac_scale.user_manager import JacScaleUserManager
|
|
5
|
+
from jaclang.pycore.runtime import plugin_manager as pm
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_get_user_manager_implementation():
|
|
9
|
+
"""Test that the plugin method returns the correct class instance."""
|
|
10
|
+
# It returns an Instance, as seen in plugin.jac implementation
|
|
11
|
+
user_manager = JacScalePlugin.get_user_manager(base_path="")
|
|
12
|
+
assert isinstance(user_manager, JacScaleUserManager)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_hook_registration():
|
|
16
|
+
"""Test that the hook is registered with Jac plugin manager."""
|
|
17
|
+
# Create plugin instance
|
|
18
|
+
plugin = JacScalePlugin()
|
|
19
|
+
|
|
20
|
+
# Register manually for the test
|
|
21
|
+
if not pm.is_registered(plugin):
|
|
22
|
+
pm.register(plugin)
|
|
23
|
+
|
|
24
|
+
# Check hook implementations
|
|
25
|
+
hook_impls = pm.hook.get_user_manager.get_hookimpls()
|
|
26
|
+
|
|
27
|
+
# Verify our plugin's method is in the implementations
|
|
28
|
+
found = False
|
|
29
|
+
for impl in hook_impls:
|
|
30
|
+
# Check if the function belongs to our plugin class or module
|
|
31
|
+
if (
|
|
32
|
+
impl.plugin_name == "scale"
|
|
33
|
+
or isinstance(impl.plugin, JacScalePlugin)
|
|
34
|
+
or impl.function.__qualname__ == "JacScalePlugin.get_user_manager"
|
|
35
|
+
):
|
|
36
|
+
found = True
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
assert found, "JacScalePlugin.get_user_manager not found in hook implementations"
|