mirror of
https://github.com/TencentCloud/Octop.git
synced 2026-10-28 07:33:35 +08:00
* feat: workspace backend sandboxing, execute-env injection, and ephemeral image refs Workspace file-I/O hardening across backend and dashboard: - Host directory sandboxing via root_dir allow-list and denied prefixes - Execute-environment defaults injected into harness backend specs - Ephemeral workspace image rematerialization for model calls - Attachment hint expansion and inbound store improvements - Portable memory backend and skill workspace catalog updates - Dashboard UI for agent backend fields and root-dir selection - Bump harness-agent 0.9.23 / harness-memory 0.9.7; docs + install notes * feat: add user invitation codes with admin management and redeem flow - Add user_invites table (migration 009) plus SQLite/Postgres reconcile paths - Add InviteRepo and InviteService for create/revoke/list/redeem - Expose public validate/redeem endpoints and admin create/revoke/list routers - Extract default-agent bootstrap into octop.infra.agents.default_agent - Add invite error codes and en/zh i18n strings - Dashboard: invite drawer, login redeem UI, users list, and locales * fix: add Windows cross-platform guards for pre-existing sandbox/rootfs tests - Guard POSIX/container-only tests (rootfs-absolute /.octop/... workspace paths) with posix_only in test_thread_artifacts.py and test_agent_manager.py - Normalize the rendered workspace path assertion in test_octop_builtin_skills.py (forward slashes on Windows, not str(tmp_path) backslashes) - Canonicalize ws_dir before the prefix check in attachment hint/path tests (tempfile may yield an 8.3 short path on Windows while the resolved attachment path uses the long form) These failures predate the user-invites commit and are unrelated to it; the product path logic is correct. --------- Co-authored-by: jubaoliang <jubaoliang@tencent.com>
135 lines
4.6 KiB
Python
135 lines
4.6 KiB
Python
"""Live integration tests for :class:`octop.infra.agents.manager.AgentManager`.
|
|
|
|
Requires ``OPENAI_API_KEY``, ``OPENAI_BASE_URL``, and ``OPENAI_MODEL_NAME`` in
|
|
the repo-root ``.env``. Skipped by default — run with::
|
|
|
|
uv run pytest tests/live/test_agent_manager_live.py -m live -v
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from octop.infra.agents.manager import AgentCreateSpec
|
|
from octop.infra.agents.providers import KIND_TO_PROTOCOL
|
|
from octop.infra.errors import OctopError
|
|
|
|
pytestmark = pytest.mark.live
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_boot_registers_provider_from_db(live_agent_manager, live_openai_config) -> None:
|
|
providers = live_agent_manager.providers.build_harness_configs()
|
|
assert len(providers) == 1
|
|
p = providers[0]
|
|
assert p.id == live_openai_config.provider_name
|
|
assert p.base_url == live_openai_config.base_url
|
|
assert KIND_TO_PROTOCOL["openai"] == "openai"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_agent_seeds_workspace_and_starts(
|
|
live_agent_manager,
|
|
live_openai_config,
|
|
tmp_path,
|
|
) -> None:
|
|
row = await live_agent_manager.create(
|
|
AgentCreateSpec(
|
|
name="live-bot",
|
|
default_model=live_openai_config.default_model,
|
|
system_prompt="You are a terse assistant.",
|
|
),
|
|
)
|
|
|
|
db_row = live_agent_manager.get_row(row.agent_id)
|
|
assert db_row is not None
|
|
assert db_row.last_state == "running"
|
|
ws = live_agent_manager._paths.agent_workspace(row.agent_id)
|
|
assert (ws / "AGENTS.md").is_file()
|
|
|
|
harness = live_agent_manager.get_agent(row.agent_id)
|
|
assert harness is not None
|
|
entry = live_agent_manager._harness_manager.get_agent(row.agent_id) # type: ignore[union-attr]
|
|
assert entry.agent_id == row.agent_id
|
|
cfg = live_agent_manager._build_harness_config(row)
|
|
backend = cfg.backend
|
|
assert backend["type"] == "local_shell"
|
|
assert backend["root_dir"] == "/"
|
|
assert backend["virtual_mode"] is True
|
|
# execute-env injection (PR #376): platform identity folded into shell env
|
|
assert backend.get("inherit_env") is True
|
|
env = backend["env"]
|
|
assert env["OCTOP_AGENT_ID"] == row.agent_id
|
|
assert "OCTOP_AUTH_DIR" in env and "OCTOP_HOME" in env and "OCTOP_SKILLS_DIR" in env
|
|
assert str(cfg.workspace_dir) == str(ws)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_with_expert_template_writes_soul(
|
|
live_agent_manager,
|
|
live_openai_config,
|
|
) -> None:
|
|
row = await live_agent_manager.create(
|
|
AgentCreateSpec(
|
|
name="general",
|
|
default_model=live_openai_config.default_model,
|
|
template_name="general-assistant",
|
|
),
|
|
)
|
|
|
|
ws = live_agent_manager._paths.agent_workspace(row.agent_id)
|
|
assert (ws / "SOUL.md").is_file()
|
|
soul = (ws / "SOUL.md").read_text(encoding="utf-8")
|
|
assert len(soul) > 20
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_reloads_running_agent(live_agent_manager, live_openai_config) -> None:
|
|
row = await live_agent_manager.create(
|
|
AgentCreateSpec(name="reload-me", default_model=live_openai_config.default_model),
|
|
)
|
|
updated = await live_agent_manager.update(
|
|
row.agent_id,
|
|
name="reload-me-v2",
|
|
system_prompt="Updated prompt.",
|
|
)
|
|
assert updated.name == "reload-me-v2"
|
|
assert live_agent_manager.get_row(row.agent_id).last_state == "running"
|
|
assert live_agent_manager.get_agent(row.agent_id) is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_removes_agent(live_agent_manager, live_openai_config) -> None:
|
|
row = await live_agent_manager.create(
|
|
AgentCreateSpec(name="delete-me", default_model=live_openai_config.default_model),
|
|
)
|
|
agent_id = row.agent_id
|
|
await live_agent_manager.delete(agent_id)
|
|
|
|
assert live_agent_manager.get_row(agent_id) is None
|
|
with pytest.raises(OctopError):
|
|
live_agent_manager.get_agent(agent_id)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_provider_changed_hot_reload(live_agent_manager, live_openai_config) -> None:
|
|
await live_agent_manager.on_provider_changed()
|
|
providers = live_agent_manager.providers.build_harness_configs()
|
|
assert any(p.id == live_openai_config.provider_name for p in providers)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_agent_call_returns_reply(live_agent_manager, live_openai_config) -> None:
|
|
row = await live_agent_manager.create(
|
|
AgentCreateSpec(
|
|
name="caller",
|
|
default_model=live_openai_config.default_model,
|
|
system_prompt="Reply with exactly one word: pong",
|
|
),
|
|
)
|
|
agent = live_agent_manager.get_agent(row.agent_id)
|
|
result = await agent.call("ping")
|
|
assert result is not None
|
|
text = str(result).lower()
|
|
assert len(text) > 0
|