feat: add wheel package

This commit is contained in:
qixinbo
2026-03-31 21:06:13 +08:00
parent 01524aaff5
commit 7cdbf1d333
9 changed files with 628 additions and 41 deletions
+102
View File
@@ -0,0 +1,102 @@
import json
import sys
from importlib import import_module
from pathlib import Path
from typer.testing import CliRunner
BACKEND_ROOT = Path(__file__).resolve().parents[1]
REPO_ROOT = BACKEND_ROOT.parent
NANOBOT_ROOT = REPO_ROOT / "nanobot"
if str(BACKEND_ROOT) not in sys.path:
sys.path.insert(0, str(BACKEND_ROOT))
if str(NANOBOT_ROOT) not in sys.path:
sys.path.insert(0, str(NANOBOT_ROOT))
app = import_module("app.cli").app
runner = CliRunner()
class _FakeProcess:
def __init__(self, pid: int = 9527, exit_code: int | None = None) -> None:
self.pid = pid
self._exit_code = exit_code
def poll(self):
return self._exit_code
def test_start_command_writes_state(monkeypatch, tmp_path) -> None:
pid_file = tmp_path / "run" / "state.json"
log_file = tmp_path / "run" / "service.log"
monkeypatch.setattr("app.cli.subprocess.Popen", lambda *args, **kwargs: _FakeProcess())
monkeypatch.setattr("app.cli._wait_for_server_ready", lambda *_args, **_kwargs: True)
result = runner.invoke(
app,
[
"start",
"--host",
"127.0.0.1",
"--port",
"18999",
"--pid-file",
str(pid_file),
"--log-file",
str(log_file),
],
)
assert result.exit_code == 0
assert "已启动" in result.stdout
assert pid_file.exists()
state = json.loads(pid_file.read_text(encoding="utf-8"))
assert state["pid"] == 9527
assert state["host"] == "127.0.0.1"
assert state["port"] == 18999
def test_status_command_reports_running(monkeypatch, tmp_path) -> None:
pid_file = tmp_path / "run" / "state.json"
pid_file.parent.mkdir(parents=True, exist_ok=True)
pid_file.write_text(
json.dumps({"pid": 9527, "host": "127.0.0.1", "port": 18080}, ensure_ascii=False),
encoding="utf-8",
)
monkeypatch.setattr("app.cli._is_process_running", lambda pid: pid == 9527)
result = runner.invoke(app, ["status", "--pid-file", str(pid_file)])
assert result.exit_code == 0
assert "running" in result.stdout
assert "127.0.0.1:18080" in result.stdout
def test_stop_command_cleans_state(monkeypatch, tmp_path) -> None:
pid_file = tmp_path / "run" / "state.json"
pid_file.parent.mkdir(parents=True, exist_ok=True)
pid_file.write_text(json.dumps({"pid": 9527}, ensure_ascii=False), encoding="utf-8")
monkeypatch.setattr("app.cli._is_process_running", lambda pid: pid == 9527)
monkeypatch.setattr("app.cli._stop_pid", lambda pid, timeout: pid == 9527)
result = runner.invoke(app, ["stop", "--pid-file", str(pid_file)])
assert result.exit_code == 0
assert "已停止" in result.stdout
assert not pid_file.exists()
def test_status_command_cleans_stale_state(monkeypatch, tmp_path) -> None:
pid_file = tmp_path / "run" / "state.json"
pid_file.parent.mkdir(parents=True, exist_ok=True)
pid_file.write_text(json.dumps({"pid": 9527}, ensure_ascii=False), encoding="utf-8")
monkeypatch.setattr("app.cli._is_process_running", lambda _pid: False)
result = runner.invoke(app, ["status", "--pid-file", str(pid_file)])
assert result.exit_code == 0
assert "stopped" in result.stdout
assert not pid_file.exists()
@@ -0,0 +1,78 @@
from pathlib import Path
import sys
from fastapi.staticfiles import StaticFiles
from fastapi.testclient import TestClient
BACKEND_ROOT = Path(__file__).resolve().parents[1]
REPO_ROOT = BACKEND_ROOT.parent
NANOBOT_ROOT = REPO_ROOT / "nanobot"
if str(BACKEND_ROOT) not in sys.path:
sys.path.insert(0, str(BACKEND_ROOT))
if str(NANOBOT_ROOT) not in sys.path:
sys.path.insert(0, str(NANOBOT_ROOT))
import main
def _prepare_webui(monkeypatch, tmp_path: Path) -> None:
webui_dir = tmp_path / "webui"
assets_dir = webui_dir / "assets"
assets_dir.mkdir(parents=True, exist_ok=True)
(webui_dir / "index.html").write_text("<html><body>dataclaw-webui</body></html>", encoding="utf-8")
(assets_dir / "app.js").write_text("window.__TASK2__=true;", encoding="utf-8")
monkeypatch.setattr(main, "_WEBUI_DIR", webui_dir)
monkeypatch.setattr(main, "_WEBUI_INDEX", webui_dir / "index.html")
monkeypatch.setattr(main, "_WEBUI_STATIC", StaticFiles(directory=str(webui_dir), html=False))
def _prepare_lifecycle(monkeypatch) -> None:
async def fake_start():
return None
async def fake_stop():
return None
monkeypatch.setattr(main.nanobot_service, "start", fake_start)
monkeypatch.setattr(main.nanobot_service, "stop", fake_stop)
def test_webui_static_assets_served_from_backend(monkeypatch, tmp_path) -> None:
_prepare_webui(monkeypatch, tmp_path)
_prepare_lifecycle(monkeypatch)
client = TestClient(main.app)
index_resp = client.get("/")
assert index_resp.status_code == 200
assert "dataclaw-webui" in index_resp.text
asset_resp = client.get("/assets/app.js")
assert asset_resp.status_code == 200
assert "window.__TASK2__=true;" in asset_resp.text
def test_spa_route_fallback_to_index_html(monkeypatch, tmp_path) -> None:
_prepare_webui(monkeypatch, tmp_path)
_prepare_lifecycle(monkeypatch)
client = TestClient(main.app)
spa_resp = client.get("/settings/users")
assert spa_resp.status_code == 200
assert "dataclaw-webui" in spa_resp.text
missing_asset_resp = client.get("/assets/missing.js")
assert missing_asset_resp.status_code == 404
def test_backend_accessible_without_frontend_dev_server(monkeypatch, tmp_path) -> None:
_prepare_webui(monkeypatch, tmp_path)
_prepare_lifecycle(monkeypatch)
client = TestClient(main.app)
ui_resp = client.get("/")
assert ui_resp.status_code == 200
assert "dataclaw-webui" in ui_resp.text
api_resp = client.get("/nanobot/status")
assert api_resp.status_code == 200
assert api_resp.json()["status"] in {"running", "stopped"}