Skip to content

Commit 6eddb99

Browse files
committed
fix: update check script
1 parent 4b260f9 commit 6eddb99

4 files changed

Lines changed: 280 additions & 9 deletions

File tree

.agents/skills/tinyengine-dsl-generator/scripts/check_css.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,25 @@
1919
import re
2020
import subprocess
2121
import sys
22-
from typing import Dict, List, Any
22+
from typing import Any, Dict, List
23+
24+
25+
def _extract_inner_dsl(dsl_data: Dict[str, Any]) -> Dict[str, Any]:
26+
"""
27+
解包外层包装结构,返回内层 DSL。
28+
29+
落盘的页面/区块文件是"外层包装 + 内层 DSL":
30+
- 页面 DSL 在 page_content 内
31+
- 区块 DSL 在 content 内
32+
仅当内层确实是含 componentName 的节点时才解包,避免误吞同名普通字段;
33+
否则原样返回。
34+
"""
35+
if isinstance(dsl_data, dict):
36+
for key in ('page_content', 'content'):
37+
inner = dsl_data.get(key)
38+
if isinstance(inner, dict) and 'componentName' in inner:
39+
return inner
40+
return dsl_data
2341

2442

2543
class CSSChecker:
@@ -32,8 +50,9 @@ def __init__(self, dsl_data: Dict[str, Any]):
3250

3351
def check(self) -> bool:
3452
"""检查CSS语法"""
35-
page_content = self.dsl.get('page_content', self.dsl)
36-
css_string = page_content.get('css', '')
53+
# 从外层包装(page_content/content)解包到内层 DSL 后再读取 css
54+
inner = _extract_inner_dsl(self.dsl)
55+
css_string = inner.get('css', '')
3756

3857
if not css_string:
3958
self.warnings.append("No CSS field found")

.agents/skills/tinyengine-dsl-generator/scripts/check_event_bindings.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,25 @@
88

99
import json
1010
import sys
11-
from typing import Dict, List, Any
11+
from typing import Any, Dict, List
12+
13+
14+
def _extract_inner_dsl(dsl_data: Dict[str, Any]) -> Dict[str, Any]:
15+
"""
16+
解包外层包装结构,返回内层 DSL。
17+
18+
落盘的页面/区块文件是"外层包装 + 内层 DSL":
19+
- 页面 DSL 在 page_content 内
20+
- 区块 DSL 在 content 内
21+
仅当内层确实是含 componentName 的节点时才解包,避免误吞同名普通字段;
22+
否则原样返回。
23+
"""
24+
if isinstance(dsl_data, dict):
25+
for key in ('page_content', 'content'):
26+
inner = dsl_data.get(key)
27+
if isinstance(inner, dict) and 'componentName' in inner:
28+
return inner
29+
return dsl_data
1230

1331

1432
class EventBindingChecker:
@@ -21,10 +39,10 @@ def __init__(self, dsl_data: Dict[str, Any]):
2139

2240
def check(self) -> bool:
2341
"""检查所有事件绑定"""
24-
# 从page_content或直接检查
25-
page_content = self.dsl.get('page_content', self.dsl)
42+
# 从外层包装(page_content/content)解包到内层 DSL 后再检查
43+
inner = _extract_inner_dsl(self.dsl)
2644

27-
self._check_node(page_content)
45+
self._check_node(inner)
2846
return len(self.errors) == 0
2947

