",
+ }
+
+
+def validate_approval_record(
+ path: Path,
+ *,
+ version: str,
+ expected_current_commit: str = "",
+) -> list[ApprovalCheck]:
+ if not path.is_file():
+ return [ApprovalCheck("approval-record:file", False, f"missing file: {path}")]
+ text = path.read_text(encoding="utf-8")
+ checks: list[ApprovalCheck] = []
+
+ decisions = _decision_map(text)
+ for decision, expected in _expected_decisions(version).items():
+ actual = decisions.get(decision, "")
+ checks.append(
+ ApprovalCheck(
+ name=f"decision:{decision}",
+ ok=actual == expected,
+ detail=json.dumps({"actual": actual, "expected": expected}, ensure_ascii=False),
+ )
+ )
+
+ strategies = _strategy_map(text)
+ approved = [name for name in STRATEGIES if strategies.get(name, "").lower() == "yes"]
+ checks.append(
+ ApprovalCheck(
+ name="publication-strategy:single-approved",
+ ok=len(approved) == 1,
+ detail=json.dumps(
+ {"approved": approved, "expected": "exactly one reviewed publication strategy"},
+ ensure_ascii=False,
+ ),
+ )
+ )
+
+ python_source_ref = _source_ref(text, "ksadk-python")
+ web_source_ref = _source_ref(text, "ksadk-web")
+ source_refs = {
+ "ksadk-python": python_source_ref,
+ "ksadk-web": web_source_ref,
+ }
+ for source_name, source_ref in source_refs.items():
+ checks.append(
+ ApprovalCheck(
+ name=f"publication-strategy:{source_name}-source",
+ ok=_source_ref_is_filled(source_ref),
+ detail=json.dumps(
+ {
+ "actual": source_ref,
+ "expected": "approved source reference",
+ },
+ ensure_ascii=False,
+ ),
+ )
+ )
+ if expected_current_commit:
+ checks.append(
+ ApprovalCheck(
+ name=f"publication-strategy:{source_name}-current-commit",
+ ok=expected_current_commit in source_ref,
+ detail=json.dumps(
+ {
+ "actual": source_ref,
+ "expectedCommit": expected_current_commit,
+ },
+ ensure_ascii=False,
+ ),
+ )
+ )
+
+ signoffs = _signoff_rows(text)
+ for role in REQUIRED_SIGNOFF_ROLES:
+ cells = signoffs.get(role, [])
+ filled = len(cells) >= 4 and all(cell.strip() for cell in cells[1:4])
+ checks.append(
+ ApprovalCheck(
+ name=f"signoff:{role}",
+ ok=filled,
+ detail="name, decision, and date must be filled",
+ )
+ )
+
+ return checks
+
+
+def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--approval-record", type=Path, default=DEFAULT_APPROVAL_RECORD)
+ parser.add_argument("--version", default=_current_version())
+ parser.add_argument(
+ "--expected-current-commit",
+ default=None,
+ help="commit SHA that approved source references must include; defaults to git rev-parse HEAD",
+ )
+ parser.add_argument("--json", action="store_true", help="print JSON output")
+ return parser.parse_args(argv)
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+ args = parse_args(argv)
+ expected_current_commit = (
+ _current_commit() if args.expected_current_commit is None else args.expected_current_commit
+ )
+ checks = validate_approval_record(
+ args.approval_record,
+ version=args.version,
+ expected_current_commit=expected_current_commit,
+ )
+ ok = all(check.ok for check in checks)
+ payload = {
+ "ok": ok,
+ "approvalRecord": str(args.approval_record),
+ "checks": [asdict(check) for check in checks],
+ }
+ if args.json:
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
+ else:
+ print(f"approval record: {args.approval_record}")
+ for check in checks:
+ state = "ok" if check.ok else "fail"
+ print(f"{state}: {check.name} - {check.detail}")
+ return 0 if ok else 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/check_publication_state.py b/scripts/check_publication_state.py
index 48c34ad..fe21ba6 100644
--- a/scripts/check_publication_state.py
+++ b/scripts/check_publication_state.py
@@ -1,15 +1,19 @@
"""检查公开发布状态。
-发布前使用 `--phase pre-publish`,确保 GitHub Pages 可访问且 PyPI 上还没有
-当前版本;发布后使用 `--phase post-publish`,确保 PyPI 已能查询到当前版本。
+发布前使用 `--phase pre-publish`,确保 GitHub Pages 可访问、历史 GitHub
+Release 保留,且 PyPI 上还没有当前版本;发布后使用 `--phase post-publish`,
+确保 PyPI 已能查询到当前版本。主包和兼容别名包都按同一版本检查,
+避免只发布其中一个包。
"""
from __future__ import annotations
import argparse
import json
+import os
import sys
import urllib.error
+import urllib.parse
import urllib.request
from pathlib import Path
@@ -26,7 +30,12 @@ def _current_version() -> str:
def _open(url: str) -> tuple[int, bytes]:
- request = urllib.request.Request(url, headers={"User-Agent": "ksadk-publication-check"})
+ headers = {"User-Agent": "ksadk-publication-check"}
+ if urllib.parse.urlparse(url).hostname == "api.github.com":
+ token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
+ if token:
+ headers["Authorization"] = f"Bearer {token}"
+ request = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(request, timeout=20) as response:
return response.status, response.read()
@@ -65,6 +74,26 @@ def _pypi_version_exists(project: str, version: str) -> bool:
return True
+def _github_release_tags(url: str) -> set[str]:
+ status, body = _open(url)
+ if status != 200:
+ raise RuntimeError(f"github releases: 期望 HTTP 200,实际 {status}: {url}")
+ data = json.loads(body)
+ if not isinstance(data, list):
+ raise RuntimeError("github releases: 响应不是 release 列表")
+ return {str(item.get("tag_name") or "") for item in data if isinstance(item, dict)}
+
+
+def _expect_release_history(url: str, required_tags: list[str]) -> None:
+ if not required_tags:
+ return
+ tags = _github_release_tags(url)
+ missing = [tag for tag in required_tags if tag not in tags]
+ if missing:
+ raise RuntimeError(f"GitHub Release 历史缺失: {', '.join(missing)}")
+ print(f"github releases: required history present: {', '.join(required_tags)}")
+
+
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--phase", choices=("pre-publish", "post-publish"), required=True)
@@ -72,9 +101,20 @@ def main() -> int:
parser.add_argument("--project", default="ksadk")
parser.add_argument("--alias-project", default="agentengine-sdk-python")
parser.add_argument("--docs-url", default="https://kingsoftcloud.github.io/ksadk-python/")
+ parser.add_argument(
+ "--github-releases-url",
+ default="https://api.github.com/repos/kingsoftcloud/ksadk-python/releases?per_page=100",
+ )
+ parser.add_argument(
+ "--required-release-tags",
+ default="v0.6.1,v0.6.2,v0.6.3",
+ help="逗号分隔的历史 GitHub Release tag;传空字符串可跳过检查",
+ )
args = parser.parse_args()
_expect_http_ok("docs", args.docs_url)
+ required_tags = [tag.strip() for tag in args.required_release_tags.split(",") if tag.strip()]
+ _expect_release_history(args.github_releases_url, required_tags)
latest = _pypi_project_version(args.project)
print(f"pypi:{args.project}: latest={latest}")
@@ -85,10 +125,17 @@ def main() -> int:
alias_latest = _pypi_project_version(args.alias_project)
print(f"pypi:{args.alias_project}: latest={alias_latest}")
+ alias_exists = _pypi_version_exists(args.alias_project, args.version)
+ print(f"pypi:{args.alias_project}=={args.version}: exists={alias_exists}")
+
if args.phase == "pre-publish" and exists:
raise RuntimeError(f"发布前检查失败:PyPI 已存在 {args.project}=={args.version}")
+ if args.phase == "pre-publish" and alias_exists:
+ raise RuntimeError(f"发布前检查失败:PyPI 已存在 {args.alias_project}=={args.version}")
if args.phase == "post-publish" and not exists:
raise RuntimeError(f"发布后检查失败:PyPI 尚未存在 {args.project}=={args.version}")
+ if args.phase == "post-publish" and not alias_exists:
+ raise RuntimeError(f"发布后检查失败:PyPI 尚未存在 {args.alias_project}=={args.version}")
print(f"✅ publication {args.phase} check passed for {args.project}=={args.version}")
return 0
diff --git a/scripts/generate_public_assets.py b/scripts/generate_public_assets.py
new file mode 100644
index 0000000..8113c50
--- /dev/null
+++ b/scripts/generate_public_assets.py
@@ -0,0 +1,599 @@
+#!/usr/bin/env python3
+"""Generate public README and documentation visual assets."""
+
+from __future__ import annotations
+
+import io
+import os
+import select
+import shutil
+import socket
+import subprocess
+import sys
+import tempfile
+import threading
+import time
+from contextlib import contextmanager
+from pathlib import Path
+from types import SimpleNamespace
+
+from PIL import Image
+from rich.ansi import AnsiDecoder
+from rich.console import Console
+from rich.terminal_theme import TerminalTheme
+
+
+ROOT = Path(__file__).resolve().parents[1]
+ASSETS_DIR = ROOT / "public-docs" / "assets"
+ARCH_SVG = ASSETS_DIR / "ksadk-runtime-architecture.svg"
+ARCH_PNG = ASSETS_DIR / "ksadk-runtime-architecture.png"
+HERO_PNG = ASSETS_DIR / "ksadk-runtime-platform-hero.png"
+DEMO_GIF = ASSETS_DIR / "ksadk-local-debugging-demo.gif"
+WEB_UI_SCREENSHOT = ASSETS_DIR / "ksadk-web-ui-screenshot.png"
+
+CLI_SCREENSHOT_THEME = TerminalTheme(
+ background=(255, 255, 255),
+ foreground=(34, 34, 34),
+ normal=[
+ (34, 34, 34),
+ (220, 38, 38),
+ (0, 128, 96),
+ (128, 128, 0),
+ (0, 120, 140),
+ (160, 64, 160),
+ (0, 128, 160),
+ (120, 120, 120),
+ ],
+ bright=[
+ (0, 0, 0),
+ (255, 87, 51),
+ (0, 150, 110),
+ (255, 193, 7),
+ (0, 140, 170),
+ (180, 80, 180),
+ (0, 150, 180),
+ (80, 80, 80),
+ ],
+)
+
+
+def generate_architecture_svg() -> None:
+ ASSETS_DIR.mkdir(parents=True, exist_ok=True)
+ svg = """
+ KsADK Agent Runtime Platform 架构图
+ 从 Agent 代码到 KsADK SDK、统一运行时、Skill Runtime、Workspace、Sandbox、Memory Knowledge、AgentEngine、Serverless、Hermes 和 OpenClaw 的运行链路。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ KsADK Agent Runtime Platform
+ 一次构建 Agent,到处运行:统一开发、调试、运行、沙箱、部署和观测体验
+
+
+ AGENT CODE
+
+
+ Google ADK
+
+ LangGraph
+
+ LangChain
+
+ DeepAgents
+
+
+
+
+
+
+ KsADK SDK
+ Runner 适配 / 配置管理 / Toolsets / 项目打包
+
+
+
+
+
+
+ 统一运行时
+ CLI / Browser Web UI / OpenAI-Compatible API / Streaming Sessions
+ 本地开发时即验证部署后的运行边界
+
+
+
+
+
+
+
+
+
+
+ Skill Runtime
+ Skill Space / workflow
+
+
+ Workspace
+ 会话文件 / artifacts
+
+
+ Sandbox
+ 隔离命令 / 代码执行
+
+
+ Memory & Knowledge
+ 长期记忆 / 知识库
+
+
+
+
+
+
+
+
+
+ AgentEngine
+ 远端运行、服务入口与平台能力
+
+
+
+
+
+
+ Serverless / Hermes / OpenClaw Runtime
+
+
+"""
+ ARCH_SVG.write_text(svg, encoding="utf-8")
+
+
+def render_architecture_png() -> None:
+ if ARCH_PNG.exists() and os.environ.get("KSADK_REGENERATE_ARCHITECTURE_PNG") != "1":
+ return
+ converter = shutil.which("rsvg-convert")
+ if converter is None:
+ raise RuntimeError("rsvg-convert is required to render architecture PNG")
+ subprocess.run(
+ [converter, str(ARCH_SVG), "--width", "1600", "--output", str(ARCH_PNG)],
+ check=True,
+ )
+
+
+def _capture_cli_help_plain() -> str:
+ env = os.environ.copy()
+ env.pop("NO_COLOR", None)
+ env.pop("AGENTENGINE_NO_COLOR", None)
+ env["AGENTENGINE_OUTPUT_MODE"] = "pretty"
+ env["COLUMNS"] = "120"
+ completed = subprocess.run(
+ [sys.executable, "-m", "ksadk.cli", "-h"],
+ cwd=ROOT,
+ env=env,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ check=True,
+ )
+ return completed.stdout
+
+
+def _capture_cli_help_ansi() -> str:
+ env = os.environ.copy()
+ env.pop("NO_COLOR", None)
+ env.pop("AGENTENGINE_NO_COLOR", None)
+ env["AGENTENGINE_OUTPUT_MODE"] = "pretty"
+ env["TERM"] = "xterm-256color"
+ env["COLUMNS"] = "120"
+
+ try:
+ import pty
+
+ master_fd, slave_fd = pty.openpty()
+ except (ImportError, OSError):
+ return _capture_cli_help_plain()
+
+ try:
+ process = subprocess.Popen(
+ [sys.executable, "-m", "ksadk.cli", "-h"],
+ cwd=ROOT,
+ env=env,
+ stdin=subprocess.DEVNULL,
+ stdout=slave_fd,
+ stderr=slave_fd,
+ close_fds=True,
+ )
+ finally:
+ os.close(slave_fd)
+
+ chunks: list[bytes] = []
+ try:
+ while True:
+ readable, _, _ = select.select([master_fd], [], [], 0.1)
+ if master_fd in readable:
+ try:
+ data = os.read(master_fd, 4096)
+ except OSError:
+ break
+ if not data:
+ break
+ chunks.append(data)
+
+ if process.poll() is not None:
+ while True:
+ try:
+ data = os.read(master_fd, 4096)
+ except OSError:
+ break
+ if not data:
+ break
+ chunks.append(data)
+ break
+ finally:
+ os.close(master_fd)
+
+ return_code = process.wait()
+ if return_code != 0:
+ raise subprocess.CalledProcessError(return_code, [sys.executable, "-m", "ksadk.cli", "-h"])
+ return b"".join(chunks).decode("utf-8", "replace")
+
+
+def _strip_ansi(text: str) -> str:
+ import re
+
+ return re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", text)
+
+
+def _trim_cli_help_for_readme(output: str) -> str:
+ lines = output.replace("\r\n", "\n").splitlines()
+ selected: list[str] = []
+ for line in lines:
+ plain = _strip_ansi(line)
+ if "可用命令" in plain or "Available Commands" in plain:
+ break
+ selected.append(line)
+ return "\n".join(selected).rstrip() + "\n"
+
+
+def generate_hero_png() -> None:
+ ASSETS_DIR.mkdir(parents=True, exist_ok=True)
+ converter = shutil.which("rsvg-convert")
+ if converter is None:
+ raise RuntimeError("rsvg-convert is required to render CLI screenshot PNG")
+
+ output = _trim_cli_help_for_readme(_capture_cli_help_ansi())
+ ansi = "\x1b[1;30m$ agentengine -h\x1b[0m\n" + output
+ console = Console(
+ record=True,
+ width=118,
+ force_terminal=True,
+ color_system="truecolor",
+ file=io.StringIO(),
+ highlight=False,
+ )
+ decoder = AnsiDecoder()
+ for line in decoder.decode(ansi):
+ console.print(line, markup=False, highlight=False)
+
+ svg = console.export_svg(title="agentengine -h", theme=CLI_SCREENSHOT_THEME)
+ temp_path: Path | None = None
+ try:
+ with tempfile.NamedTemporaryFile(
+ "w",
+ encoding="utf-8",
+ suffix=".svg",
+ prefix="ksadk-cli-help-",
+ dir=ASSETS_DIR,
+ delete=False,
+ ) as temp_file:
+ temp_file.write(svg)
+ temp_path = Path(temp_file.name)
+ subprocess.run(
+ [converter, str(temp_path), "--width", "1600", "--output", str(HERO_PNG)],
+ check=True,
+ )
+ finally:
+ if temp_path is not None:
+ temp_path.unlink(missing_ok=True)
+
+
+def _find_chromium_executable() -> str | None:
+ explicit_path = os.environ.get("KSADK_ASSET_CHROMIUM")
+ if explicit_path and Path(explicit_path).is_file():
+ return explicit_path
+
+ candidates: list[Path] = []
+ cache_roots = [
+ Path.home() / "Library" / "Caches" / "ms-playwright",
+ Path.home() / ".cache" / "ms-playwright",
+ ]
+ for cache_root in cache_roots:
+ candidates.extend(
+ cache_root.glob(
+ "chromium-*/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
+ )
+ )
+ candidates.extend(
+ cache_root.glob(
+ "chromium-*/chrome-mac/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
+ )
+ )
+ candidates.extend(cache_root.glob("chromium-*/chrome-linux/chrome"))
+
+ for candidate in candidates:
+ if candidate.is_file():
+ return str(candidate)
+
+ for executable_name in (
+ "chromium",
+ "chromium-browser",
+ "google-chrome",
+ "google-chrome-stable",
+ "chrome",
+ ):
+ resolved = shutil.which(executable_name)
+ if resolved:
+ return resolved
+ return None
+
+
+class _PublicDemoRunner:
+ """用于公开资产生成的 deterministic runner,不连接外部模型或云环境。"""
+
+ def __init__(self):
+ from ksadk.runners.base_runner import BaseRunner
+
+ class Runner(BaseRunner):
+ def __init__(self):
+ super().__init__(
+ detection_result=SimpleNamespace(
+ name="runtime-platform-demo",
+ description="KsADK 真实 Web UI 演示",
+ type=SimpleNamespace(value="langgraph"),
+ ),
+ project_dir=str(ROOT),
+ )
+
+ def load_agent(self) -> None:
+ return None
+
+ async def invoke(self, input_data: dict) -> dict:
+ return {
+ "output": (
+ "KsADK 已完成本地调试检查:运行时、Workspace、Sandbox "
+ "与工具调用链路均可在 Web UI 中观察。"
+ )
+ }
+
+ async def stream(self, input_data: dict):
+ import asyncio
+
+ yield {
+ "type": "tool_call",
+ "tool_name": "workspace_status",
+ "tool_args": {"path": "/workspace", "include_artifacts": True},
+ "status": "running",
+ }
+ await asyncio.sleep(0.45)
+ yield {
+ "type": "tool_result",
+ "tool_name": "workspace_status",
+ "tool_output": '{"workspace":"ready","artifacts":3,"sandbox":"enabled"}',
+ }
+ await asyncio.sleep(0.45)
+ yield {
+ "type": "thinking",
+ "delta": "正在检查 Skill Runtime、Workspace、Sandbox 和长期记忆配置边界。",
+ }
+ await asyncio.sleep(0.45)
+ for delta in (
+ "KsADK 已接入 LangGraph Runner,",
+ "本地 Web UI 正在通过 Responses 流式协议返回结果。",
+ "\n\n- Workspace:可浏览会话文件和 artifacts",
+ "\n- Sandbox:支持隔离命令执行",
+ "\n- Skills:未配置 Skill Space 时会明确降级,不伪造工具结果",
+ ):
+ yield {"type": "text", "delta": delta}
+ await asyncio.sleep(0.35)
+ yield {
+ "type": "responses_output",
+ "response_id": "resp_public_demo",
+ "output": [
+ {
+ "id": "call_workspace_status",
+ "type": "function_call",
+ "name": "workspace_status",
+ "arguments": '{"path":"/workspace"}',
+ }
+ ],
+ }
+ yield {
+ "type": "final",
+ "output": (
+ "KsADK 已完成本地调试检查:运行时、Workspace、Sandbox "
+ "与工具调用链路均可在 Web UI 中观察。"
+ ),
+ }
+
+ self.runner = Runner()
+
+
+@contextmanager
+def _temporary_env(values: dict[str, str | None]):
+ original = {key: os.environ.get(key) for key in values}
+ try:
+ for key, value in values.items():
+ if value is None:
+ os.environ.pop(key, None)
+ else:
+ os.environ[key] = value
+ yield
+ finally:
+ for key, value in original.items():
+ if value is None:
+ os.environ.pop(key, None)
+ else:
+ os.environ[key] = value
+
+
+@contextmanager
+def _run_public_demo_server():
+ import importlib
+
+ import uvicorn
+ from ksadk.sessions.in_memory import InMemorySessionService
+
+ server_app_module = importlib.import_module("ksadk.server.app")
+ service = InMemorySessionService()
+ demo_runner = _PublicDemoRunner().runner
+ server_app_module.resolve_session_service = lambda: service
+ server_app_module.set_runner(demo_runner)
+
+ sock = socket.socket()
+ sock.bind(("127.0.0.1", 0))
+ host, port = sock.getsockname()
+ sock.close()
+
+ config = uvicorn.Config(server_app_module.app, host=host, port=port, log_level="warning")
+ server = uvicorn.Server(config)
+ thread = threading.Thread(target=server.run, daemon=True)
+ thread.start()
+
+ deadline = time.time() + 8
+ while not server.started and time.time() < deadline:
+ time.sleep(0.05)
+
+ if not server.started:
+ server.should_exit = True
+ thread.join(timeout=5)
+ raise RuntimeError("KsADK Web UI demo server failed to start")
+
+ try:
+ yield f"http://{host}:{port}"
+ finally:
+ server.should_exit = True
+ thread.join(timeout=5)
+
+
+def _save_web_ui_gif(frame_paths: list[Path]) -> None:
+ frames: list[Image.Image] = []
+ for frame_path in frame_paths:
+ image = Image.open(frame_path).convert("RGB")
+ target_width = 1100
+ target_height = round(image.height * target_width / image.width)
+ image = image.resize((target_width, target_height), Image.Resampling.LANCZOS)
+ frames.append(image)
+
+ durations = [900, 900, 1000, 1200, 1800]
+ frames[0].save(
+ DEMO_GIF,
+ save_all=True,
+ append_images=frames[1:],
+ duration=durations[: len(frames)],
+ loop=0,
+ optimize=True,
+ )
+
+
+def generate_web_ui_assets() -> None:
+ try:
+ from playwright.sync_api import sync_playwright
+ except ImportError as exc:
+ raise RuntimeError(
+ "playwright is required to generate real Web UI assets. "
+ "Install dev dependencies and run `python -m playwright install chromium`."
+ ) from exc
+
+ chromium = _find_chromium_executable()
+ if chromium is None:
+ raise RuntimeError(
+ "Chromium is required to generate real Web UI assets. "
+ "Set KSADK_ASSET_CHROMIUM or run `python -m playwright install chromium`."
+ )
+
+ ASSETS_DIR.mkdir(parents=True, exist_ok=True)
+ with tempfile.TemporaryDirectory(prefix="ksadk-public-web-ui-") as tmp_dir:
+ tmp_path = Path(tmp_dir)
+ frame_paths = [tmp_path / f"frame-{index}.png" for index in range(5)]
+ with _temporary_env(
+ {
+ "AGENTENGINE_UI_DIR": str(tmp_path / ".agentengine" / "ui"),
+ "OPENAI_MODEL_NAME": "gpt-4o-mini",
+ "OPENAI_API_KEY": None,
+ "OPENAI_BASE_URL": None,
+ "OPENAI_API_BASE": None,
+ }
+ ):
+ with _run_public_demo_server() as base_url:
+ with sync_playwright() as playwright:
+ browser = playwright.chromium.launch(executable_path=chromium, headless=True)
+ try:
+ page = browser.new_page(
+ viewport={"width": 1440, "height": 940},
+ device_scale_factor=1,
+ )
+ page.goto(f"{base_url}/chat")
+ page.wait_for_load_state("networkidle")
+ page.wait_for_selector("textarea")
+ page.screenshot(path=str(frame_paths[0]), full_page=False)
+ page.locator("textarea").fill(
+ "请检查这个 Agent 的工具、Workspace、Sandbox 和长期记忆配置"
+ )
+ page.screenshot(path=str(frame_paths[1]), full_page=False)
+ page.locator('button[type="submit"]').click()
+ page.wait_for_timeout(900)
+ page.screenshot(path=str(frame_paths[2]), full_page=False)
+ page.wait_for_timeout(1200)
+ page.screenshot(path=str(frame_paths[3]), full_page=False)
+ page.wait_for_function(
+ "() => document.body.innerText.includes('运行完成')",
+ timeout=10000,
+ )
+ page.screenshot(path=str(frame_paths[4]), full_page=False)
+ finally:
+ browser.close()
+
+ final_image = Image.open(frame_paths[-1]).convert("RGB")
+ final_image.save(WEB_UI_SCREENSHOT, optimize=True)
+ _save_web_ui_gif(frame_paths)
+
+
+def main() -> int:
+ generate_hero_png()
+ generate_architecture_svg()
+ render_architecture_png()
+ generate_web_ui_assets()
+ print(f"generated {HERO_PNG.relative_to(ROOT)}")
+ print(f"generated {ARCH_SVG.relative_to(ROOT)}")
+ print(f"generated {ARCH_PNG.relative_to(ROOT)}")
+ print(f"generated {WEB_UI_SCREENSHOT.relative_to(ROOT)}")
+ print(f"generated {DEMO_GIF.relative_to(ROOT)}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/open_source_audit.py b/scripts/open_source_audit.py
index d8a8375..ecb5008 100644
--- a/scripts/open_source_audit.py
+++ b/scripts/open_source_audit.py
@@ -97,6 +97,12 @@ def to_dict(self) -> dict[str, object]:
)
PUBLIC_REPO_RULES = COMMON_RULES + (
+ DenyRule(
+ name="non-curated-docs",
+ prefixes=("docs/",),
+ allowed_paths=("docs/maintainer-approval-record.md",),
+ description="internal planning and technical design docs stay out of the public repository; user docs live in public-docs/",
+ ),
DenyRule(
name="internal-deploy-material",
prefixes=(
@@ -124,11 +130,6 @@ def to_dict(self) -> dict[str, object]:
contains=("/.env.example",),
description="environment examples must be curated before publication to avoid private endpoints or credential names",
),
- DenyRule(
- name="non-curated-docs",
- prefixes=("docs/",),
- description="internal planning and technical design docs stay out of the public repository; user docs live in public-docs/",
- ),
)
WHEEL_RULES = (
@@ -210,7 +211,7 @@ def to_dict(self) -> dict[str, object]:
name="internal-service-endpoint",
pattern=re.compile(
r"(? dict[str, object]:
".md",
".py",
".sh",
+ ".svg",
".toml",
".ts",
".tsx",
diff --git a/scripts/prepare_ksadk_python_export.py b/scripts/prepare_ksadk_python_export.py
index 3e1f9aa..79cb0a0 100644
--- a/scripts/prepare_ksadk_python_export.py
+++ b/scripts/prepare_ksadk_python_export.py
@@ -27,7 +27,7 @@
DEFAULT_OUTPUT_DIR = Path("/tmp/ksadk-python-export-candidate")
-CURATED_DOCS: set[str] = set()
+CURATED_DOCS: set[str] = {"docs/maintainer-approval-record.md"}
ROOT_EXPORT_FILES = {
".dockerignore",
@@ -66,6 +66,9 @@
SCRIPT_EXPORT_FILES = {
"scripts/audit_release_artifacts.py",
+ "scripts/check_approval_record.py",
+ "scripts/check_publication_state.py",
+ "scripts/generate_public_assets.py",
"scripts/open_source_audit.py",
"scripts/prepare_ksadk_python_export.py",
"scripts/prepare_ksadk_web_export.py",
@@ -73,7 +76,12 @@
PUBLIC_TEST_FILES = {
"tests/conftest.py",
+ "tests/test_check_approval_record.py",
+ "tests/test_check_publication_state.py",
+ "tests/test_markdown_repair.py",
"tests/test_open_source_audit.py",
+ "tests/test_public_positioning_docs.py",
+ "tests/test_public_release_gates.py",
"tests/test_prepare_ksadk_python_export.py",
"tests/test_prepare_ksadk_web_export.py",
"tests/test_runtime_common_packaging.py",
diff --git a/tests/skills/test_service_client_http.py b/tests/skills/test_service_client_http.py
index 91273af..dadaae3 100644
--- a/tests/skills/test_service_client_http.py
+++ b/tests/skills/test_service_client_http.py
@@ -243,7 +243,7 @@ def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(404)
client = SkillServiceClient(
- base_url="http://maicp.inner.api.ksyun.com",
+ base_url="http://aicp.inner.api.ksyun.com",
account_id="2000003485",
transport=httpx.MockTransport(handler),
)
@@ -255,7 +255,7 @@ def handler(request: httpx.Request) -> httpx.Response:
method, url, headers = requests[0]
assert method == "GET"
assert url == (
- "http://maicp.inner.api.ksyun.com/"
+ "http://aicp.inner.api.ksyun.com/"
"?Action=ListSkillsBySpaceId&Version=2024-06-12"
"&SpaceId=ss-1&PageNumber=1&PageSize=100"
)
@@ -318,7 +318,7 @@ def handler(request: httpx.Request) -> httpx.Response:
client = SkillServiceClient(
base_url="http://aicp.inner.api.ksyun.com",
- account_id="73398439",
+ account_id="2000003485",
transport=httpx.MockTransport(handler),
)
@@ -334,7 +334,7 @@ def handler(request: httpx.Request) -> httpx.Response:
)
assert headers["x-ksc-region"] == "cn-beijing-6"
assert headers["x-ksc-custom-source"] == "pre"
- assert headers["x-ksc-account-id"] == "73398439"
+ assert headers["x-ksc-account-id"] == "2000003485"
def test_service_client_uses_registered_kop_action_for_available_premade_skills():
@@ -364,7 +364,7 @@ def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(404)
client = SkillServiceClient(
- base_url="http://maicp.inner.api.ksyun.com",
+ base_url="http://aicp.inner.api.ksyun.com",
account_id="2000003485",
transport=httpx.MockTransport(handler),
)
@@ -376,7 +376,7 @@ def handler(request: httpx.Request) -> httpx.Response:
method, url, headers = requests[0]
assert method == "GET"
assert url == (
- "http://maicp.inner.api.ksyun.com/"
+ "http://aicp.inner.api.ksyun.com/"
"?Action=ListAvailablePremadeSkills&Version=2024-06-12"
)
assert headers["x-action"] == "ListAvailablePremadeSkills"
diff --git a/tests/test_check_approval_record.py b/tests/test_check_approval_record.py
new file mode 100644
index 0000000..493895f
--- /dev/null
+++ b/tests/test_check_approval_record.py
@@ -0,0 +1,147 @@
+from __future__ import annotations
+
+import importlib.util
+import json
+import subprocess
+import sys
+from pathlib import Path
+
+
+REPO_ROOT = Path(__file__).resolve().parents[1]
+SCRIPT_PATH = REPO_ROOT / "scripts" / "check_approval_record.py"
+
+
+def _load_module():
+ spec = importlib.util.spec_from_file_location("check_approval_record", SCRIPT_PATH)
+ assert spec is not None
+ module = importlib.util.module_from_spec(spec)
+ assert spec.loader is not None
+ sys.modules[spec.name] = module
+ spec.loader.exec_module(module)
+ return module
+
+
+def _approved_record(python_source: str = "cd5fa22b1e78f03a8a9d025017e97ad414fdaa74") -> str:
+ return """# ksadk Public Release Approval Record
+
+## Required Approval Decisions
+
+| Decision | Approved value |
+| --- | --- |
+| License | Apache-2.0 |
+| Python repository | kingsoftcloud/ksadk-python |
+| Web UI repository | kingsoftcloud/ksadk-web |
+| Python package version | 0.6.4 |
+| Public docs URL | https://kingsoftcloud.github.io/ksadk-python/ |
+| Package metadata repository URL | https://github.com/kingsoftcloud/ksadk-python |
+| Package metadata documentation URL | https://kingsoftcloud.github.io/ksadk-python/ |
+| Security contact | security@kingsoft.com |
+
+## Publication Strategy
+
+| Strategy | Approved |
+| --- | --- |
+| Reviewed GitHub pull request | No |
+| Clean export from reviewed candidate | Yes |
+| Rewritten Git history after secret scan | No |
+
+The approved strategy must name the commit, tag, or export archive used for:
+
+- `ksadk-python`: {python_source}
+- `ksadk-web`: /tmp/ksadk-web-export-candidate
+
+## Approval Sign-Off
+
+| Role | Name | Decision | Date |
+| --- | --- | --- | --- |
+| Maintainer | Alice | Approved | 2026-05-28 |
+| Security reviewer | Bob | Approved | 2026-05-28 |
+| Release owner | Carol | Approved | 2026-05-28 |
+""".format(python_source=python_source)
+
+
+def test_template_approval_record_fails_until_strategy_and_signoffs_are_filled():
+ module = _load_module()
+
+ checks = module.validate_approval_record(
+ REPO_ROOT / "docs" / "maintainer-approval-record.md",
+ version="0.6.4",
+ expected_current_commit="current-reviewed-commit",
+ )
+
+ failed = {check.name for check in checks if not check.ok}
+ assert "publication-strategy:single-approved" in failed
+ assert "publication-strategy:ksadk-python-source" in failed
+ assert "publication-strategy:ksadk-web-source" in failed
+ assert "publication-strategy:ksadk-python-current-commit" in failed
+ assert "publication-strategy:ksadk-web-current-commit" in failed
+ assert "signoff:Maintainer" in failed
+ assert "signoff:Security reviewer" in failed
+ assert "signoff:Release owner" in failed
+ assert all(check.ok for check in checks if check.name.startswith("decision:"))
+
+
+def test_filled_approval_record_passes(tmp_path):
+ module = _load_module()
+ record = tmp_path / "approval.md"
+ record.write_text(_approved_record(), encoding="utf-8")
+
+ checks = module.validate_approval_record(record, version="0.6.4", expected_current_commit="")
+
+ assert all(check.ok for check in checks)
+
+
+def test_filled_record_fails_when_source_references_do_not_match_current_commit(tmp_path):
+ module = _load_module()
+ record = tmp_path / "approval.md"
+ record.write_text(_approved_record("old-reviewed-source"), encoding="utf-8")
+
+ checks = module.validate_approval_record(
+ record,
+ version="0.6.4",
+ expected_current_commit="new-reviewed-commit",
+ )
+
+ failed = {check.name for check in checks if not check.ok}
+ assert "publication-strategy:ksadk-python-current-commit" in failed
+ assert "publication-strategy:ksadk-web-current-commit" in failed
+
+
+def test_filled_record_passes_when_source_references_include_current_commit(tmp_path):
+ module = _load_module()
+ record = tmp_path / "approval.md"
+ record.write_text(
+ _approved_record("reviewed export from new-reviewed-commit")
+ .replace(
+ "- `ksadk-web`: /tmp/ksadk-web-export-candidate",
+ "- `ksadk-web`: /tmp/ksadk-web-export-candidate at new-reviewed-commit",
+ ),
+ encoding="utf-8",
+ )
+
+ checks = module.validate_approval_record(
+ record,
+ version="0.6.4",
+ expected_current_commit="new-reviewed-commit",
+ )
+
+ assert all(check.ok for check in checks)
+
+
+def test_cli_json_reports_failed_template_record():
+ result = subprocess.run(
+ [sys.executable, str(SCRIPT_PATH), "--json"],
+ cwd=REPO_ROOT,
+ check=False,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+
+ assert result.returncode == 1
+ payload = json.loads(result.stdout)
+ assert payload["ok"] is False
+ assert any(
+ check["name"] == "publication-strategy:single-approved" and not check["ok"]
+ for check in payload["checks"]
+ )
diff --git a/tests/test_check_publication_state.py b/tests/test_check_publication_state.py
new file mode 100644
index 0000000..36cf97d
--- /dev/null
+++ b/tests/test_check_publication_state.py
@@ -0,0 +1,165 @@
+from __future__ import annotations
+
+import importlib.util
+import sys
+from pathlib import Path
+
+import pytest
+
+
+SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "check_publication_state.py"
+
+
+def _load_module():
+ spec = importlib.util.spec_from_file_location("check_publication_state", SCRIPT_PATH)
+ assert spec is not None
+ module = importlib.util.module_from_spec(spec)
+ assert spec.loader is not None
+ sys.modules[spec.name] = module
+ spec.loader.exec_module(module)
+ return module
+
+
+def _run_main(monkeypatch, module, *, phase: str, version_exists: dict[tuple[str, str], bool]):
+ monkeypatch.setattr(
+ sys,
+ "argv",
+ [
+ "check_publication_state.py",
+ "--phase",
+ phase,
+ "--version",
+ "0.6.4",
+ ],
+ )
+ monkeypatch.setattr(module, "_expect_http_ok", lambda name, url: None)
+ monkeypatch.setattr(
+ module,
+ "_github_release_tags",
+ lambda url: {"v0.6.1", "v0.6.2", "v0.6.3"},
+ )
+ monkeypatch.setattr(
+ module,
+ "_pypi_project_version",
+ lambda project: {"ksadk": "0.6.3", "agentengine-sdk-python": "0.6.2"}[project],
+ )
+ monkeypatch.setattr(
+ module,
+ "_pypi_version_exists",
+ lambda project, version: version_exists.get((project, version), False),
+ )
+
+ return module.main()
+
+
+def test_pre_publish_fails_when_alias_package_version_already_exists(monkeypatch):
+ module = _load_module()
+
+ with pytest.raises(RuntimeError, match="agentengine-sdk-python==0.6.4"):
+ _run_main(
+ monkeypatch,
+ module,
+ phase="pre-publish",
+ version_exists={
+ ("ksadk", "0.6.4"): False,
+ ("agentengine-sdk-python", "0.6.4"): True,
+ },
+ )
+
+
+def test_post_publish_fails_when_alias_package_version_is_missing(monkeypatch):
+ module = _load_module()
+
+ with pytest.raises(RuntimeError, match="agentengine-sdk-python==0.6.4"):
+ _run_main(
+ monkeypatch,
+ module,
+ phase="post-publish",
+ version_exists={
+ ("ksadk", "0.6.4"): True,
+ ("agentengine-sdk-python", "0.6.4"): False,
+ },
+ )
+
+
+def test_publication_state_fails_when_historical_github_release_is_missing(monkeypatch):
+ module = _load_module()
+
+ monkeypatch.setattr(
+ sys,
+ "argv",
+ [
+ "check_publication_state.py",
+ "--phase",
+ "pre-publish",
+ "--version",
+ "0.6.4",
+ ],
+ )
+ monkeypatch.setattr(module, "_expect_http_ok", lambda name, url: None)
+ monkeypatch.setattr(module, "_github_release_tags", lambda url: {"v0.6.1", "v0.6.3"})
+
+ with pytest.raises(RuntimeError, match="v0.6.2"):
+ module.main()
+
+
+def test_github_api_request_uses_available_token(monkeypatch):
+ module = _load_module()
+ captured = {}
+
+ class FakeResponse:
+ status = 200
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *_exc):
+ return None
+
+ def read(self):
+ return b"[]"
+
+ def fake_urlopen(request, timeout):
+ captured["headers"] = dict(request.header_items())
+ captured["timeout"] = timeout
+ return FakeResponse()
+
+ monkeypatch.setenv("GH_TOKEN", "gh-test-token")
+ monkeypatch.setattr(module.urllib.request, "urlopen", fake_urlopen)
+
+ status, body = module._open("https://api.github.com/repos/kingsoftcloud/ksadk-python/releases")
+
+ assert status == 200
+ assert body == b"[]"
+ assert captured["headers"]["Authorization"] == "Bearer gh-test-token"
+ assert captured["timeout"] == 20
+
+
+def test_github_token_is_not_sent_to_url_containing_github_api_as_query(monkeypatch):
+ module = _load_module()
+ captured = {}
+
+ class FakeResponse:
+ status = 200
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *_exc):
+ return None
+
+ def read(self):
+ return b"ok"
+
+ def fake_urlopen(request, timeout):
+ captured["headers"] = dict(request.header_items())
+ return FakeResponse()
+
+ monkeypatch.setenv("GH_TOKEN", "gh-test-token")
+ monkeypatch.setattr(module.urllib.request, "urlopen", fake_urlopen)
+
+ status, body = module._open("https://example.com/status?next=api.github.com")
+
+ assert status == 200
+ assert body == b"ok"
+ assert "Authorization" not in captured["headers"]
diff --git a/tests/test_conversation_runtime.py b/tests/test_conversation_runtime.py
index 1cc32f7..85321a9 100644
--- a/tests/test_conversation_runtime.py
+++ b/tests/test_conversation_runtime.py
@@ -28,7 +28,14 @@
stream_conversation_turn,
stream_responses_conversation_turn,
)
-from ksadk.runtime_context import get_current_invocation_context
+from ksadk.runtime_context import (
+ PlatformInvocationContext,
+ get_current_invocation_context_or_default,
+ get_current_account_id,
+ get_current_invocation_context,
+ get_current_user_id,
+ platform_invocation_scope,
+)
from ksadk.sessions.base import SessionEvent
from ksadk.sessions.in_memory import InMemorySessionService
from ksadk.tracing.exporters.inmemory_exporter import InMemoryExporter
@@ -201,6 +208,43 @@ def _extract_sse_payload(chunks: list[str], event_name: str) -> dict:
raise AssertionError(f"SSE event {event_name!r} not found")
+def test_runtime_context_helpers_return_defaults_outside_invocation_scope():
+ assert get_current_invocation_context() is None
+ context = get_current_invocation_context_or_default()
+ assert context.user_id == ""
+ assert context.account_id == ""
+ assert context.session_id == ""
+ assert context.history == []
+ assert context.attachments == []
+ assert get_current_user_id() == ""
+ assert get_current_account_id() == ""
+ assert get_current_user_id(default="anonymous") == "anonymous"
+ assert get_current_account_id(default="tenantless") == "tenantless"
+
+
+def test_runtime_context_helpers_read_current_invocation_scope():
+ context = PlatformInvocationContext(
+ agent_id="demo-agent",
+ user_id="user-1",
+ account_id="acct-1",
+ session_id="sess-1",
+ history=[],
+ input_content=[],
+ input_messages=[],
+ input_parts=[],
+ attachments=[],
+ attachment_results=[],
+ current_attachments=[],
+ current_attachment_results=[],
+ has_current_files=False,
+ runner_type="mock",
+ )
+
+ with platform_invocation_scope(context):
+ assert get_current_user_id() == "user-1"
+ assert get_current_account_id() == "acct-1"
+
+
@pytest.fixture
def in_memory_trace_exporter():
provider = TracerProvider()
@@ -1488,6 +1532,7 @@ async def test_invoke_conversation_once_binds_platform_invocation_context_and_am
session_id=None,
messages=[{"role": "user", "content": "继续"}],
model="gpt-4o",
+ account_id="acct-1",
prepare_runner=lambda current_runner, model: current_runner.prepare_for_request(model),
session_service_provider=lambda: service,
)
@@ -1498,10 +1543,12 @@ async def test_invoke_conversation_once_binds_platform_invocation_context_and_am
assert runner.calls[-1]["memory_context"] == {"formatted_text": "Memory facts"}
assert runner.calls[-1]["platform_context"]["agent_id"] == "demo-agent"
assert runner.calls[-1]["platform_context"]["user_id"] == "user-1"
+ assert runner.calls[-1]["platform_context"]["account_id"] == "acct-1"
assert runner.calls[-1]["platform_context"]["session_id"] == session_id
assert runner.captured_runtime_context is not None
assert runner.captured_runtime_context.agent_id == "demo-agent"
assert runner.captured_runtime_context.user_id == "user-1"
+ assert runner.captured_runtime_context.account_id == "acct-1"
assert runner.captured_runtime_context.session_id == session_id
assert runner.captured_runtime_context.kb_context == {"formatted_text": "KB facts"}
assert runner.captured_runtime_context.memory_context == {"formatted_text": "Memory facts"}
diff --git a/tests/test_markdown_repair.py b/tests/test_markdown_repair.py
new file mode 100644
index 0000000..1fcd007
--- /dev/null
+++ b/tests/test_markdown_repair.py
@@ -0,0 +1,84 @@
+from __future__ import annotations
+
+from ksadk.markdown import repair_markdown
+
+
+def test_repair_markdown_closes_unclosed_fenced_code_block():
+ raw = "下面是示例:\n```python\nprint('hello')"
+
+ repaired = repair_markdown(raw, enabled=True)
+
+ assert repaired == "下面是示例:\n\n```python\nprint('hello')\n```\n"
+
+
+def test_repair_markdown_closes_long_fenced_code_block_with_matching_marker():
+ raw = "下面是示例:\n````python\nprint('``` still code')"
+
+ repaired = repair_markdown(raw, enabled=True)
+
+ assert repaired == "下面是示例:\n\n````python\nprint('``` still code')\n````\n"
+
+
+def test_repair_markdown_preserves_already_closed_fenced_code_block():
+ raw = "说明\n\n```python\nprint('hello')\n```\n"
+
+ repaired = repair_markdown(raw, enabled=True)
+
+ assert repaired == raw
+
+
+def test_repair_markdown_normalizes_blank_lines_around_tables_and_lists():
+ raw = "结果如下:\n| 名称 | 值 |\n| --- | --- |\n| A | 1 |\n结论:\n- 第一项\n- 第二项\n下一段"
+
+ repaired = repair_markdown(raw, enabled=True)
+
+ assert repaired == (
+ "结果如下:\n\n"
+ "| 名称 | 值 |\n"
+ "| --- | --- |\n"
+ "| A | 1 |\n\n"
+ "结论:\n\n"
+ "- 第一项\n"
+ "- 第二项\n\n"
+ "下一段\n"
+ )
+
+
+def test_repair_markdown_is_disabled_by_default():
+ raw = "结果如下:\n| 名称 | 值 |\n| --- | --- |\n| A | 1 |\n结论:\n- 第一项\n下一段"
+
+ repaired = repair_markdown(raw)
+
+ assert repaired == raw
+
+
+def test_repair_markdown_can_be_enabled_with_one_switch():
+ raw = "结果如下:\n| 名称 | 值 |\n| --- | --- |\n| A | 1 |\n结论:\n- 第一项\n下一段"
+
+ repaired = repair_markdown(raw, enabled=True)
+
+ assert repaired == (
+ "结果如下:\n\n"
+ "| 名称 | 值 |\n"
+ "| --- | --- |\n"
+ "| A | 1 |\n\n"
+ "结论:\n\n"
+ "- 第一项\n\n"
+ "下一段\n"
+ )
+
+
+def test_repair_markdown_is_idempotent():
+ raw = "标题\n```json\n{\"ok\": true}"
+
+ once = repair_markdown(raw, enabled=True)
+ twice = repair_markdown(once, enabled=True)
+
+ assert once == twice
+
+
+def test_repair_markdown_handles_empty_and_non_string_values():
+ assert repair_markdown("") == ""
+ assert repair_markdown(None) == ""
+ assert repair_markdown(123) == "123"
+ assert repair_markdown(123, enabled=True) == "123\n"
diff --git a/tests/test_open_source_audit.py b/tests/test_open_source_audit.py
index 3d064c4..86f9e0e 100644
--- a/tests/test_open_source_audit.py
+++ b/tests/test_open_source_audit.py
@@ -310,9 +310,9 @@ def test_content_audit_allows_aicp_internal_endpoints_but_blocks_other_internal_
(tmp_path / "aicp.py").write_text(
"\n".join(
[
+ 'AICP_PUBLIC = "aicp.api.ksyun.com"',
'AICP_INTERNAL = "aicp.internal.api.ksyun.com"',
'AICP_INNER = "aicp.inner.api.ksyun.com"',
- 'MAICP_INNER = "maicp.inner.api.ksyun.com"',
]
),
encoding="utf-8",
diff --git a/tests/test_prepare_ksadk_python_export.py b/tests/test_prepare_ksadk_python_export.py
index 9ae5838..25a3f22 100644
--- a/tests/test_prepare_ksadk_python_export.py
+++ b/tests/test_prepare_ksadk_python_export.py
@@ -43,12 +43,21 @@ def _make_git_repo(root: Path) -> None:
"ksadk_runtime_common/__init__.py": "\n",
"ksadk_runtime_common/schemas/event.json": "{}\n",
"tests/test_open_source_audit.py": "def test_public():\n assert True\n",
+ "tests/test_public_positioning_docs.py": "def test_public_docs():\n assert True\n",
+ "tests/test_check_publication_state.py": "def test_publication_state():\n assert True\n",
"tests/test_tracing_setup_otlp.py": "def test_tracing_public():\n assert True\n",
"tests/test_deploy_integration.py": "def test_internal():\n assert True\n",
"tests/snapshots/help_snapshots.txt": "internal snapshot\n",
"public-docs/index.md": "# Public docs\n",
+ "public-docs/assets/ksadk-runtime-platform-hero.png": "hero\n",
+ "public-docs/assets/ksadk-web-ui-screenshot.png": "screenshot\n",
+ "public-docs/assets/ksadk-runtime-architecture.svg": " \n",
+ "public-docs/assets/ksadk-runtime-architecture.png": "png\n",
+ "public-docs/assets/ksadk-local-debugging-demo.gif": "gif\n",
"scripts/open_source_audit.py": "print('audit')\n",
"scripts/audit_release_artifacts.py": "print('dist audit')\n",
+ "scripts/check_publication_state.py": "print('publication')\n",
+ "scripts/generate_public_assets.py": "print('assets')\n",
"scripts/prepare_ksadk_python_export.py": "print('export')\n",
"scripts/prepare_ksadk_web_export.py": "print('web export')\n",
"docs/internal/release-secret.md": "internal\n",
@@ -115,12 +124,21 @@ def test_export_plan_selects_public_candidate_files_and_excludes_local_artifacts
assert "ksadk_runtime_common/__init__.py" in plan.export_paths
assert "ksadk_runtime_common/schemas/event.json" in plan.export_paths
assert "tests/test_open_source_audit.py" in plan.export_paths
+ assert "tests/test_public_positioning_docs.py" in plan.export_paths
+ assert "tests/test_check_publication_state.py" in plan.export_paths
assert "tests/test_tracing_setup_otlp.py" in plan.export_paths
assert "public-docs/index.md" in plan.export_paths
+ assert "public-docs/assets/ksadk-runtime-platform-hero.png" in plan.export_paths
+ assert "public-docs/assets/ksadk-web-ui-screenshot.png" in plan.export_paths
+ assert "public-docs/assets/ksadk-runtime-architecture.svg" in plan.export_paths
+ assert "public-docs/assets/ksadk-runtime-architecture.png" in plan.export_paths
+ assert "public-docs/assets/ksadk-local-debugging-demo.gif" in plan.export_paths
assert "docs/release-checklist.md" not in plan.export_paths
assert "docs/ksadk开源准备计划.md" not in plan.export_paths
assert "scripts/open_source_audit.py" in plan.export_paths
assert "scripts/audit_release_artifacts.py" in plan.export_paths
+ assert "scripts/check_publication_state.py" in plan.export_paths
+ assert "scripts/generate_public_assets.py" in plan.export_paths
assert "scripts/prepare_ksadk_python_export.py" in plan.export_paths
assert "scripts/prepare_ksadk_web_export.py" in plan.export_paths
assert "tests/test_deploy_integration.py" not in plan.export_paths
@@ -186,6 +204,11 @@ def test_cli_writes_clean_export_candidate_and_manifest(tmp_path):
assert not (output_dir / "ksadk" / "server" / "web-ui").exists()
assert (output_dir / "ksadk_runtime_common" / "__init__.py").is_file()
assert (output_dir / "public-docs" / "index.md").is_file()
+ assert (output_dir / "public-docs" / "assets" / "ksadk-runtime-platform-hero.png").is_file()
+ assert (output_dir / "public-docs" / "assets" / "ksadk-web-ui-screenshot.png").is_file()
+ assert (output_dir / "public-docs" / "assets" / "ksadk-runtime-architecture.svg").is_file()
+ assert (output_dir / "public-docs" / "assets" / "ksadk-runtime-architecture.png").is_file()
+ assert (output_dir / "public-docs" / "assets" / "ksadk-local-debugging-demo.gif").is_file()
assert (output_dir / "export-manifest.json").is_file()
assert not (output_dir / ".pypirc").exists()
assert not (output_dir / "docs" / "internal").exists()
diff --git a/tests/test_prepare_ksadk_web_export.py b/tests/test_prepare_ksadk_web_export.py
index c1bac6e..0c0eea0 100644
--- a/tests/test_prepare_ksadk_web_export.py
+++ b/tests/test_prepare_ksadk_web_export.py
@@ -122,7 +122,7 @@ def test_default_hosted_root_points_to_workspace_sibling_repo():
exporter = _load_export_module()
assert exporter.DEFAULT_HOSTED_ROOT.name == "agentengine-hosted-ui"
- assert exporter.DEFAULT_HOSTED_ROOT.parent.name in {"agentengine", "agent-sdk"}
+ assert exporter.DEFAULT_HOSTED_ROOT.is_absolute()
def test_resolve_default_hosted_root_supports_nested_workspace(tmp_path):
diff --git a/tests/test_public_positioning_docs.py b/tests/test_public_positioning_docs.py
new file mode 100644
index 0000000..7f15414
--- /dev/null
+++ b/tests/test_public_positioning_docs.py
@@ -0,0 +1,449 @@
+from __future__ import annotations
+
+from pathlib import Path
+import re
+import tomllib
+import yaml
+
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+class MkdocsTestLoader(yaml.SafeLoader):
+ pass
+
+
+def _ignore_python_name(loader: MkdocsTestLoader, node: yaml.Node):
+ return loader.construct_scalar(node)
+
+
+MkdocsTestLoader.add_constructor(
+ "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format",
+ _ignore_python_name,
+)
+
+
+def _read(relative_path: str) -> str:
+ return (ROOT / relative_path).read_text(encoding="utf-8")
+
+
+def _public_markdown_and_config_files() -> list[Path]:
+ files = [
+ ROOT / "README.md",
+ ROOT / "README.zh-CN.md",
+ ROOT / "README.en.md",
+ ROOT / "CHANGELOG.md",
+ ROOT / "mkdocs.yml",
+ ROOT / "pyproject.toml",
+ ]
+ files.extend(sorted((ROOT / "public-docs").rglob("*.md")))
+ return files
+
+
+PUBLIC_FORBIDDEN_PATTERNS = (
+ ("pre_release_region", re.compile(r"\bpre[\W_]*online\b", re.IGNORECASE)),
+ ("pre_release_region_zh", re.compile(r"\u9884\u53d1")),
+ (
+ "private_icp_endpoint",
+ re.compile(
+ r"\b(?!(?:aicp[.-](?:inner|internal)[.-]api[.-]ksyun[.-]com|aicp[.-]api[.-]ksyun[.-]com)\b)"
+ r"\w*icp[.-](?:inner|internal)[.-]api[.-][\w.-]+\b",
+ re.IGNORECASE,
+ ),
+ ),
+ ("private_agent_api_endpoint", re.compile(r"\bagent[.-]api[.-]pre\b", re.IGNORECASE)),
+ ("private_kspmas_endpoint", re.compile(r"\bkspmas[.-]internal\b", re.IGNORECASE)),
+ ("private_region_header", re.compile(r"\bX[-_]K(?:sc|SC)[-_]Region\b")),
+ ("private_custom_source_header", re.compile(r"\bX[-_]KSC[-_]CUSTOM[-_]SOURCE\b")),
+ (
+ "private_review_process",
+ re.compile(
+ r"\b(?:internal\s+(?:ezone|review\s+gate|maintainer\s+review)|company\s+review)\b",
+ re.IGNORECASE,
+ ),
+ ),
+ ("private_review_process_zh", re.compile(r"\u5185\u90e8\s*(?:ezone|review|\u5ba1\u6838)")),
+)
+
+
+def _assert_no_public_sensitive_patterns(relative_path: str, text: str) -> None:
+ for label, pattern in PUBLIC_FORBIDDEN_PATTERNS:
+ assert not pattern.search(text), f"{relative_path} matches {label}: {pattern.pattern}"
+
+
+def test_readmes_position_ksadk_as_runtime_platform():
+ expected_sections = (
+ "简体中文(默认)",
+ "一次构建 Agent,到处运行。",
+ "Agent Runtime Platform",
+ "public-docs/assets/ksadk-runtime-platform-hero-wide.png",
+ "真实 CLI 截图",
+ "为什么需要 KsADK",
+ "30 秒快速体验",
+ "真实本地 Web UI 演示",
+ "public-docs/assets/ksadk-web-ui-screenshot.png",
+ "public-docs/assets/ksadk-local-debugging-demo.gif",
+ "public-docs/assets/ksadk-runtime-architecture.png",
+ "架构",
+ "生态定位对比",
+ "可观测",
+ "文档与样例",
+ "相关项目",
+ "参与贡献",
+ )
+ for relative_path in ("README.md", "README.zh-CN.md"):
+ text = _read(relative_path)
+ for expected in expected_sections:
+ assert expected in text, f"{relative_path} missing {expected}"
+ assert "```mermaid" not in text
+ assert "Agent Development Kit" not in text
+ _assert_no_public_sensitive_patterns(relative_path, text)
+ assert "当前版本:" not in text
+ assert "候选版本:" not in text
+ assert "## 0.6.4 重点" not in text
+ assert "## 0.6.3 重点" not in text
+ assert "repair_markdown" not in text
+ assert "[CHANGELOG.md](CHANGELOG.md)" not in text
+ assert "https://github.com/kingsoftcloud/ksadk-python/releases" not in text
+
+
+def test_english_readme_positions_ksadk_as_runtime_platform():
+ text = _read("README.en.md")
+ expected_sections = (
+ "Build agents once. Run them anywhere.",
+ "Agent Runtime Platform",
+ "public-docs/assets/ksadk-runtime-platform-hero-wide.png",
+ "Real KsADK CLI screenshot",
+ "Why KsADK",
+ "30 Seconds Quick Start",
+ "local debugging Web UI",
+ "public-docs/assets/ksadk-web-ui-screenshot.png",
+ "public-docs/assets/ksadk-local-debugging-demo.gif",
+ "public-docs/assets/ksadk-runtime-architecture.png",
+ "Architecture",
+ "Ecosystem Positioning",
+ "Observability",
+ "Docs And Examples",
+ "Related Projects",
+ "Contributing",
+ )
+ for expected in expected_sections:
+ assert expected in text
+ assert "```mermaid" not in text
+ assert "Agent Development Kit" not in text
+ _assert_no_public_sensitive_patterns("README.en.md", text)
+ assert "Current version:" not in text
+ assert "Candidate version:" not in text
+ assert "## 0.6.4 Highlights" not in text
+ assert "## 0.6.3 Highlights" not in text
+ assert "repair_markdown" not in text
+ assert "[CHANGELOG.md](CHANGELOG.md)" not in text
+ assert "https://github.com/kingsoftcloud/ksadk-python/releases" not in text
+
+
+def test_docs_homepage_uses_runtime_platform_information_architecture():
+ zh = _read("public-docs/index.md")
+ en = _read("public-docs/index.en.md")
+
+ for expected in (
+ "一次构建 Agent,到处运行。",
+ "Agent Runtime Platform",
+ "assets/ksadk-runtime-platform-hero-wide.png",
+ "真实 CLI 截图",
+ "为什么需要 KsADK",
+ "真实本地 Web UI 演示",
+ "assets/ksadk-web-ui-screenshot.png",
+ "assets/ksadk-local-debugging-demo.gif",
+ "assets/ksadk-runtime-architecture.png",
+ "生态定位",
+ "VEADK",
+ "AgentRun",
+ "OpenTelemetry",
+ "Hermes",
+ "OpenClaw",
+ "KSYUN_REGION=cn-beijing-6",
+ ):
+ assert expected in zh
+
+ for expected in (
+ "Build agents once. Run them anywhere.",
+ "Agent Runtime Platform",
+ "assets/ksadk-runtime-platform-hero-wide.png",
+ "Real KsADK CLI screenshot",
+ "Why KsADK",
+ "real local Web UI",
+ "assets/ksadk-web-ui-screenshot.png",
+ "assets/ksadk-local-debugging-demo.gif",
+ "assets/ksadk-runtime-architecture.png",
+ "Ecosystem Positioning",
+ "VEADK",
+ "AgentRun",
+ "OpenTelemetry",
+ "Hermes",
+ "OpenClaw",
+ "KSYUN_REGION=cn-beijing-6",
+ ):
+ assert expected in en
+
+ for text in (zh, en):
+ assert "Agent Development Kit" not in text
+ assert "成熟 Agent SDK" not in text
+
+
+def test_docs_include_phase_one_positioning_pages():
+ zh_pages = {
+ "public-docs/getting-started/why-ksadk.md": (
+ "为什么需要 KsADK",
+ "一次构建 Agent,到处运行。",
+ "ADK 解决 Agent 开发",
+ "LangGraph",
+ "OpenAI Agents SDK",
+ "Agent Runtime Platform",
+ ),
+ "public-docs/getting-started/architecture.md": (
+ "架构",
+ "KsADK Agent Runtime Platform 架构",
+ "Skill Runtime",
+ "Workspace",
+ "Sandbox",
+ "Hermes / OpenClaw Runtime",
+ ),
+ "public-docs/getting-started/comparison.md": (
+ "生态定位对比",
+ "这页不是能力打分榜",
+ "VEADK",
+ "AgentRun",
+ "repair_markdown(text, enabled=True)",
+ ),
+ }
+ en_pages = {
+ "public-docs/getting-started/why-ksadk.en.md": (
+ "Why KsADK",
+ "Build agents once. Run them anywhere.",
+ "How do I run, debug, expose, deploy, and observe agents consistently?",
+ "OpenAI Agents SDK",
+ "Agent Runtime Platform",
+ ),
+ "public-docs/getting-started/architecture.en.md": (
+ "Architecture",
+ "KsADK Agent Runtime Platform architecture",
+ "Skill Runtime",
+ "Workspace",
+ "Sandbox",
+ "Hermes / OpenClaw Runtime",
+ ),
+ "public-docs/getting-started/comparison.en.md": (
+ "Ecosystem Positioning",
+ "not a feature scorecard",
+ "VEADK",
+ "AgentRun",
+ "repair_markdown(text, enabled=True)",
+ ),
+ }
+
+ for relative_path, expected_terms in {**zh_pages, **en_pages}.items():
+ text = _read(relative_path)
+ for expected in expected_terms:
+ assert expected in text, f"{relative_path} missing {expected}"
+ _assert_no_public_sensitive_patterns(relative_path, text)
+
+
+def test_public_positioning_does_not_use_misleading_feature_scorecards():
+ scorecard_headers = (
+ "| 能力 | ADK | LangGraph | OpenAI Agents SDK | KsADK |",
+ "| Capability | ADK | LangGraph | OpenAI Agents SDK | KsADK |",
+ )
+ misleading_cells = (
+ "| OpenAI 兼容 API | 不内置 | 不内置 | 部分支持 | 支持 |",
+ "| OpenAI Compatible API | No | No | Partial | Yes |",
+ )
+
+ for relative_path in (
+ "README.md",
+ "README.zh-CN.md",
+ "README.en.md",
+ "public-docs/index.md",
+ "public-docs/index.en.md",
+ "public-docs/getting-started/comparison.md",
+ "public-docs/getting-started/comparison.en.md",
+ ):
+ text = _read(relative_path)
+ for header in scorecard_headers:
+ assert header not in text, f"{relative_path} still uses old scorecard header"
+ for cell in misleading_cells:
+ assert cell not in text, f"{relative_path} still uses misleading OpenAI comparison"
+
+ for relative_path in (
+ "public-docs/index.md",
+ "public-docs/index.en.md",
+ "public-docs/getting-started/comparison.md",
+ "public-docs/getting-started/comparison.en.md",
+ ):
+ text = _read(relative_path)
+ assert "VEADK" in text
+ assert "AgentRun" in text
+
+
+def test_readmes_stay_concise_and_do_not_duplicate_changelog():
+ version_heading_pattern = re.compile(r"^##\s+0\.\d+\.\d+", re.MULTILINE)
+
+ for relative_path in ("README.md", "README.zh-CN.md", "README.en.md"):
+ text = _read(relative_path)
+ assert not version_heading_pattern.search(text), (
+ f"{relative_path} should link to CHANGELOG/Releases instead of listing version highlights"
+ )
+ assert "GitHub Releases" not in text
+ assert "[CHANGELOG.md](CHANGELOG.md)" not in text
+ assert "repair_markdown" not in text
+ assert "最新" not in text
+ assert len(text.splitlines()) < 200, f"{relative_path} should stay concise"
+ assert 'KsADK ' in text
+ assert '' in text
+ assert 'width="860"' in text
+ assert "ksadk-runtime-platform-hero-wide.png" in text
+
+
+def test_public_positioning_uses_factual_ecosystem_focus_terms():
+ expected_terms_by_path = {
+ "public-docs/getting-started/comparison.md": ("A2UI/Frontend", "VeFaaS", "AgentRuntime 生命周期", "Serverless Devs"),
+ "public-docs/getting-started/comparison.en.md": ("A2UI/Frontend", "VeFaaS", "AgentRuntime lifecycle", "Serverless Devs"),
+ }
+
+ for relative_path, expected_terms in expected_terms_by_path.items():
+ text = _read(relative_path)
+ for expected in expected_terms:
+ assert expected in text, f"{relative_path} missing ecosystem evidence term: {expected}"
+
+
+def test_public_visual_assets_are_present_and_nonempty():
+ expected_assets = (
+ "public-docs/assets/ksadk-runtime-platform-hero.png",
+ "public-docs/assets/ksadk-runtime-platform-hero-wide.png",
+ "public-docs/assets/ksadk-web-ui-screenshot.png",
+ "public-docs/assets/ksadk-runtime-architecture.svg",
+ "public-docs/assets/ksadk-runtime-architecture.png",
+ "public-docs/assets/ksadk-local-debugging-demo.gif",
+ )
+ for relative_path in expected_assets:
+ path = ROOT / relative_path
+ assert path.is_file(), f"{relative_path} missing"
+ assert path.stat().st_size > 4096, f"{relative_path} is unexpectedly small"
+
+
+def test_readme_image_links_resolve_inside_repository():
+ markdown_image = re.compile(r"!\[[^\]]*\]\(([^)]+)\)")
+ html_image = re.compile(r' ]*\bsrc="([^"]+)"', re.IGNORECASE)
+
+ for relative_markdown_path in ("README.md", "README.zh-CN.md", "README.en.md"):
+ text = _read(relative_markdown_path)
+ image_targets = markdown_image.findall(text) + html_image.findall(text)
+ assert image_targets, f"{relative_markdown_path} should contain rendered images"
+ for image_target in image_targets:
+ if "://" in image_target:
+ continue
+ image_path = (ROOT / image_target).resolve()
+ assert image_path.is_relative_to(ROOT), (
+ f"{relative_markdown_path} image escapes repository: {image_target}"
+ )
+ assert image_path.is_file(), (
+ f"{relative_markdown_path} image target missing: {image_target}"
+ )
+ assert image_path.stat().st_size > 4096, (
+ f"{relative_markdown_path} image target unexpectedly small: {image_target}"
+ )
+
+
+def test_public_navigation_is_task_oriented():
+ mkdocs = _read("mkdocs.yml")
+ for expected in ("Getting Started", "Build", "Run", "Deploy", "Observe", "Extend", "Reference"):
+ assert expected in mkdocs
+ for expected in (
+ "为什么需要 KsADK: getting-started/why-ksadk.md",
+ "架构: getting-started/architecture.md",
+ "生态定位对比: getting-started/comparison.md",
+ "为什么需要 KsADK: Why KsADK",
+ "架构: Architecture",
+ "生态定位对比: Ecosystem Positioning",
+ ):
+ assert expected in mkdocs
+ assert "快速开始: Quick Start" in mkdocs
+ assert "Kingsoft Cloud Agent Development Kit" not in mkdocs
+ assert "金山云智能体开发套件" not in mkdocs
+
+
+def test_markdown_repair_is_documented_as_opt_in():
+ expected_by_path = {
+ "public-docs/guides/agent-best-practices.md": (
+ "Markdown 输出修复",
+ "显式开启",
+ "repair_markdown(text, enabled=True)",
+ "默认不会改写模型原文",
+ ),
+ "public-docs/guides/agent-best-practices.en.md": (
+ "Markdown Output Repair",
+ "enable the lightweight repair helper",
+ "repair_markdown(text, enabled=True)",
+ "does not rewrite raw model output by default",
+ ),
+ }
+
+ for relative_path, expected_terms in expected_by_path.items():
+ text = _read(relative_path)
+ for expected in expected_terms:
+ assert expected in text, f"{relative_path} missing {expected}"
+
+
+def test_english_navigation_translates_all_chinese_labels():
+ config = yaml.load(_read("mkdocs.yml"), Loader=MkdocsTestLoader)
+ translations = (
+ config["plugins"][1]["i18n"]["languages"][1]["nav_translations"]
+ )
+
+ def iter_labels(items):
+ for item in items:
+ if isinstance(item, str):
+ yield Path(item).stem
+ elif isinstance(item, dict):
+ for label, children in item.items():
+ yield str(label)
+ if isinstance(children, list):
+ yield from iter_labels(children)
+
+ missing = sorted(
+ label
+ for label in iter_labels(config["nav"])
+ if any("\u4e00" <= character <= "\u9fff" for character in label)
+ and label not in translations
+ )
+
+ assert missing == []
+
+
+def test_public_materials_do_not_publish_environment_specific_release_words():
+ for path in _public_markdown_and_config_files():
+ text = path.read_text(encoding="utf-8")
+ relative_path = path.relative_to(ROOT)
+ _assert_no_public_sensitive_patterns(str(relative_path), text)
+
+
+def test_package_metadata_is_runtime_platform_positioned_for_patch_candidate():
+ pyproject = tomllib.loads(_read("pyproject.toml"))
+ init_text = _read("ksadk/__init__.py")
+ version_text = _read("ksadk/version.py")
+
+ assert pyproject["project"]["version"] == "0.6.4"
+ assert 'VERSION = "0.6.4"' in version_text
+ assert "Agent Runtime Platform" in pyproject["project"]["description"]
+ assert "Agent Runtime Platform" in init_text
+ assert "Agent Development Kit" not in pyproject["project"]["description"]
+ assert "Agent Development Kit" not in init_text
+
+
+def test_patch_version_changelog_is_ready_for_authorized_release():
+ changelog = _read("CHANGELOG.md")
+ assert "## [0.6.4] - 2026-06-10" in changelog
+ assert "## [0.6.4] - Unreleased" not in changelog
+ assert "用户 review 通过前" not in changelog
+ assert "不创建 tag" not in changelog
+ assert "不发布 GitHub Release" not in changelog
+ assert "不上传 PyPI" not in changelog
diff --git a/tests/test_public_release_gates.py b/tests/test_public_release_gates.py
new file mode 100644
index 0000000..ac5149b
--- /dev/null
+++ b/tests/test_public_release_gates.py
@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+import re
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def _makefile() -> str:
+ return (ROOT / "Makefile").read_text(encoding="utf-8")
+
+
+def _target_dependencies(makefile: str, target: str) -> set[str]:
+ match = re.search(rf"^{re.escape(target)}:\s*(?P[^\n]*)$", makefile, re.MULTILINE)
+ assert match, f"missing Makefile target: {target}"
+ return set(match.group("deps").split())
+
+
+def test_external_publish_targets_require_review_gate_and_publish_state_check():
+ makefile = _makefile()
+
+ for target in ("publish", "publish-test"):
+ deps = _target_dependencies(makefile, target)
+ assert "open-source-approval-check" in deps
+ assert "public-preflight" in deps
+ assert "public-publish-check" in deps
+
+
+def test_public_release_tag_requires_approval_check():
+ makefile = _makefile()
+
+ deps = _target_dependencies(makefile, "public-release-tag")
+
+ assert "open-source-approval-check" in deps
+ assert "public-preflight" in deps
+ assert "public-publish-check" in deps
+ assert "内部审核" not in makefile
+
+
+def test_publication_state_make_target_uses_valid_phase():
+ makefile = _makefile()
+
+ match = re.search(
+ r"^open-source-publication-state:\n(?P(?:\t.*\n)+)",
+ makefile,
+ re.MULTILINE,
+ )
+ assert match
+ body = match.group("body")
+
+ assert "--phase placeholder" not in body
+ assert "--phase pre-publish" in body or 'PUBLIC_PUBLISH_PHASE' in body
+
+
+def test_public_test_and_ci_cover_release_gate_and_runtime_markdown_tests():
+ makefile = _makefile()
+ ci = (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8")
+
+ for required in (
+ "tests/test_check_approval_record.py",
+ "tests/test_public_release_gates.py",
+ "tests/test_markdown_repair.py",
+ "tests/test_conversation_runtime.py",
+ "tests/test_server_session_app.py",
+ ):
+ assert required in makefile
+ assert required in ci
diff --git a/tests/test_server_session_app.py b/tests/test_server_session_app.py
index 179b219..7fe09db 100644
--- a/tests/test_server_session_app.py
+++ b/tests/test_server_session_app.py
@@ -991,12 +991,14 @@ async def test_chat_completions_forwards_model_to_runner(monkeypatch):
"messages": [{"role": "user", "content": "hello"}],
"stream": False,
"model": "glm-5.1",
+ "account_id": "acct-chat",
},
)
assert response.status_code == 200
assert runner.prepared_models == ["glm-5.1"]
assert runner.calls[-1]["model"] == "glm-5.1"
+ assert runner.calls[-1]["platform_context"]["account_id"] == "acct-chat"
@pytest.mark.asyncio
@@ -1479,6 +1481,7 @@ async def test_responses_uses_official_conversation_as_runtime_session(monkeypat
"input": "hello",
"conversation": "conv-a",
"safety_identifier": "user-a",
+ "account_id": "acct-a",
"stream": False,
},
)
@@ -1491,6 +1494,7 @@ async def test_responses_uses_official_conversation_as_runtime_session(monkeypat
assert session.user_id == "user-a"
assert runner.calls[-1]["session_id"] == "conv-a"
assert runner.calls[-1]["platform_context"]["user_id"] == "user-a"
+ assert runner.calls[-1]["platform_context"]["account_id"] == "acct-a"
@pytest.mark.asyncio
diff --git a/uv.lock b/uv.lock
index 9d458ec..be2e353 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2451,7 +2451,7 @@ wheels = [
[[package]]
name = "ksadk"
-version = "0.6.3"
+version = "0.6.4"
source = { editable = "." }
dependencies = [
{ name = "a2a-sdk" },