Files
OpenOPC/tests/test_org_saved_crud.py
2026-07-01 17:56:31 +08:00

542 lines
21 KiB
Python

"""CRUD tests for saved org architectures under .opc/config/company_orgs."""
from __future__ import annotations
import asyncio
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
import yaml
def test_name_regex_accepts_valid_ids():
from opc.plugins.office_ui.ws_handler import _SAVED_ORG_NAME_RE
for name in ["lab_org", "startup-v2", "a", "a1", "0" * 64, "foo_bar-baz"]:
assert _SAVED_ORG_NAME_RE.match(name), f"should accept {name!r}"
def test_name_regex_rejects_invalid_ids():
from opc.plugins.office_ui.ws_handler import _SAVED_ORG_NAME_RE
for name in ["", "..", "../etc", "foo/bar", "has space", "x" * 65, "A1", "unicode_ß"]:
assert not _SAVED_ORG_NAME_RE.match(name), f"should reject {name!r}"
def test_saved_org_path_valid_resolves_under_company_orgs(monkeypatch, tmp_path):
from opc.plugins.office_ui import ws_handler as wh
monkeypatch.setattr("opc.core.config._find_project_root", lambda: tmp_path)
result = wh._saved_org_path("my_org")
expected = tmp_path / ".opc" / "config" / "company_orgs" / "org_my_org_config.yaml"
assert result == expected
def test_saved_org_path_lax_slugifies_display_name(monkeypatch, tmp_path):
from opc.plugins.office_ui import ws_handler as wh
monkeypatch.setattr("opc.core.config._find_project_root", lambda: tmp_path)
result = wh._saved_org_path("HKU Lab", strict=False)
assert result == tmp_path / ".opc" / "config" / "company_orgs" / "org_hku_lab_config.yaml"
with pytest.raises(ValueError):
wh._saved_org_path("../etc/passwd")
def _write_saved_org(config_dir: Path, organization_id: str, *, organization_name: str = "Test Co", roles_count: int = 2) -> Path:
from opc.core.org_config import org_config_path
from opc.plugins.office_ui.org_architecture_snapshot import ORG_ARCHITECTURE_KIND
payload = {
"schema_version": 2,
"kind": ORG_ARCHITECTURE_KIND,
"organization_id": organization_id,
"organization_name": organization_name,
"company": {
"name": organization_name,
"topology": "flat",
"company_profile": "custom",
"final_decider_role_id": None,
"company_profiles": ["corporate", "custom"],
},
"roles": [
{"id": f"role_{i}", "name": f"Role {i}", "responsibility": "Work.", "reports_to": "owner"}
for i in range(roles_count)
],
"employees": [],
"escalation_rules": [],
"runtime_policies": {},
"talent_templates": [],
"teams": [],
"team_runtime": {},
"installed_packages": [],
"role_serial_queue_enabled": True,
"metadata": {"source": "test"},
}
path = org_config_path(config_dir, organization_id)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(yaml.dump(payload, default_flow_style=False, sort_keys=False), encoding="utf-8")
return path
class _FakeServerStateStore:
def __init__(self, state: dict[str, str] | None = None) -> None:
self.state = dict(state or {})
async def get_server_state(self, key: str, default: str = "") -> str:
return self.state.get(key, default)
async def set_server_state(self, key: str, value: str) -> None:
self.state[key] = value
class _FakeWS:
def __init__(self) -> None:
self.messages: list[dict] = []
async def send_json(self, payload: dict) -> None:
self.messages.append(payload)
def _async_cm() -> AsyncMock:
lock = AsyncMock()
lock.__aenter__ = AsyncMock(return_value=None)
lock.__aexit__ = AsyncMock(return_value=None)
return lock
def test_saved_file_structure_is_loadable(tmp_path):
from opc.plugins.office_ui.org_architecture_snapshot import ORG_ARCHITECTURE_KIND
config_dir = tmp_path / ".opc" / "config"
path = _write_saved_org(config_dir, "fixture_org", roles_count=3)
parsed = yaml.safe_load(path.read_text(encoding="utf-8"))
assert parsed["schema_version"] == 2
assert parsed["kind"] == ORG_ARCHITECTURE_KIND
assert parsed["organization_id"] == "fixture_org"
assert parsed["company"]["company_profile"] == "custom"
assert len(parsed["roles"]) == 3
def test_saved_org_storage_rejects_reserved_corporate_id(tmp_path):
from opc.core.org_config import (
list_org_config_paths,
org_config_path,
read_org_index,
write_org_config_payload,
write_org_index,
)
config_dir = tmp_path / ".opc" / "config"
config_dir.mkdir(parents=True)
with pytest.raises(ValueError):
org_config_path(config_dir, "corporate")
with pytest.raises(ValueError):
write_org_index(config_dir, "corporate")
with pytest.raises(ValueError):
write_org_config_payload(config_dir, "corporate", {"organization_id": "corporate"})
orgs_dir = config_dir / "company_orgs"
orgs_dir.mkdir(parents=True)
(orgs_dir / "org_corporate_config.yaml").write_text("organization_id: corporate\n", encoding="utf-8")
(config_dir / "org_index.yaml").write_text("active_organization_id: corporate\n", encoding="utf-8")
assert read_org_index(config_dir) is None
assert list_org_config_paths(config_dir) == []
def test_load_via_import_format_accepted_by_loader(tmp_path):
from opc.core.config import OPCConfig
from opc.plugins.office_ui.org_architecture_snapshot import apply_org_architecture_snapshot, parse_org_architecture_snapshot
config_dir = tmp_path / ".opc" / "config"
path = _write_saved_org(config_dir, "lab", roles_count=2)
cfg = apply_org_architecture_snapshot(OPCConfig(), parse_org_architecture_snapshot(path.read_text()))
assert cfg.org.organization_id == "lab"
assert len(cfg.org.roles) == 2
assert cfg.org.company_profile == "custom"
def test_write_active_org_config_never_writes_reserved_corporate_saved_org(tmp_path):
from opc.core.config import OPCConfig
from opc.plugins.office_ui import ws_handler as wh
cfg = OPCConfig()
cfg.org.company_profile = "custom"
cfg.org.organization_id = "corporate"
cfg.org.organization_name = "Corporate"
cfg.org.company_name = "Corporate"
handler = wh.WSHandler.__new__(wh.WSHandler)
handler.engine = SimpleNamespace(config=cfg, opc_home=tmp_path / ".opc")
handler._write_active_org_config(cfg)
config_dir = tmp_path / ".opc" / "config"
assert cfg.org.organization_id != "corporate"
assert not (config_dir / "company_orgs" / "org_corporate_config.yaml").exists()
assert (config_dir / "company_orgs" / f"org_{cfg.org.organization_id}_config.yaml").exists()
def test_org_service_rejects_corporate_write_operations(tmp_path):
from opc.core.config import OPCConfig
from opc.plugins.office_ui.services.context import ModeState, OfficeServiceContext
from opc.plugins.office_ui.services.models import ServiceError
from opc.plugins.office_ui.services.org import OrgService
async def run() -> None:
engine = SimpleNamespace(
config=OPCConfig(),
opc_home=tmp_path / ".opc",
org_engine=MagicMock(),
talent_market=MagicMock(),
)
context = OfficeServiceContext(
engine=engine,
agent_store=MagicMock(),
chat_store=MagicMock(),
event_adapter=MagicMock(),
mode_state=ModeState(exec_mode="company", company_profile="corporate"),
)
with pytest.raises(ServiceError) as exc:
await OrgService(context).add_role({
"role_id": "analyst",
"name": "Analyst",
"responsibility": "Analyze.",
})
assert exc.value.code == "org_read_only"
asyncio.run(run())
def test_org_service_saved_create_from_corporate_mode(tmp_path):
from opc.core.config import OPCConfig
from opc.core.org_config import (
apply_org_config_payload_to_config,
load_org_config_payload,
read_org_index,
validate_runnable_org_config,
)
from opc.plugins.office_ui.services.context import ModeState, OfficeServiceContext
from opc.plugins.office_ui.services.org import OrgService
async def run() -> None:
cfg = OPCConfig()
org_engine = SimpleNamespace(config=cfg, reload_from_config=MagicMock())
talent_market = SimpleNamespace(config=cfg)
engine = SimpleNamespace(
config=cfg,
opc_home=tmp_path / ".opc",
org_engine=org_engine,
talent_market=talent_market,
)
context = OfficeServiceContext(
engine=engine,
agent_store=MagicMock(),
chat_store=MagicMock(),
event_adapter=MagicMock(),
mode_state=ModeState(exec_mode="company", company_profile="corporate"),
)
context.set_active_saved_org_name = AsyncMock()
result = await OrgService(context).saved_create(
organization_name="HKU Research Lab",
members=[
{
"name": "Research Lead",
"responsibility": "Sets research direction.",
"prompt": "Lead with a careful research protocol.",
},
{
"name": "Data Scientist",
"responsibility": "Builds datasets.",
"prompt": "Prefer reproducible data checks.",
"reports_to_index": 0,
},
],
)
config_dir = tmp_path / ".opc" / "config"
assert result.payload["ok"] is True
assert result.payload["organization_id"] == "hku_research_lab"
assert result.payload["roles_count"] == 2
assert result.payload["employees_count"] == 2
assert read_org_index(config_dir) == "hku_research_lab"
assert not (config_dir / "company_index.yaml").exists()
path = config_dir / "company_orgs" / "org_hku_research_lab_config.yaml"
assert path.exists()
payload, _ = load_org_config_payload(config_dir, "hku_research_lab")
assert payload["organization_name"] == "HKU Research Lab"
assert len(payload["roles"]) == 2
assert payload["employees"] == []
assert payload["roles"][0]["id"] == "research_lead"
assert payload["roles"][0]["prompt_refs"] == ["Lead with a careful research protocol."]
assert payload["roles"][1]["reports_to"] == "research_lead"
assert payload["roles"][1]["prompt_refs"] == ["Prefer reproducible data checks."]
validated = apply_org_config_payload_to_config(OPCConfig(), payload, source_path=path)
validate_runnable_org_config(validated, organization_id="hku_research_lab")
from opc.layer2_organization.org_engine import OrgEngine
runtime_org = OrgEngine(validated, opc_home=tmp_path / ".opc")
assert runtime_org.get_role_prompt_context("research_lead") == "Lead with a careful research protocol."
assert runtime_org.get_role_prompt_context("data_scientist") == "Prefer reproducible data checks."
assert engine.config.org.organization_id == "hku_research_lab"
assert engine.config is org_engine.config
assert engine.config is talent_market.config
org_engine.reload_from_config.assert_called_once()
context.set_active_saved_org_name.assert_awaited_once_with("hku_research_lab")
asyncio.run(run())
def test_org_service_saved_create_validates_minimum_members(tmp_path):
from opc.core.config import OPCConfig
from opc.plugins.office_ui.services.context import ModeState, OfficeServiceContext
from opc.plugins.office_ui.services.models import ServiceError
from opc.plugins.office_ui.services.org import OrgService
async def run() -> None:
cfg = OPCConfig()
context = OfficeServiceContext(
engine=SimpleNamespace(
config=cfg,
opc_home=tmp_path / ".opc",
org_engine=SimpleNamespace(config=cfg, reload_from_config=MagicMock()),
talent_market=SimpleNamespace(config=cfg),
),
agent_store=MagicMock(),
chat_store=MagicMock(),
event_adapter=MagicMock(),
mode_state=ModeState(exec_mode="task", company_profile="corporate"),
)
with pytest.raises(ServiceError) as exc:
await OrgService(context).saved_create(
organization_name="One Person Org",
members=[{"name": "Solo"}],
)
assert exc.value.code == "org_members_required"
asyncio.run(run())
def test_org_service_saved_create_validates_member_hierarchy(tmp_path):
from opc.core.config import OPCConfig
from opc.plugins.office_ui.services.context import ModeState, OfficeServiceContext
from opc.plugins.office_ui.services.models import ServiceError
from opc.plugins.office_ui.services.org import OrgService
async def run() -> None:
cfg = OPCConfig()
context = OfficeServiceContext(
engine=SimpleNamespace(
config=cfg,
opc_home=tmp_path / ".opc",
org_engine=SimpleNamespace(config=cfg, reload_from_config=MagicMock()),
talent_market=SimpleNamespace(config=cfg),
),
agent_store=MagicMock(),
chat_store=MagicMock(),
event_adapter=MagicMock(),
mode_state=ModeState(exec_mode="company", company_profile="corporate"),
)
with pytest.raises(ServiceError) as exc:
await OrgService(context).saved_create(
organization_name="Bad Hierarchy Org",
members=[
{"name": "Lead"},
{"name": "Peer", "reports_to_index": 1},
],
)
assert exc.value.code == "invalid_org_member_hierarchy"
asyncio.run(run())
def test_org_saved_create_handler_switches_to_new_org(tmp_path):
from opc.plugins.office_ui import ws_handler as wh
from opc.plugins.office_ui.services.models import ServiceResult
async def run() -> None:
org_service = SimpleNamespace(saved_create=AsyncMock(return_value=ServiceResult({
"ok": True,
"name": "hku_lab",
"organization_id": "hku_lab",
"organization_name": "HKU Lab",
"filename": "org_hku_lab_config.yaml",
"roles_count": 2,
"employees_count": 2,
})))
handler = wh.WSHandler.__new__(wh.WSHandler)
handler._ensure_office_services = MagicMock(return_value=SimpleNamespace(org=org_service))
handler._apply_mode_switch = AsyncMock(return_value=True)
handler._task_preferred_agent = "native"
ws = _FakeWS()
await handler._handle_org_saved_create(ws, {
"organization_name": "HKU Lab",
"members": [{"name": "Lead"}, {"name": "Analyst", "reports_to_index": 0}],
})
org_service.saved_create.assert_awaited_once()
handler._apply_mode_switch.assert_awaited_once_with("org", "custom", "native", org_id="hku_lab")
assert ws.messages[-1]["type"] == "org_saved_create"
assert ws.messages[-1]["payload"]["ok"] is True
assert ws.messages[-1]["payload"]["organization_id"] == "hku_lab"
asyncio.run(run())
def test_org_service_rebinds_replaced_config_to_runtime_components(tmp_path):
from opc.core.config import OPCConfig, RoleConfig
from opc.core.org_config import build_org_config_payload_from_config
from opc.plugins.office_ui.services.context import ModeState, OfficeServiceContext
from opc.plugins.office_ui.services.org import OrgService
async def run() -> None:
original = OPCConfig()
original.org.company_profile = "custom"
original.org.organization_id = "lab"
original.org.organization_name = "Lab"
original.org.company_name = "Lab"
org_engine = SimpleNamespace(config=original, reload_from_config=MagicMock())
talent_market = SimpleNamespace(config=original)
engine = SimpleNamespace(
config=original,
opc_home=tmp_path / ".opc",
org_engine=org_engine,
talent_market=talent_market,
)
context = OfficeServiceContext(
engine=engine,
agent_store=MagicMock(),
chat_store=MagicMock(),
event_adapter=MagicMock(),
mode_state=ModeState(exec_mode="org", company_profile="custom"),
)
context.persist_runtime_config = lambda: None
replacement = OPCConfig()
replacement.org.company_profile = "custom"
replacement.org.organization_id = "lab"
replacement.org.organization_name = "Lab"
replacement.org.company_name = "Lab"
replacement.org.roles = [
RoleConfig(id="director", name="Director", responsibility="Own direction."),
]
payload = build_org_config_payload_from_config(
replacement,
organization_id="lab",
organization_name="Lab",
)
await OrgService(context).import_config(payload)
assert engine.config is org_engine.config
assert engine.config is talent_market.config
assert [role.id for role in engine.config.org.roles] == ["director"]
org_engine.reload_from_config.assert_called()
asyncio.run(run())
def test_saved_list_returns_active_id(monkeypatch, tmp_path):
from opc.core.org_config import write_org_index
from opc.plugins.office_ui import ws_handler as wh
async def run() -> None:
monkeypatch.setattr("opc.core.config._find_project_root", lambda: tmp_path)
config_dir = tmp_path / ".opc" / "config"
path = _write_saved_org(config_dir, "lab", organization_name="HKU Lab", roles_count=2)
saved = yaml.safe_load(path.read_text(encoding="utf-8"))
saved["employees"] = [
{"employee_id": "ava", "name": "Ava Chen", "role_id": "role_0"},
{
"employee_id": "role-0-default-employee",
"name": "Role 0 Default Employee",
"role_id": "role_0",
"metadata": {
"is_default_employee": True,
"auto_created_for_role": "role_0",
"employee_origin": "system_default",
},
},
]
path.write_text(yaml.dump(saved, default_flow_style=False, sort_keys=False), encoding="utf-8")
write_org_index(config_dir, "lab")
handler = wh.WSHandler.__new__(wh.WSHandler)
handler.engine = SimpleNamespace(opc_home=tmp_path / ".opc")
handler.agent_store = _FakeServerStateStore()
ws = _FakeWS()
await handler._handle_org_saved_list(ws, {})
payload = ws.messages[-1]["payload"]
assert payload["active_name"] == "lab"
assert payload["orgs"][0]["organization_id"] == "lab"
assert payload["orgs"][0]["organization_name"] == "HKU Lab"
assert payload["orgs"][0]["filename"] == "org_lab_config.yaml"
assert payload["orgs"][0]["employees_count"] == 1
assert not (config_dir / "company_index.yaml").exists()
asyncio.run(run())
def test_saved_delete_rejects_active_org(monkeypatch, tmp_path):
from opc.core.org_config import write_org_index
from opc.plugins.office_ui import ws_handler as wh
async def run() -> None:
monkeypatch.setattr("opc.core.config._find_project_root", lambda: tmp_path)
config_dir = tmp_path / ".opc" / "config"
path = _write_saved_org(config_dir, "lab", roles_count=2)
write_org_index(config_dir, "lab")
handler = wh.WSHandler.__new__(wh.WSHandler)
handler.engine = SimpleNamespace(opc_home=tmp_path / ".opc")
handler.agent_store = _FakeServerStateStore()
ws = _FakeWS()
await handler._handle_org_saved_delete(ws, {"organization_id": "lab"})
assert ws.messages[-1]["payload"]["ok"] is False
assert ws.messages[-1]["payload"]["error"] == "cannot_delete_active"
assert path.exists()
asyncio.run(run())
def test_startup_restores_active_saved_org_when_org_config_empty(monkeypatch, tmp_path):
from opc.core.config import OPCConfig
from opc.core.org_config import write_org_index
from opc.plugins.office_ui import ws_handler as wh
async def run() -> None:
monkeypatch.setattr("opc.core.config._find_project_root", lambda: tmp_path)
config_dir = tmp_path / ".opc" / "config"
_write_saved_org(config_dir, "lab", roles_count=2)
write_org_index(config_dir, "lab")
cfg = OPCConfig()
cfg.org.company_profile = "custom"
cfg.org.roles = []
handler = wh.WSHandler.__new__(wh.WSHandler)
handler.engine = SimpleNamespace(
config=cfg,
opc_home=tmp_path / ".opc",
org_engine=MagicMock(),
talent_market=MagicMock(),
)
handler.agent_store = _FakeServerStateStore()
handler._exec_mode = "org"
handler._config_lock = _async_cm()
await handler._restore_active_saved_org_if_needed()
assert [role.id for role in handler.engine.config.org.roles] == ["role_0", "role_1"]
assert handler.engine.config.org.organization_id == "lab"
on_disk = yaml.safe_load((config_dir / "company_orgs" / "org_lab_config.yaml").read_text())
assert len(on_disk["roles"]) == 2
assert not (config_dir / "company_index.yaml").exists()
handler.engine.org_engine.reload_from_config.assert_called_once()
asyncio.run(run())