3048
def _check_node(self, node: Any) -> None:
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#!/usr/bin/env python3
2+
"""
3+
TinyEngine DSL Validator 回归测试
4+
5+
锁定 validate_dsl.py 对"外层包装 + 内层 DSL"结构(page_content / content)
6+
的识别与递归校验行为,防止文档推荐的一键校验命令对真实页面/区块文件误报失败。
7+
8+
运行:
9+
python3 test_validate_dsl.py
10+
"""
11+
12+
import os
13+
import sys
14+
import unittest
15+
16+
# 让本脚本能直接 import 同目录下的 validate_dsl 模块
17+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
18+
from validate_dsl import TinyEngineValidator # noqa: E402
19+
20+
REPO_ROOT = os.path.abspath(
21+
os.path.join(os.path.dirname(__file__), '..', '..', '..', '..')
22+
)
23+
PAGES_DIR = os.path.join(REPO_ROOT, 'mockServer', 'data', 'pages')
24+
BLOCKS_DIR = os.path.join(REPO_ROOT, 'mockServer', 'data', 'blocks')
25+
26+
27+
def _page_wrapper():
28+
"""外层包装页面(page_content 内才是真正的页面 DSL)"""
29+
return {
30+
"name": "Login",
31+
"id": "5a3f8e1b9c4d2e7f",
32+
"app": 1,
33+
"route": "login",
34+
"page_content": {
35+
"componentName": "Page",
36+
"fileName": "Login",
37+
"props": {"className": "login-page"},
38+
"css": ".login-page { padding: 40px; }",
39+
"state": {},
40+
"methods": {},
41+
"lifeCycles": {},
42+
"children": [],
43+
"dataSource": {"list": []},
44+
},
45+
"isPage": True,
46+
"group": "staticPages",
47+
"occupier": None,
48+
"isHome": False,
49+
}
50+
51+
52+
def _block_wrapper():
53+
"""外层包装区块(content 内才是真正的区块 DSL)"""
54+
return {
55+
"id": "V85zd9sWEya25Kxh",
56+
"label": "PortalBlock",
57+
"framework": "Vue",
58+
"path": "portal",
59+
"content": {
60+
"componentName": "Block",
61+
"fileName": "PortalBlock",
62+
"props": {},
63+
"state": {},
64+
"methods": {},
65+
"children": [],
66+
"schema": {"properties": [], "events": {}, "slots": {}},
67+
"dataSource": {},
68+
},
69+
"occupier": None,
70+
"is_published": True,
71+
}
72+
73+
74+
def _raw_page_with_meta():
75+
"""原始(裸)页面 DSL,带 meta —— 协议要求的完整形态"""
76+
return {
77+
"componentName": "Page",
78+
"fileName": "HomePage",
79+
"meta": {
80+
"id": 1,
81+
"title": "首页",
82+
"router": "home",
83+
"creator": "admin",
84+
"isHome": True,
85+
"parentId": "0",
86+
"rootElement": "div",
87+
},
88+
"children": [],
89+
}
90+
91+
92+
class WrapperDetectionTests(unittest.TestCase):
93+
"""外层包装结构的识别与解包"""
94+
95+
def test_page_wrapper_detected_as_page_and_valid(self):
96+
v = TinyEngineValidator(_page_wrapper())
97+
self.assertEqual(v.schema_type, 'page')
98+
self.assertTrue(v.validate(), msg=str(v.errors))
99+
self.assertEqual(v.errors, [])
100+
101+
def test_block_wrapper_detected_as_block_and_valid(self):
102+
v = TinyEngineValidator(_block_wrapper())
103+
self.assertEqual(v.schema_type, 'block')
104+
self.assertTrue(v.validate(), msg=str(v.errors))
105+
self.assertEqual(v.errors, [])
106+
107+
def test_wrapper_does_not_require_meta(self):
108+
"""包装格式把 meta 上提到外层,内层 page_content 不应再强制要求 meta"""
109+
v = TinyEngineValidator(_page_wrapper())
110+
v.validate()
111+
self.assertFalse(any('meta' in e.lower() for e in v.errors),
112+
f"不应因缺少 meta 报错: {v.errors}")
113+
114+
def test_unwrap_tracks_wrapper_flag(self):
115+
wrapped, flag = TinyEngineValidator._unwrap(_page_wrapper())
116+
self.assertTrue(flag)
117+
self.assertEqual(wrapped['componentName'], 'Page')
118+
119+
def test_non_wrapper_returns_unchanged(self):
120+
raw = _raw_page_with_meta()
121+
inner, flag = TinyEngineValidator._unwrap(raw)
122+
self.assertFalse(flag)
123+
self.assertIs(inner, raw)
124+
125+
126+
class RawSchemaBackwardCompatTests(unittest.TestCase):
127+
"""原始(裸)DSL 的既有严格行为不应被破坏"""
128+
129+
def test_raw_page_with_meta_still_passes(self):
130+
v = TinyEngineValidator(_raw_page_with_meta())
131+
self.assertEqual(v.schema_type, 'page')
132+
self.assertTrue(v.validate(), msg=str(v.errors))
133+
134+
def test_raw_page_without_meta_still_errors(self):
135+
"""裸页面仍必须带 meta(只有包装格式才豁免)"""
136+
raw = {"componentName": "Page", "fileName": "HomePage", "children": []}
137+
v = TinyEngineValidator(raw)
138+
v.validate()
139+
self.assertTrue(any('meta' in e.lower() for e in v.errors),
140+
"裸页面缺少 meta 应报错")
141+
142+
def test_unknown_schema_still_fails(self):
143+
v = TinyEngineValidator({"foo": "bar"})
144+
self.assertEqual(v.schema_type, 'unknown')
145+
self.assertFalse(v.validate())
146+
147+
148+
class StringChildrenTests(unittest.TestCase):
149+
"""children 为字符串(文本子节点)是协议合法形态,不应误报"""
150+
151+
def test_string_children_not_flagged(self):
152+
dsl = {
153+
"componentName": "Page",
154+
"fileName": "TextPage",
155+
"meta": {
156+
"id": 1, "title": "t", "router": "r", "creator": "a",
157+
"isHome": False, "parentId": "0", "rootElement": "div",
158+
},
159+
"children": [
160+
# a / h1 的 children 是字符串文本 —— 协议允许
161+
{"componentName": "a", "id": "l1", "children": "链接文本"},
162+
{"componentName": "h1", "id": "h1", "children": "标题文本"},
163+
],
164+
}
165+
v = TinyEngineValidator(dsl)
166+
self.assertTrue(v.validate(), msg=str(v.errors))
167+
self.assertFalse(
168+
any('is not an object' in e for e in v.errors),
169+
f"字符串子节点不应被判为非对象: {v.errors}",
170+
)
171+
172+
173+
@unittest.skipUnless(
174+
os.path.isdir(PAGES_DIR),
175+
f"找不到真实页面目录: {PAGES_DIR}",
176+
)
177+
class RealFileTests(unittest.TestCase):
178+
"""对仓库内真实页面/区块文件跑端到端校验"""
179+
180+
def _validate_file(self, path):
181+
import json
182+
with open(path, 'r', encoding='utf-8') as f:
183+
data = json.load(f)
184+
v = TinyEngineValidator(data)
185+
return v
186+
187+
def test_real_login_page_passes(self):
188+
path = os.path.join(PAGES_DIR, 'Login.json')
189+
if not os.path.exists(path):
190+
self.skipTest("Login.json 不存在")
191+
v = self._validate_file(path)
192+
self.assertEqual(v.schema_type, 'page')
193+
self.assertTrue(v.validate(), msg=str(v.errors))
194+
195+
def test_real_block_passes(self):
196+
path = os.path.join(BLOCKS_DIR, 'PortalBlock.json')
197+
if not os.path.exists(path):
198+
self.skipTest("PortalBlock.json 不存在")
199+
v = self._validate_file(path)
200+
self.assertEqual(v.schema_type, 'block')
201+
self.assertTrue(v.validate(), msg=str(v.errors))
202+
203+
204+
if __name__ == '__main__':
205+
unittest.main(verbosity=2)

