@kernel-sig/console 0.1.0
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.
- package/.env.example +50 -0
- package/LICENSE +127 -0
- package/Makefile +66 -0
- package/README.md +279 -0
- package/backend/.dockerignore +12 -0
- package/backend/Dockerfile +43 -0
- package/backend/alembic/env.py +75 -0
- package/backend/alembic/script.py.mako +25 -0
- package/backend/alembic/versions/0001_initial.py +88 -0
- package/backend/alembic/versions/350e2d9b6553_add_pr_comment_table.py +48 -0
- package/backend/alembic/versions/4f8342e727fe_sig_info_roster_fields_and_branch_.py +39 -0
- package/backend/alembic/versions/5a31ade90136_add_repository_credential_pull_request_.py +293 -0
- package/backend/alembic/versions/87a008142f17_add_classification_attention_and_ai_.py +436 -0
- package/backend/alembic/versions/a1c7d90e4b52_branch_belongs_to_a_repository.py +72 -0
- package/backend/alembic/versions/b41c9d7e5f28_extend_pr_kind_categories.py +43 -0
- package/backend/alembic/versions/c8e4f2a71b93_add_release_kind.py +40 -0
- package/backend/alembic/versions/d1275091dfdc_add_needs_detail_flag_to_pull_request.py +44 -0
- package/backend/alembic/versions/e5b27c9d3a41_downgrade_cve_without_ids.py +54 -0
- package/backend/alembic/versions/ee3159a4eff0_add_sig_meeting_member_and_release_.py +164 -0
- package/backend/alembic/versions/fcf3c186d63b_attention_rule_subscriptions.py +42 -0
- package/backend/alembic.ini +40 -0
- package/backend/app/__init__.py +0 -0
- package/backend/app/api/__init__.py +0 -0
- package/backend/app/api/deps.py +74 -0
- package/backend/app/api/v1/__init__.py +0 -0
- package/backend/app/api/v1/ai.py +386 -0
- package/backend/app/api/v1/analytics.py +48 -0
- package/backend/app/api/v1/attention.py +142 -0
- package/backend/app/api/v1/auth.py +106 -0
- package/backend/app/api/v1/classification.py +192 -0
- package/backend/app/api/v1/health.py +29 -0
- package/backend/app/api/v1/issues.py +143 -0
- package/backend/app/api/v1/pulls.py +388 -0
- package/backend/app/api/v1/repositories.py +273 -0
- package/backend/app/api/v1/router.py +32 -0
- package/backend/app/api/v1/sig.py +565 -0
- package/backend/app/api/v1/users.py +87 -0
- package/backend/app/api/v1/webhooks.py +135 -0
- package/backend/app/core/__init__.py +0 -0
- package/backend/app/core/config.py +72 -0
- package/backend/app/core/crypto.py +74 -0
- package/backend/app/core/db.py +79 -0
- package/backend/app/core/exceptions.py +60 -0
- package/backend/app/core/logging.py +69 -0
- package/backend/app/core/permissions.py +87 -0
- package/backend/app/core/queue.py +57 -0
- package/backend/app/core/security.py +69 -0
- package/backend/app/domain/__init__.py +0 -0
- package/backend/app/domain/attention.py +618 -0
- package/backend/app/domain/classification.py +689 -0
- package/backend/app/domain/meeting.py +408 -0
- package/backend/app/domain/release.py +163 -0
- package/backend/app/domain/review.py +416 -0
- package/backend/app/domain/sig.py +178 -0
- package/backend/app/domain/sig_info.py +201 -0
- package/backend/app/integrations/__init__.py +0 -0
- package/backend/app/integrations/atomgit/__init__.py +0 -0
- package/backend/app/integrations/atomgit/client.py +505 -0
- package/backend/app/integrations/atomgit/models.py +311 -0
- package/backend/app/integrations/llm/__init__.py +0 -0
- package/backend/app/integrations/llm/prompts.py +222 -0
- package/backend/app/integrations/llm/provider.py +326 -0
- package/backend/app/main.py +242 -0
- package/backend/app/middleware/__init__.py +0 -0
- package/backend/app/middleware/audit.py +129 -0
- package/backend/app/middleware/request_context.py +42 -0
- package/backend/app/models/__init__.py +112 -0
- package/backend/app/models/ai.py +214 -0
- package/backend/app/models/attention.py +131 -0
- package/backend/app/models/attention_settings.py +61 -0
- package/backend/app/models/audit.py +53 -0
- package/backend/app/models/base.py +35 -0
- package/backend/app/models/classification.py +144 -0
- package/backend/app/models/credential.py +56 -0
- package/backend/app/models/issue.py +132 -0
- package/backend/app/models/meeting.py +189 -0
- package/backend/app/models/pull_request.py +288 -0
- package/backend/app/models/repository.py +196 -0
- package/backend/app/models/sig.py +194 -0
- package/backend/app/models/user.py +44 -0
- package/backend/app/schemas/__init__.py +0 -0
- package/backend/app/schemas/ai.py +120 -0
- package/backend/app/schemas/attention.py +43 -0
- package/backend/app/schemas/auth.py +26 -0
- package/backend/app/schemas/classification.py +58 -0
- package/backend/app/schemas/common.py +43 -0
- package/backend/app/schemas/pull_request.py +214 -0
- package/backend/app/schemas/repository.py +94 -0
- package/backend/app/schemas/sig.py +246 -0
- package/backend/app/schemas/user.py +79 -0
- package/backend/app/services/__init__.py +0 -0
- package/backend/app/services/ai_service.py +569 -0
- package/backend/app/services/analytics_service.py +390 -0
- package/backend/app/services/attention_queue.py +451 -0
- package/backend/app/services/attention_service.py +432 -0
- package/backend/app/services/auth_service.py +74 -0
- package/backend/app/services/classification_service.py +658 -0
- package/backend/app/services/credential_service.py +105 -0
- package/backend/app/services/pull_query.py +273 -0
- package/backend/app/services/release_service.py +446 -0
- package/backend/app/services/repository_service.py +132 -0
- package/backend/app/services/sig_service.py +385 -0
- package/backend/app/services/sync_service.py +752 -0
- package/backend/app/services/user_service.py +68 -0
- package/backend/app/worker.py +389 -0
- package/backend/entrypoint.sh +10 -0
- package/backend/pyproject.toml +68 -0
- package/backend/tests/test_analysis_api.py +154 -0
- package/backend/tests/test_atomgit_client.py +360 -0
- package/backend/tests/test_atomgit_models.py +257 -0
- package/backend/tests/test_attention.py +291 -0
- package/backend/tests/test_audit_middleware.py +135 -0
- package/backend/tests/test_classification.py +498 -0
- package/backend/tests/test_config.py +41 -0
- package/backend/tests/test_crypto.py +68 -0
- package/backend/tests/test_exceptions.py +62 -0
- package/backend/tests/test_health.py +63 -0
- package/backend/tests/test_llm_provider.py +320 -0
- package/backend/tests/test_meeting_domain.py +169 -0
- package/backend/tests/test_permissions.py +69 -0
- package/backend/tests/test_pull_query_wiring.py +66 -0
- package/backend/tests/test_pull_schemas.py +82 -0
- package/backend/tests/test_release_domain.py +82 -0
- package/backend/tests/test_review_parser.py +301 -0
- package/backend/tests/test_schemas_user.py +97 -0
- package/backend/tests/test_security.py +92 -0
- package/cli/index.js +338 -0
- package/compose.yaml +105 -0
- package/frontend/.dockerignore +4 -0
- package/frontend/Dockerfile +27 -0
- package/frontend/index.html +14 -0
- package/frontend/nginx.conf +47 -0
- package/frontend/package-lock.json +5020 -0
- package/frontend/package.json +35 -0
- package/frontend/src/api/ai.ts +124 -0
- package/frontend/src/api/analytics.ts +66 -0
- package/frontend/src/api/attention.ts +181 -0
- package/frontend/src/api/auth.ts +74 -0
- package/frontend/src/api/classification.ts +170 -0
- package/frontend/src/api/issues.ts +90 -0
- package/frontend/src/api/pulls.ts +233 -0
- package/frontend/src/api/repositories.ts +95 -0
- package/frontend/src/api/sig.ts +232 -0
- package/frontend/src/app/antd-theme.ts +94 -0
- package/frontend/src/app/providers.tsx +59 -0
- package/frontend/src/app/router.tsx +411 -0
- package/frontend/src/app/search.ts +30 -0
- package/frontend/src/components/ClassificationBadge.tsx +58 -0
- package/frontend/src/components/GateBadge.tsx +13 -0
- package/frontend/src/components/SeverityBadge.tsx +20 -0
- package/frontend/src/components/layout/AppShell.tsx +16 -0
- package/frontend/src/components/layout/AuthLayout.tsx +32 -0
- package/frontend/src/components/layout/Sidebar.tsx +223 -0
- package/frontend/src/components/layout/TopBar.tsx +47 -0
- package/frontend/src/components/pulls/DiscussionTimeline.tsx +145 -0
- package/frontend/src/components/pulls/FacetRail.tsx +199 -0
- package/frontend/src/components/pulls/LabelChips.tsx +87 -0
- package/frontend/src/components/ui/alert.tsx +30 -0
- package/frontend/src/components/ui/badge.tsx +53 -0
- package/frontend/src/components/ui/button.tsx +51 -0
- package/frontend/src/components/ui/card.tsx +64 -0
- package/frontend/src/components/ui/chart-theme.ts +65 -0
- package/frontend/src/components/ui/data-table.tsx +39 -0
- package/frontend/src/components/ui/echart.tsx +70 -0
- package/frontend/src/components/ui/empty-state.tsx +22 -0
- package/frontend/src/components/ui/input.tsx +39 -0
- package/frontend/src/components/ui/lazy-chart.tsx +21 -0
- package/frontend/src/components/ui/skeleton.tsx +19 -0
- package/frontend/src/hooks/use-current-repository.ts +44 -0
- package/frontend/src/hooks/use-current-user.ts +38 -0
- package/frontend/src/lib/api-client.ts +93 -0
- package/frontend/src/lib/css-color.ts +60 -0
- package/frontend/src/lib/utils.ts +45 -0
- package/frontend/src/main.tsx +22 -0
- package/frontend/src/pages/attention/AttentionQueuePage.tsx +316 -0
- package/frontend/src/pages/attention/RuleSettingsPanel.tsx +177 -0
- package/frontend/src/pages/branches/BranchDetailPage.tsx +632 -0
- package/frontend/src/pages/branches/BranchListPage.tsx +308 -0
- package/frontend/src/pages/dashboard/DashboardPage.tsx +657 -0
- package/frontend/src/pages/issues/IssueListPage.tsx +284 -0
- package/frontend/src/pages/login/LoginPage.tsx +95 -0
- package/frontend/src/pages/meetings/MeetingDetailPage.tsx +403 -0
- package/frontend/src/pages/meetings/MeetingListPage.tsx +264 -0
- package/frontend/src/pages/members/MembersPage.tsx +534 -0
- package/frontend/src/pages/pulls/PullDetailPage.tsx +833 -0
- package/frontend/src/pages/pulls/PullListPage.tsx +399 -0
- package/frontend/src/pages/settings/AiSettingsPage.tsx +449 -0
- package/frontend/src/pages/settings/SettingsPage.tsx +321 -0
- package/frontend/src/pages/setup/SetupPage.tsx +144 -0
- package/frontend/src/styles/globals.css +147 -0
- package/frontend/tsconfig.json +22 -0
- package/frontend/vite.config.ts +57 -0
- package/package.json +48 -0
- package/scripts/e2e-auth-flow.py +131 -0
- package/scripts/e2e-pr-detail.py +128 -0
- package/scripts/e2e-verify.py +773 -0
- package/scripts/verify-ai-pipeline.py +616 -0
- package/scripts/verify-analysis-pipeline.py +303 -0
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kernel-sig/console",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "openEuler Kernel SIG 的 PR/Issue 管理平台:一条命令在本机起一整套实例",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ksc": "./cli/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"cli/",
|
|
10
|
+
"backend/app/**/*.py",
|
|
11
|
+
"backend/alembic/**/*.py",
|
|
12
|
+
"backend/alembic/*.py",
|
|
13
|
+
"backend/alembic/script.py.mako",
|
|
14
|
+
"backend/tests/**/*.py",
|
|
15
|
+
"backend/alembic.ini",
|
|
16
|
+
"backend/pyproject.toml",
|
|
17
|
+
"backend/Dockerfile",
|
|
18
|
+
"backend/.dockerignore",
|
|
19
|
+
"backend/entrypoint.sh",
|
|
20
|
+
"frontend/src/**/*.ts",
|
|
21
|
+
"frontend/src/**/*.tsx",
|
|
22
|
+
"frontend/src/**/*.css",
|
|
23
|
+
"frontend/index.html",
|
|
24
|
+
"frontend/nginx.conf",
|
|
25
|
+
"frontend/package.json",
|
|
26
|
+
"frontend/package-lock.json",
|
|
27
|
+
"frontend/tsconfig.json",
|
|
28
|
+
"frontend/vite.config.ts",
|
|
29
|
+
"frontend/Dockerfile",
|
|
30
|
+
"frontend/.dockerignore",
|
|
31
|
+
"compose.yaml",
|
|
32
|
+
"scripts/**/*.py",
|
|
33
|
+
"Makefile",
|
|
34
|
+
"README.md",
|
|
35
|
+
".env.example"
|
|
36
|
+
],
|
|
37
|
+
"keywords": [
|
|
38
|
+
"openeuler",
|
|
39
|
+
"kernel",
|
|
40
|
+
"sig",
|
|
41
|
+
"atomgit",
|
|
42
|
+
"pull-request"
|
|
43
|
+
],
|
|
44
|
+
"license": "MulanPSL-2.0",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Kernel SIG Console 登录流程的浏览器端验证。
|
|
2
|
+
|
|
3
|
+
覆盖:路由守卫、登录页渲染、凭据校验、会话保持、退出登录、权限提示。
|
|
4
|
+
每一步都断言实际观察到的状态,不做「看起来没问题」的判断。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from playwright.sync_api import Page, expect, sync_playwright
|
|
11
|
+
|
|
12
|
+
BASE = os.environ.get("KSC_BASE_URL", "http://localhost:18080")
|
|
13
|
+
USERNAME = os.environ.get("KSC_E2E_USERNAME", "admin")
|
|
14
|
+
|
|
15
|
+
# 口令只从环境变量取,不给默认值。这个脚本登的是**你自己的实例**,
|
|
16
|
+
# 在源码里写死任何一个口令,都等于随代码一起公开它。
|
|
17
|
+
PASSWORD = os.environ.get("KSC_E2E_PASSWORD")
|
|
18
|
+
if not PASSWORD:
|
|
19
|
+
sys.exit("请用 KSC_E2E_PASSWORD 环境变量提供实例口令")
|
|
20
|
+
|
|
21
|
+
results: list[tuple[str, bool, str]] = []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def check(name: str, condition: bool, detail: str = "") -> None:
|
|
25
|
+
results.append((name, condition, detail))
|
|
26
|
+
mark = "PASS" if condition else "FAIL"
|
|
27
|
+
print(f"[{mark}] {name}" + (f" — {detail}" if detail else ""))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run(page: Page) -> None:
|
|
31
|
+
# --- 1. 未登录访问根路径应落到登录页 ---
|
|
32
|
+
page.goto(BASE, wait_until="networkidle")
|
|
33
|
+
check("未登录访问 / 被路由守卫拦截", page.url.endswith("/login"), f"当前 URL: {page.url}")
|
|
34
|
+
|
|
35
|
+
# --- 2. 登录页渲染 ---
|
|
36
|
+
expect(page.get_by_role("heading", name="登录")).to_be_visible()
|
|
37
|
+
check("登录页标题渲染", True)
|
|
38
|
+
check("用户名输入框存在", page.get_by_label("用户名").is_visible())
|
|
39
|
+
check("密码输入框存在", page.get_by_label("密码").is_visible())
|
|
40
|
+
check("登录按钮存在", page.get_by_role("button", name="登录").is_visible())
|
|
41
|
+
|
|
42
|
+
# --- 3. 深色主题令牌生效 ---
|
|
43
|
+
bg = page.evaluate("getComputedStyle(document.body).backgroundColor")
|
|
44
|
+
check("主题背景已应用(非白底)", bg not in ("rgb(255, 255, 255)", "rgba(0, 0, 0, 0)"), bg)
|
|
45
|
+
|
|
46
|
+
# --- 4. 错误密码应显示后端返回的错误信息 ---
|
|
47
|
+
page.get_by_label("用户名").fill(USERNAME)
|
|
48
|
+
page.get_by_label("密码").fill("definitely-wrong-password")
|
|
49
|
+
page.get_by_role("button", name="登录").click()
|
|
50
|
+
error_alert = page.get_by_role("alert")
|
|
51
|
+
expect(error_alert).to_be_visible(timeout=8000)
|
|
52
|
+
text = error_alert.inner_text()
|
|
53
|
+
check("错误密码显示错误提示", "用户名或密码错误" in text, text.strip())
|
|
54
|
+
check("错误后仍停留在登录页", page.url.endswith("/login"))
|
|
55
|
+
|
|
56
|
+
# --- 5. 正确凭据登录 ---
|
|
57
|
+
page.get_by_label("密码").fill(PASSWORD)
|
|
58
|
+
page.get_by_role("button", name="登录").click()
|
|
59
|
+
page.wait_for_url("**/dashboard", timeout=12000)
|
|
60
|
+
check("登录后跳转到仪表盘", page.url.endswith("/dashboard"), page.url)
|
|
61
|
+
|
|
62
|
+
# --- 6. 仪表盘内容 ---
|
|
63
|
+
expect(page.get_by_role("heading", name="总览")).to_be_visible()
|
|
64
|
+
check("仪表盘标题渲染", True)
|
|
65
|
+
body = page.inner_text("body")
|
|
66
|
+
check("侧栏显示登录用户名", USERNAME in body or "管理员" in body)
|
|
67
|
+
|
|
68
|
+
# --- 7. 会话保持:刷新后不被踢出 ---
|
|
69
|
+
page.reload(wait_until="networkidle")
|
|
70
|
+
page.wait_for_timeout(1200)
|
|
71
|
+
check("刷新后会话保持", page.url.endswith("/dashboard"), f"当前 URL: {page.url}")
|
|
72
|
+
|
|
73
|
+
# --- 8. 会话 Cookie 为 HttpOnly ---
|
|
74
|
+
cookies = page.context.cookies()
|
|
75
|
+
access = next((c for c in cookies if c["name"] == "ksc_access"), None)
|
|
76
|
+
check("访问令牌 Cookie 存在", access is not None)
|
|
77
|
+
if access:
|
|
78
|
+
check("访问令牌 Cookie 为 HttpOnly", access.get("httpOnly", False))
|
|
79
|
+
|
|
80
|
+
# --- 9. 已初始化后 /setup 应被拒绝 ---
|
|
81
|
+
page.goto(f"{BASE}/setup", wait_until="networkidle")
|
|
82
|
+
page.wait_for_timeout(1000)
|
|
83
|
+
check("/setup 在已初始化后被拒", not page.url.endswith("/setup"), f"当前 URL: {page.url}")
|
|
84
|
+
|
|
85
|
+
# --- 10. 退出登录 ---
|
|
86
|
+
page.goto(f"{BASE}/dashboard", wait_until="networkidle")
|
|
87
|
+
page.wait_for_selector('button[aria-label="退出登录"]', timeout=8000)
|
|
88
|
+
page.click('button[aria-label="退出登录"]')
|
|
89
|
+
page.wait_for_url("**/login", timeout=10000)
|
|
90
|
+
check("退出后回到登录页", page.url.endswith("/login"), page.url)
|
|
91
|
+
|
|
92
|
+
# --- 11. 退出后受保护路由再次被拦截 ---
|
|
93
|
+
page.goto(f"{BASE}/dashboard", wait_until="networkidle")
|
|
94
|
+
page.wait_for_timeout(1200)
|
|
95
|
+
check("退出后 /dashboard 被拦截", page.url.endswith("/login"), page.url)
|
|
96
|
+
|
|
97
|
+
page.screenshot(path="/tmp/ksp-final.png", full_page=True)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main() -> int:
|
|
101
|
+
with sync_playwright() as p:
|
|
102
|
+
browser = p.chromium.launch(headless=True)
|
|
103
|
+
context = browser.new_context(viewport={"width": 1440, "height": 900})
|
|
104
|
+
page = context.new_page()
|
|
105
|
+
|
|
106
|
+
console_errors: list[str] = []
|
|
107
|
+
page.on(
|
|
108
|
+
"console",
|
|
109
|
+
lambda msg: console_errors.append(msg.text) if msg.type == "error" else None,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
run(page)
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
check("脚本执行完成", False, f"{type(exc).__name__}: {exc}")
|
|
116
|
+
page.screenshot(path="/tmp/ksp-failure.png", full_page=True)
|
|
117
|
+
finally:
|
|
118
|
+
browser.close()
|
|
119
|
+
|
|
120
|
+
print("\n" + "=" * 60)
|
|
121
|
+
failed = [r for r in results if not r[1]]
|
|
122
|
+
print(f"通过 {len(results) - len(failed)} / {len(results)}")
|
|
123
|
+
if console_errors:
|
|
124
|
+
print("\n浏览器控制台错误:")
|
|
125
|
+
for err in console_errors[:10]:
|
|
126
|
+
print(" -", err[:200])
|
|
127
|
+
return 1 if failed else 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
sys.exit(main())
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""PR 详情页的浏览器端验证。
|
|
2
|
+
|
|
3
|
+
覆盖本次新增的三块能力:上游标签按原色展示、讨论时间线、机器人评论折叠。
|
|
4
|
+
其中最关键的一条是**评论正文必须按纯文本渲染** —— 上游正文含
|
|
5
|
+
``<table>`` 与 ``<a href>``,若哪天有人改用 innerHTML,浏览器会把这些
|
|
6
|
+
标签解析成真实 DOM,这个脚本会立刻失败。
|
|
7
|
+
|
|
8
|
+
用法:
|
|
9
|
+
KSC_BASE_URL=http://host:port KSC_PULL_ID=<uuid> \
|
|
10
|
+
python3 scripts/e2e-pr-detail.py
|
|
11
|
+
|
|
12
|
+
直接按 id 进详情页,不走列表 —— 列表默认只列 open 的 30 条,
|
|
13
|
+
目标 PR 未必在第一页,靠翻页找样本会让脚本本身变脆。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
from playwright.sync_api import Page, sync_playwright
|
|
20
|
+
|
|
21
|
+
BASE = os.environ.get("KSC_BASE_URL", "http://localhost:18080")
|
|
22
|
+
USERNAME = os.environ.get("KSC_E2E_USERNAME", "admin")
|
|
23
|
+
|
|
24
|
+
# 口令只从环境变量取,不给默认值。这个脚本登的是**你自己的实例**,
|
|
25
|
+
# 在源码里写死任何一个口令,都等于随代码一起公开它。
|
|
26
|
+
PASSWORD = os.environ.get("KSC_E2E_PASSWORD")
|
|
27
|
+
if not PASSWORD:
|
|
28
|
+
sys.exit("请用 KSC_E2E_PASSWORD 环境变量提供实例口令")
|
|
29
|
+
|
|
30
|
+
PULL_ID = os.environ.get("KSC_PULL_ID", "")
|
|
31
|
+
|
|
32
|
+
results: list[tuple[str, bool, str]] = []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check(name: str, condition: bool, detail: str = "") -> None:
|
|
36
|
+
results.append((name, condition, detail))
|
|
37
|
+
print(f"[{'PASS' if condition else 'FAIL'}] {name}" + (f" — {detail}" if detail else ""))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def login(page: Page) -> None:
|
|
41
|
+
page.goto(f"{BASE}/login", wait_until="networkidle")
|
|
42
|
+
page.get_by_label("用户名").fill(USERNAME)
|
|
43
|
+
page.get_by_label("密码").fill(PASSWORD)
|
|
44
|
+
page.get_by_role("button", name="登录").click()
|
|
45
|
+
page.wait_for_url(lambda url: "/login" not in url, timeout=15000)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run(page: Page) -> None:
|
|
49
|
+
login(page)
|
|
50
|
+
|
|
51
|
+
if not PULL_ID:
|
|
52
|
+
check("提供了 KSC_PULL_ID", False, "未设置环境变量")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
# --- 进入目标 PR 详情 ---
|
|
56
|
+
page.goto(f"{BASE}/pulls/{PULL_ID}", wait_until="networkidle")
|
|
57
|
+
|
|
58
|
+
check("已进入 PR 详情页", "/pulls/" in page.url, page.url)
|
|
59
|
+
check("详情页渲染了标题", page.locator("h1").count() > 0)
|
|
60
|
+
|
|
61
|
+
# --- 标签:以原色渲染 ---
|
|
62
|
+
labels_heading = page.get_by_text("标签", exact=True)
|
|
63
|
+
check("侧栏「标签」面板存在", labels_heading.count() > 0)
|
|
64
|
+
|
|
65
|
+
chips = page.get_by_text("openeuler-cla/yes", exact=True)
|
|
66
|
+
check("标签 chip 渲染", chips.count() > 0, f"{chips.count()} 个")
|
|
67
|
+
if chips.count() > 0:
|
|
68
|
+
color = chips.first.evaluate("el => getComputedStyle(el).color")
|
|
69
|
+
# 上游色 #25e42b 亮度足够,应原样作为前景色(rgb 37,228,43)
|
|
70
|
+
check("标签沿用上游色而非中性色", color == "rgb(37, 228, 43)", color)
|
|
71
|
+
|
|
72
|
+
# --- 讨论标签页 ---
|
|
73
|
+
# 页签现在是 AntD 的 Tabs:role=tab,不是 button
|
|
74
|
+
discussion = page.get_by_role("tab", name="讨论")
|
|
75
|
+
check("「讨论」标签页存在", discussion.count() > 0)
|
|
76
|
+
check("「评审」旧标签页已移除", page.get_by_role("tab", name="评审", exact=True).count() == 0)
|
|
77
|
+
discussion.click()
|
|
78
|
+
page.wait_for_timeout(600)
|
|
79
|
+
|
|
80
|
+
details = page.locator("details")
|
|
81
|
+
check("机器人评论折叠为 <details>", details.count() > 0, f"{details.count()} 条")
|
|
82
|
+
|
|
83
|
+
if details.count() > 0:
|
|
84
|
+
check("折叠块默认收起", details.first.evaluate("el => !el.open"))
|
|
85
|
+
|
|
86
|
+
summaries = page.locator("details > summary")
|
|
87
|
+
check("折叠块标注机器人身份", summaries.first.inner_text().find("机器人") >= 0)
|
|
88
|
+
|
|
89
|
+
# --- 正文纯文本渲染(注入面检查)---
|
|
90
|
+
# 用 textContent 而非 innerText:折叠块内容不参与渲染,
|
|
91
|
+
# innerText 读不到,会被误判成"标签没渲染"
|
|
92
|
+
body_text = page.locator("ol").first.text_content() or ""
|
|
93
|
+
check("上游 HTML 标签以字面量呈现", "<table>" in body_text)
|
|
94
|
+
check("未被解析成真实表格元素", page.locator("ol table").count() == 0)
|
|
95
|
+
check("未被解析成真实链接元素(正文内)", page.locator("ol p a").count() == 0)
|
|
96
|
+
|
|
97
|
+
# --- 展开一条机器人评论,确认正文可见 ---
|
|
98
|
+
if details.count() > 0:
|
|
99
|
+
details.first.locator("summary").click()
|
|
100
|
+
page.wait_for_timeout(200)
|
|
101
|
+
check("展开后正文可读", details.first.evaluate("el => el.open"))
|
|
102
|
+
|
|
103
|
+
# --- 侧栏分类面板 ---
|
|
104
|
+
check("侧栏「分类」面板存在", page.get_by_text("分类", exact=True).count() > 0)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main() -> int:
|
|
108
|
+
with sync_playwright() as p:
|
|
109
|
+
browser = p.chromium.launch(headless=True)
|
|
110
|
+
page = browser.new_page(viewport={"width": 1440, "height": 900})
|
|
111
|
+
try:
|
|
112
|
+
run(page)
|
|
113
|
+
except Exception as exc: # noqa: BLE001 — 脚本要报出失败细节而非堆栈
|
|
114
|
+
check("执行过程未抛异常", False, f"{type(exc).__name__}: {exc}")
|
|
115
|
+
page.screenshot(path="/tmp/pr-detail-failure.png", full_page=True)
|
|
116
|
+
print("失败截图:/tmp/pr-detail-failure.png")
|
|
117
|
+
finally:
|
|
118
|
+
browser.close()
|
|
119
|
+
|
|
120
|
+
failed = [name for name, ok, _ in results if not ok]
|
|
121
|
+
print(f"\n{len(results) - len(failed)}/{len(results)} 通过")
|
|
122
|
+
if failed:
|
|
123
|
+
print("失败项:" + "、".join(failed))
|
|
124
|
+
return 1 if failed else 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
sys.exit(main())
|