mirror of
https://github.com/TencentCloud/Octop.git
synced 2026-10-16 05:39:20 +08:00
* feat: admin user batch policies, token UX, default FS root, and media push hardening Add admin user batch enable/disable/delete and resource policies (token quota, max experts), improve token inputs with K/M presets, default unrestricted local backends to host filesystem root, and tighten gateway tool-media push with clearer path-outside-root stream errors. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(workspace): treat Windows drive roots as host-root sentinels Default FS-root backends use C:/ on Windows; treating that as a scoped jail put workspaces under the drive root and broke bootstrap/invite tests. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: jubaoliang <jubaoliang@tencent.com> Co-authored-by: Cursor <cursoragent@cursor.com>
139 lines
4.8 KiB
Python
139 lines
4.8 KiB
Python
"""tests/integration/test_setup_bootstrap.py — initial-admin path."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from octop.infra.server import OctopServer
|
|
from octop.infra.setup.password_file import read_password
|
|
from tests.support.app import write_octop_config
|
|
from tests.support.auth import auth_header, bootstrap_admin
|
|
|
|
|
|
async def test_setup_status_flips_after_bootstrap(patched_app_client: Any) -> None:
|
|
c, _srv, home = patched_app_client
|
|
r = await c.get("/api/setup/status")
|
|
assert r.status_code == 200
|
|
assert r.json()["setup_required"] is True
|
|
|
|
r = await bootstrap_admin(c, home)
|
|
assert r.status_code == 201
|
|
|
|
r = await c.get("/api/setup/status")
|
|
assert r.json()["setup_required"] is False
|
|
|
|
|
|
async def test_bootstrap_creates_default_main_agent(patched_app_client: Any) -> None:
|
|
c, _srv, home = patched_app_client
|
|
await bootstrap_admin(c, home)
|
|
auth = await auth_header(c)
|
|
|
|
r = await c.get("/api/agents", headers=auth)
|
|
assert r.status_code == 200
|
|
agents = r.json()
|
|
assert len(agents) == 1
|
|
assert agents[0]["agent_id"] == "main"
|
|
assert agents[0]["name"] == "小通 · 通用助手"
|
|
assert agents[0]["state"] in {"created", "idle", "stopped", "failed", "running", "unknown"}
|
|
|
|
|
|
async def test_main_not_created_until_finish(patched_app_client: Any) -> None:
|
|
c, _srv, home = patched_app_client
|
|
pw = read_password(home.parent)
|
|
assert pw is not None
|
|
tok = (await c.post("/api/setup/verify-password", json={"password": pw})).json()["wizard_token"]
|
|
admin = (
|
|
await c.post(
|
|
"/api/setup/initial-admin",
|
|
json={"username": "admin", "password": "TestPass12"},
|
|
headers={"Authorization": f"Bearer {tok}"},
|
|
)
|
|
).json()
|
|
auth = {"Authorization": f"Bearer {admin['access_token']}"}
|
|
r = await c.get("/api/agents", headers=auth)
|
|
assert r.status_code == 200
|
|
assert r.json() == []
|
|
|
|
finish = await c.post(
|
|
"/api/setup/finish",
|
|
json={"provider_draft": None},
|
|
headers={"Authorization": f"Bearer {tok}"},
|
|
)
|
|
assert finish.status_code == 200
|
|
r = await c.get("/api/agents", headers=auth)
|
|
assert len(r.json()) == 1
|
|
assert r.json()[0]["agent_id"] == "main"
|
|
|
|
|
|
async def test_double_bootstrap_returns_410(patched_app_client: Any) -> None:
|
|
c, _srv, home = patched_app_client
|
|
r = await bootstrap_admin(c, home)
|
|
assert r.status_code == 201
|
|
r = await c.post(
|
|
"/api/setup/initial-admin",
|
|
json={"username": "admin2", "password": "TestPass12"},
|
|
)
|
|
assert r.status_code == 410
|
|
assert r.json()["error"]["code"] == "SETUP_REQUIRED"
|
|
|
|
|
|
async def test_start_writes_wizard_password_when_no_users(tmp_octop_home: Path) -> None:
|
|
srv = OctopServer(home=tmp_octop_home)
|
|
await srv.start()
|
|
try:
|
|
assert (tmp_octop_home.parent / "octop-login.txt").exists()
|
|
finally:
|
|
await srv.stop()
|
|
|
|
|
|
async def test_start_skips_wizard_password_when_disabled(tmp_octop_home: Path) -> None:
|
|
write_octop_config(tmp_octop_home, require_setup_password=False)
|
|
srv = OctopServer(home=tmp_octop_home)
|
|
await srv.start()
|
|
try:
|
|
assert not (tmp_octop_home / "octop-login.txt").exists()
|
|
finally:
|
|
await srv.stop()
|
|
|
|
|
|
async def test_main_agent_seeds_general_assistant_workspace(patched_app_client: Any) -> None:
|
|
"""Finish creates main with expert files even when no provider is configured yet."""
|
|
c, _srv, home = patched_app_client
|
|
await bootstrap_admin(c, home)
|
|
ws = home / "agents" / "main"
|
|
assert (ws / "SOUL.md").is_file()
|
|
# New agents use system_files_path=.octop — BackendWorkspace remaps skills/.
|
|
assert (ws / ".octop" / "skills" / "octop-assistant" / "SKILL.md").is_file()
|
|
|
|
|
|
async def test_main_agent_uses_fs_root_backend(patched_app_client: Any) -> None:
|
|
"""Default main uses the same FS-root local backend as dashboard-created agents."""
|
|
from octop.infra.utils.host_dirs import host_fs_tree_root
|
|
|
|
c, srv, home = patched_app_client
|
|
await bootstrap_admin(c, home)
|
|
assert srv.app_runtime is not None
|
|
row = srv.app_runtime.agent_registry.get_row("main")
|
|
assert row is not None
|
|
cfg = json.loads(row.config_json or "{}")
|
|
assert cfg["backend"] == {
|
|
"type": "local_shell",
|
|
"root_dir": host_fs_tree_root(),
|
|
"virtual_mode": True,
|
|
}
|
|
assert cfg["workspace_dir"] == str((home / "agents" / "main").resolve())
|
|
|
|
|
|
async def test_main_agent_uses_general_assistant_template(patched_app_client: Any) -> None:
|
|
c, _srv, home = patched_app_client
|
|
await bootstrap_admin(c, home)
|
|
auth = await auth_header(c)
|
|
r = await c.get("/api/agents", headers=auth)
|
|
assert r.status_code == 200
|
|
agent = r.json()[0]
|
|
assert agent["agent_id"] == "main"
|
|
assert agent["name"] == "小通 · 通用助手"
|
|
assert agent.get("template_name") == "general-assistant"
|