mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-11 06:30:25 +00:00
Shipping lineup: remove Chat, ship Code disabled, gate unshipped personas
Manifests carry ships/group; release builds hide ships:false coworkers (OPENWORKER_UNSHIPPED=1 restores them). Persona detail serves bundle media; staffing gate reads the registry, not the filtered list.
This commit is contained in:
@@ -25,7 +25,8 @@ def test_bundle_registers_with_team_traits(tmp_path):
|
||||
assert reg.get(pid).manifest.team == "worker"
|
||||
|
||||
|
||||
def test_lead_surfaces_workers_do_not(tmp_path):
|
||||
def test_lead_surfaces_workers_do_not(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("OPENWORKER_UNSHIPPED", "1") # teams are ships:false — internal builds
|
||||
reg = _reg(tmp_path)
|
||||
ids = [e["name"] for e in reg.sidebar()]
|
||||
assert "devops-lead" in ids
|
||||
|
||||
@@ -38,7 +38,8 @@ def test_lead_carries_no_execution_tools(tmp_path):
|
||||
assert {"shell", "git"} <= set(reg.get(pid).tools)
|
||||
|
||||
|
||||
def test_workers_never_surface_lead_does(tmp_path):
|
||||
def test_workers_never_surface_lead_does(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("OPENWORKER_UNSHIPPED", "1") # teams are ships:false — internal builds
|
||||
reg = _reg(tmp_path)
|
||||
ids = [e["name"] for e in reg.sidebar()]
|
||||
assert "devsecops-lead" in ids
|
||||
|
||||
@@ -127,6 +127,7 @@ def test_persona_set_default_connection(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
def test_persona_enable_toggle(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("OPENWORKER_UNSHIPPED", "1") # ops is ships:false now
|
||||
mgr = _mgr(tmp_path, monkeypatch)
|
||||
client = TestClient(create_app(mgr))
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Phase 1 gate — persona registry lifecycle (installed → enabled → surfaced + default)."""
|
||||
"""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
|
||||
|
||||
@@ -11,26 +13,62 @@ 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", "chat", "cowork", "ops"} <= set(reg.ids())
|
||||
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_sidebar_defaults_to_surfaced_builtins(tmp_path):
|
||||
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); Chat
|
||||
# stays default-hidden via the surfaced axis. Installed personas remain opt-in.
|
||||
# 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", "code", "ops", "security", "cloud-posture", "dep-audit",
|
||||
"cowork", "ops", "security", "cloud-posture", "dep-audit",
|
||||
"swe-lead", "devsecops-lead", "devops-lead",
|
||||
}
|
||||
assert not any(
|
||||
@@ -43,23 +81,30 @@ def test_sidebar_defaults_to_surfaced_builtins(tmp_path):
|
||||
)
|
||||
assert sidebar[0]["default"] is True
|
||||
# An explicit disable removes a builtin from the picker.
|
||||
reg.set_enabled("code", False)
|
||||
assert "code" not in [e["name"] for e in reg.sidebar()]
|
||||
reg.set_enabled("security", False)
|
||||
assert "security" not in [e["name"] for e in reg.sidebar()]
|
||||
|
||||
|
||||
def test_chat_retired_by_default_but_resolvable(tmp_path):
|
||||
def test_code_ships_disabled_but_recoverable(tmp_path):
|
||||
reg = _reg(tmp_path)
|
||||
# Chat is retired (owner call 2026-08-11): disabled + unsurfaced out of the box,
|
||||
# unlike the other builtins — Coworker covers quick Q&A.
|
||||
assert reg.is_enabled("chat") is False
|
||||
assert reg.is_surfaced("chat") is False
|
||||
assert reg.agent("chat").name == "chat" # live sessions keep resolving
|
||||
# Still recoverable from Settings ▸ Coworkers (enable implies surface).
|
||||
reg.set_enabled("chat", True)
|
||||
assert "chat" in [e["name"] for e in reg.sidebar()]
|
||||
# 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_surface_toggle_filters_picker_but_keeps_resolvable(tmp_path):
|
||||
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()]
|
||||
@@ -98,14 +143,13 @@ def test_agent_resolution(tmp_path):
|
||||
assert reg.agent("does-not-exist").name == reg.default_id()
|
||||
|
||||
|
||||
def test_list_all_carries_workspace_enum(tmp_path):
|
||||
# Post-§16 collapse: workspace derives from family — code → git, knowledge → deliverable
|
||||
# (scratch). Only builder-registered Chat keeps "none". Ops is a scratch persona now.
|
||||
def test_list_all_carries_workspace_enum(tmp_path, internal):
|
||||
# Post-§16 collapse: workspace derives from family — code → git, knowledge →
|
||||
# deliverable (scratch). Ops is a scratch persona now.
|
||||
reg = _reg(tmp_path)
|
||||
ws = {p["id"]: p["workspace"] for p in reg.list_all()}
|
||||
assert ws["code"] == "git"
|
||||
assert ws["cowork"] == "deliverable"
|
||||
assert ws["chat"] == "none"
|
||||
assert ws["ops"] == "deliverable"
|
||||
|
||||
|
||||
|
||||
+16
-20
@@ -60,17 +60,12 @@ def test_chat_completions_openai_shape(tmp_path):
|
||||
def test_agents_and_memory_rest(tmp_path):
|
||||
client = _client(tmp_path, [])
|
||||
agents = client.get("/v1/agents").json()["agents"]
|
||||
# The picker lists enabled+surfaced personas — builtins ship enabled (UX-029);
|
||||
# Chat stays default-hidden via the surfaced axis. The security bundles (Phase C)
|
||||
# ship in the picker out of the box.
|
||||
# The picker lists enabled+surfaced personas. Release lineup (owner 2026-08-21):
|
||||
# OpenWorker + the security bundles; Code ships disabled, Chat is gone, and
|
||||
# ships:false personas (teams, ops, design) need OPENWORKER_UNSHIPPED=1.
|
||||
names = [a["name"] for a in agents]
|
||||
assert names[0] == "cowork"
|
||||
# Leads surface (they're how a user starts a team); team WORKERS never do —
|
||||
# they're staffed by a lead, not started solo.
|
||||
assert set(names) == {
|
||||
"cowork", "code", "ops", "security", "cloud-posture", "dep-audit",
|
||||
"swe-lead", "devsecops-lead", "devops-lead",
|
||||
}
|
||||
assert set(names) == {"cowork", "security", "cloud-posture", "dep-audit"}
|
||||
assert "skills" in client.get("/v1/skills").json() # catalog (may be empty)
|
||||
|
||||
added = client.post("/v1/memory", json={"content": "prefer pathlib"}).json()
|
||||
@@ -99,17 +94,17 @@ def test_disable_persona_archives_its_sessions(tmp_path):
|
||||
)
|
||||
)
|
||||
|
||||
mk("chat-a", "chat")
|
||||
mk("chat-b", "chat")
|
||||
mk("chat-old", "chat")
|
||||
mk("chat-a", "code")
|
||||
mk("chat-b", "code")
|
||||
mk("chat-old", "code")
|
||||
store.set_flags(
|
||||
"chat-old", archived=True
|
||||
) # already archived — must not be re-counted
|
||||
mk("cowork-a", "cowork")
|
||||
mk("__run__r1", "chat") # internal automation thread — never touched
|
||||
mk("__run__r1", "code") # internal automation thread — never touched
|
||||
|
||||
client = TestClient(create_app(manager))
|
||||
body = client.post("/v1/personas/chat", json={"enabled": False}).json()
|
||||
body = client.post("/v1/personas/code", json={"enabled": False}).json()
|
||||
assert body["ok"] is True
|
||||
assert body["archived_sessions"] == 2
|
||||
assert store.load("chat-a").archived and store.load("chat-b").archived
|
||||
@@ -121,8 +116,8 @@ def test_disable_persona_archives_its_sessions(tmp_path):
|
||||
assert store.load("chat-a").archived
|
||||
|
||||
# The dedicated §5/§8 enable route shares the same semantic.
|
||||
mk("chat-c", "chat")
|
||||
client.post("/v1/personas/chat/enable", json={"enabled": False})
|
||||
mk("chat-c", "code")
|
||||
client.post("/v1/personas/code/enable", json={"enabled": False})
|
||||
assert store.load("chat-c").archived
|
||||
|
||||
|
||||
@@ -869,18 +864,19 @@ def test_ws_with_workspace_query(tmp_path):
|
||||
assert "turn_end" in _drain(ws)
|
||||
|
||||
|
||||
def test_ws_chat_agent_needs_no_workspace(tmp_path):
|
||||
def test_ws_removed_agent_id_falls_back_to_default(tmp_path):
|
||||
# Chat is removed (owner 2026-08-21): a stored session or deep link carrying
|
||||
# agent=chat resolves to the default persona instead of erroring.
|
||||
manager = SessionManager(
|
||||
workspace=None,
|
||||
data_dir=tmp_path,
|
||||
provider=ScriptedProvider([_text("hi from chat")]),
|
||||
provider=ScriptedProvider([_text("hi")]),
|
||||
)
|
||||
client = TestClient(create_app(manager))
|
||||
with client.websocket_connect("/ws/session/chat1?agent=chat") as ws:
|
||||
ready = ws.receive_json()
|
||||
assert ready["type"] == "ready"
|
||||
assert ready["data"]["agent"] == "chat"
|
||||
assert ready["data"]["workspace"] is None
|
||||
assert ready["data"]["agent"] == "cowork"
|
||||
ws.send_json({"type": "user_message", "text": "hello"})
|
||||
assert "turn_end" in _drain(ws)
|
||||
|
||||
|
||||
@@ -45,8 +45,9 @@ def test_chat_agent_has_no_workspace_tools():
|
||||
|
||||
|
||||
def test_get_agent_fallback():
|
||||
assert get_agent("chat").name == "chat"
|
||||
# Unknown ids fall back to the default persona (Cowork), per the persona registry.
|
||||
# Chat is removed (owner 2026-08-21): its id, like any unknown id, falls back to
|
||||
# the default persona per the registry.
|
||||
assert get_agent("chat").name == "cowork"
|
||||
assert get_agent("nope").name == "cowork"
|
||||
|
||||
|
||||
|
||||
@@ -272,10 +272,10 @@ def test_engine_catalog_respects_settings_disable(tmp_path):
|
||||
client.patch("/v1/skills/hidden", json={"enabled": False})
|
||||
|
||||
from coworker.agent import build_engine
|
||||
from coworker.agents.registry import get_agent
|
||||
from coworker.agents.chat import chat_agent
|
||||
|
||||
engine = build_engine(
|
||||
agent=get_agent("chat"),
|
||||
agent=chat_agent(), # workspace-free agent (persona retired; builder remains)
|
||||
provider=ScriptedProvider(),
|
||||
skill_filter=lambda: manager.effective_skill_names("s1"),
|
||||
)
|
||||
|
||||
@@ -149,11 +149,11 @@ def test_live_load_skill_semantics(manager):
|
||||
disable applies to RUNNING sessions; delete ≡ disable to the model);
|
||||
· the ONLY thing that persists is what a conversation already loaded (history)."""
|
||||
from coworker.agent import build_engine
|
||||
from coworker.agents.registry import get_agent
|
||||
from coworker.agents.chat import chat_agent
|
||||
|
||||
_skill(manager.skill_store.global_dir, "early", body="early body")
|
||||
engine = build_engine(
|
||||
agent=get_agent("chat"),
|
||||
agent=chat_agent(), # workspace-free agent (persona retired; builder remains)
|
||||
provider=ScriptedProvider(),
|
||||
skill_filter=lambda: manager.effective_skill_names("s1"),
|
||||
)
|
||||
@@ -198,12 +198,12 @@ def test_disable_countermand_for_loaded_skills(manager):
|
||||
import json as _json
|
||||
|
||||
from coworker.agent import build_engine
|
||||
from coworker.agents.registry import get_agent
|
||||
from coworker.agents.chat import chat_agent
|
||||
|
||||
_skill(manager.skill_store.global_dir, "used-one", body="used body")
|
||||
_skill(manager.skill_store.global_dir, "unused-one", body="never loaded")
|
||||
engine = build_engine(
|
||||
agent=get_agent("chat"),
|
||||
agent=chat_agent(), # workspace-free agent (persona retired; builder remains)
|
||||
provider=ScriptedProvider(),
|
||||
skill_filter=lambda: manager.effective_skill_names("s1"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user