mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-03 04:49:26 +00:00
Brief lives in project memory; pushed wakes share the sweep pipeline; case-ledger dedup. Board stays the lead's substrate; outward actions ride the normal approval settings.
160 lines
6.3 KiB
Python
160 lines
6.3 KiB
Python
"""Phase 1 gate — persona registry lifecycle (installed → enabled → surfaced + default),
|
|
plus the shipping lineup (owner calls 2026-08-21): Chat removed, Code disabled by default,
|
|
ships:false personas hidden outside internal builds (OPENWORKER_UNSHIPPED=1)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from coworker.personas.registry import DEFAULT_PERSONA_ID, PersonaRegistry
|
|
|
|
|
|
def _reg(tmp_path) -> PersonaRegistry:
|
|
return PersonaRegistry(state_path=tmp_path / "personas.json")
|
|
|
|
|
|
@pytest.fixture
|
|
def internal(monkeypatch):
|
|
"""Internal build: ships:false personas (teams, ops, design…) are visible."""
|
|
monkeypatch.setenv("OPENWORKER_UNSHIPPED", "1")
|
|
|
|
|
|
def test_builtins_present(tmp_path):
|
|
reg = _reg(tmp_path)
|
|
assert {"code", "cowork", "ops"} <= set(reg.ids())
|
|
assert "chat" not in reg.ids() # removed entirely, not just disabled
|
|
assert reg.get("ops").builtin is True
|
|
# Ops came from a markdown manifest; Code from a builder.
|
|
assert reg.get("ops").manifest is not None
|
|
assert reg.get("code").manifest is None
|
|
|
|
|
|
def test_release_lineup(tmp_path, monkeypatch):
|
|
# A release build (no flag) offers exactly OpenWorker + the security coworkers;
|
|
# Code is listed in Settings but disabled + unsurfaced (the recovery path).
|
|
monkeypatch.delenv("OPENWORKER_UNSHIPPED", raising=False)
|
|
reg = _reg(tmp_path)
|
|
assert [e["name"] for e in reg.sidebar()] == [
|
|
"cowork", "cloud-posture", "dep-audit", "security",
|
|
]
|
|
listed = {p["id"]: p for p in reg.list_all()}
|
|
assert set(listed) == {"cowork", "code", "cloud-posture", "dep-audit", "security"}
|
|
assert listed["code"]["enabled"] is False and listed["code"]["surfaced"] is False
|
|
assert listed["cloud-posture"]["group"] == "security"
|
|
assert listed["cowork"]["group"] == "general"
|
|
# Enabling Code from Settings puts it in the picker (enable implies surface).
|
|
reg.set_enabled("code", True)
|
|
assert "code" in [e["name"] for e in reg.sidebar()]
|
|
|
|
|
|
def test_unshipped_hidden_unless_enabled(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("OPENWORKER_UNSHIPPED", raising=False)
|
|
reg = _reg(tmp_path)
|
|
assert "swe-lead" not in {p["id"] for p in reg.list_all()}
|
|
# Still resolvable (a session born on it keeps working)…
|
|
assert reg.agent("swe-lead").name == "swe-lead"
|
|
# …and an explicit user enable (made on an internal build) keeps it visible.
|
|
reg.set_enabled("swe-lead", True)
|
|
assert "swe-lead" in {p["id"] for p in reg.list_all()}
|
|
|
|
|
|
def test_sidebar_defaults_to_surfaced_builtins(tmp_path, internal):
|
|
reg = _reg(tmp_path)
|
|
sidebar = reg.sidebar()
|
|
ids = [e["name"] for e in sidebar]
|
|
# Built-ins ship enabled (UX-029: the coworker picker is their front door) except
|
|
# Code (owner 2026-08-21). Installed personas remain opt-in.
|
|
assert ids[0] == "cowork"
|
|
# Leads surface (the user's entry to a team — "the team IS the lead"); team
|
|
# workers never do.
|
|
assert set(ids) == {
|
|
"cowork", "ops", "security", "cloud-posture", "dep-audit",
|
|
"swe-lead", "devsecops-lead", "devops-lead", "triage-lead",
|
|
}
|
|
assert not any(
|
|
i in ids
|
|
for i in (
|
|
"swe-worker", "design-worker", "test-worker",
|
|
"appsec-worker", "secrets-worker", "posture-worker",
|
|
"logs-worker", "infra-worker", "change-worker",
|
|
)
|
|
)
|
|
assert sidebar[0]["default"] is True
|
|
# An explicit disable removes a builtin from the picker.
|
|
reg.set_enabled("security", False)
|
|
assert "security" not in [e["name"] for e in reg.sidebar()]
|
|
|
|
|
|
def test_code_ships_disabled_but_recoverable(tmp_path):
|
|
reg = _reg(tmp_path)
|
|
# Code ships disabled + unsurfaced (owner call 2026-08-21): OpenWorker leads the
|
|
# launch, Code stays one checkbox away as the plain work-in-my-repo persona.
|
|
assert reg.is_enabled("code") is False
|
|
assert reg.is_surfaced("code") is False
|
|
assert reg.agent("code").name == "code" # live sessions keep resolving
|
|
reg.set_enabled("code", True)
|
|
assert "code" in [e["name"] for e in reg.sidebar()]
|
|
|
|
|
|
def test_chat_gone_resolves_to_default(tmp_path):
|
|
reg = _reg(tmp_path)
|
|
# Chat is removed outright; a stray persona=chat session id falls back to the
|
|
# default persona instead of erroring.
|
|
assert reg.get("chat") is None
|
|
assert reg.agent("chat").name == reg.default_id()
|
|
|
|
|
|
def test_surface_toggle_filters_picker_but_keeps_resolvable(tmp_path, internal):
|
|
reg = _reg(tmp_path)
|
|
reg.set_surfaced("ops", False)
|
|
assert "ops" not in [e["name"] for e in reg.sidebar()]
|
|
# Still installed + still resolvable (a session already on Ops keeps working).
|
|
assert "ops" in reg.ids()
|
|
assert reg.agent("ops").name == "ops"
|
|
assert any(p["id"] == "ops" and not p["surfaced"] for p in reg.list_all())
|
|
|
|
|
|
def test_disable_default_falls_back(tmp_path):
|
|
reg = _reg(tmp_path)
|
|
assert reg.default_id() == DEFAULT_PERSONA_ID # cowork
|
|
reg.set_enabled("ops", True) # another persona must be enabled to fall back to
|
|
reg.set_enabled("cowork", False)
|
|
# Cowork off → default resolves to another enabled persona, not cowork.
|
|
assert reg.default_id() != "cowork"
|
|
# Unknown / unspecified persona falls back to the (new) default, which is enabled.
|
|
fallback = reg.agent(None)
|
|
assert reg.is_enabled(fallback.name)
|
|
|
|
|
|
def test_set_default_enables_and_persists(tmp_path):
|
|
reg = _reg(tmp_path)
|
|
reg.set_default("ops")
|
|
assert reg.default_id() == "ops" and reg.is_enabled("ops")
|
|
# New instance reads persisted state.
|
|
reg2 = _reg(tmp_path)
|
|
assert reg2.default_id() == "ops"
|
|
|
|
|
|
def test_agent_resolution(tmp_path):
|
|
reg = _reg(tmp_path)
|
|
assert reg.agent("ops").requires_folder is False
|
|
assert reg.agent("code").requires_folder is True
|
|
# Unknown id → default persona.
|
|
assert reg.agent("does-not-exist").name == reg.default_id()
|
|
|
|
|
|
def test_list_all_carries_requires_folder(tmp_path, internal):
|
|
# The workspace enum collapsed into the requires_folder trait
|
|
# (workspace-scratch-design.md): Code gates a folder; scratch personas don't.
|
|
reg = _reg(tmp_path)
|
|
gated = {p["id"]: p["requires_folder"] for p in reg.list_all()}
|
|
assert gated["code"] is True
|
|
assert gated["cowork"] is False
|
|
assert gated["ops"] is False
|
|
|
|
|
|
def test_set_unknown_persona_raises(tmp_path):
|
|
reg = _reg(tmp_path)
|
|
with pytest.raises(KeyError):
|
|
reg.set_enabled("ghost", False)
|