.agents/skills/tinyengine-dsl-generator/scripts/validate_dsl.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ def __init__(self, dsl_data: Dict[str, Any], schema_type: str = 'auto'):
2424
dsl_data: DSL数据
2525
schema_type: Schema类型 ('page', 'block', 'app', 'auto')
2626
"""
27-
self.dsl = dsl_data
27+
self.original = dsl_data
28+
# 落盘文件是"外层包装 + 内层 DSL",这里解包到内层 DSL 再做协议校验。
29+
self.dsl, self._from_wrapper = self._unwrap(dsl_data)
2830
self.schema_type = schema_type
2931
self.errors = []
3032
self.warnings = []
@@ -33,6 +35,24 @@ def __init__(self, dsl_data: Dict[str, Any], schema_type: str = 'auto'):
3335
if schema_type == 'auto':
3436
self.schema_type = self._detect_schema_type()
3537

38+
@staticmethod
39+
def _unwrap(dsl_data: Any):
40+
"""
41+
识别并解包外层包装结构。
42+
43+
TinyEngine 落盘的页面/区块文件是"外层包装 + 内层 DSL"结构:
44+
- 页面:真正的页面 DSL 在 `page_content` 内
45+
- 区块:真正的区块 DSL 在 `content` 内
46+
返回 (内层DSL, 是否来自包装)。若不是包装结构,原样返回 (原数据, False)。
47+
仅当内层确实是含 componentName 的节点时才解包,避免误吞同名普通字段。
48+
"""
49+
if isinstance(dsl_data, dict):
50+
for key in ('page_content', 'content'):
51+
inner = dsl_data.get(key)
52+
if isinstance(inner, dict) and 'componentName' in inner:
53+
return inner, True
54+
return dsl_data, False
55+
3656
def _detect_schema_type(self) -> str:
3757
"""自动检测schema类型"""
3858
if 'componentName' in self.dsl:
@@ -59,7 +79,7 @@ def validate(self) -> bool:
5979

6080
def _validate_page(self) -> bool:
6181
"""验证页面Schema"""
62-
required = ['componentName', 'fileName', 'meta']
82+
required = ['componentName', 'fileName']
6383
for field in required:
6484
if field not in self.dsl:
6585
self.errors.append(f"Missing required field: {field}")
@@ -68,8 +88,13 @@ def _validate_page(self) -> bool:
6888
self.errors.append(f"Page componentName must be 'Page', got: {self.dsl.get('componentName')}")
6989

7090
# 验证meta
91+
# 原始页面协议(IPageSchema)要求 meta;但外层包装格式把 meta 等元信息
92+
# 上提到包装层(name/route/isHome/parentId/group...),page_content 内不再含 meta。
93+
# 因此仅在校验"裸"页面 DSL 时强制要求 meta;包装格式由外层字段承载。
7194
if 'meta' in self.dsl:
7295
self._validate_meta(self.dsl['meta'])
96+
elif not self._from_wrapper:
97+
self.errors.append("Missing required field: meta")
7398

7499
# 验证children
75100
if 'children' in self.dsl and self.dsl['children']:
@@ -153,6 +178,10 @@ def _validate_block_schema(self, schema: Dict[str, Any]) -> None:
153178

154179
def _validate_children(self, children: List[Any]) -> None:
155180
"""验证子组件列表"""
181+
# children 可以是字符串(文本子节点),按协议(IComponentSchema[] | string)
182+
# 这是合法形态,无需逐项校验;递归调用遇到字符串时同样直接返回。
183+
if isinstance(children, str):
184+
return
156185
for i, child in enumerate(children):
157186
if not isinstance(child, dict):
158187
self.errors.append(f"Child at index {i} is not an object")

0 commit comments

Comments
 (0)