mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-01 15:36:08 +00:00
Worktrees of one repo share one project; one-time path-to-git re-key (board chain honestly recomputed). Session bindings persist like team ties; grant-time presence notice; menu/binding API.
179 lines
5.9 KiB
Python
179 lines
5.9 KiB
Python
"""Pass 20 — project bindings over the manager/API surface (UX-044 backend)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from coworker.memory.base import Scope
|
|
from coworker.providers import ModelCapabilities, ProviderClient
|
|
from coworker.server import SessionManager, create_app
|
|
from coworker.sessions import SessionRecord
|
|
|
|
|
|
class _StubProvider(ProviderClient):
|
|
def complete(self, **kwargs): # pragma: no cover
|
|
raise NotImplementedError
|
|
|
|
def capabilities(self, model):
|
|
return ModelCapabilities()
|
|
|
|
|
|
def _fixture(tmp_path):
|
|
manager = SessionManager(workspace=tmp_path, provider=_StubProvider())
|
|
return TestClient(create_app(manager)), manager
|
|
|
|
|
|
def _seed_session(manager, sid: str, workspace: Path) -> None:
|
|
manager.session_store.save(
|
|
SessionRecord(
|
|
session_id=sid,
|
|
workspace=str(workspace),
|
|
model="m",
|
|
mode="auto",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
)
|
|
)
|
|
|
|
|
|
def test_project_menu_derived_and_named(tmp_path):
|
|
client, manager = _fixture(tmp_path)
|
|
ws = tmp_path / "proj"
|
|
ws.mkdir()
|
|
_seed_session(manager, "s1", ws)
|
|
|
|
menu = client.get("/v1/sessions/s1/project-menu", params={"kind": "memory"}).json()
|
|
assert menu["bound"] is None
|
|
assert menu["derived"]["key"] == str(ws.resolve())
|
|
assert menu["derived"]["kind"] == "folder"
|
|
assert menu["named"] == []
|
|
|
|
named = client.post(
|
|
"/v1/sessions/s1/project-name", json={"kind": "memory", "name": "myproj"}
|
|
).json()
|
|
assert named["ok"] and named["key"] == str(ws.resolve())
|
|
menu = client.get("/v1/sessions/s1/project-menu", params={"kind": "memory"}).json()
|
|
assert [n["name"] for n in menu["named"]] == ["myproj"]
|
|
|
|
|
|
def test_binding_swaps_memory_key(tmp_path):
|
|
client, manager = _fixture(tmp_path)
|
|
ws = tmp_path / "scratch"
|
|
ws.mkdir()
|
|
other = tmp_path / "real"
|
|
other.mkdir()
|
|
_seed_session(manager, "s1", ws)
|
|
|
|
manager.session_store.names().name_current(
|
|
"memory", "openworker", str(other.resolve())
|
|
)
|
|
manager.memory_store.add(
|
|
"the real fact", scope=Scope.WORKSPACE, workspace=str(other.resolve())
|
|
)
|
|
|
|
put = client.put(
|
|
"/v1/sessions/s1/bindings", json={"kind": "memory", "name": "openworker"}
|
|
).json()
|
|
assert put["ok"] and put["bindings"] == {"memory": "openworker"}
|
|
|
|
record = manager.session_store.load("s1")
|
|
key = manager._memory_key_for(record, record.workspace)
|
|
assert key == str(other.resolve())
|
|
|
|
# unbind → back to derivation
|
|
put = client.put("/v1/sessions/s1/bindings", json={"kind": "memory"}).json()
|
|
assert put["ok"] and put["bindings"] == {}
|
|
record = manager.session_store.load("s1")
|
|
assert manager._memory_key_for(record, record.workspace) == str(ws.resolve())
|
|
|
|
|
|
def test_binding_rejects_unknown_name_and_kind(tmp_path):
|
|
client, manager = _fixture(tmp_path)
|
|
_seed_session(manager, "s1", tmp_path)
|
|
assert not client.put(
|
|
"/v1/sessions/s1/bindings", json={"kind": "memory", "name": "ghost"}
|
|
).json()["ok"]
|
|
assert not client.put(
|
|
"/v1/sessions/s1/bindings", json={"kind": "nope", "name": "x"}
|
|
).json()["ok"]
|
|
|
|
|
|
def test_bindings_survive_turn_save(tmp_path):
|
|
"""The per-turn upsert must not clobber bindings (same doctrine as team)."""
|
|
client, manager = _fixture(tmp_path)
|
|
ws = tmp_path / "p"
|
|
ws.mkdir()
|
|
_seed_session(manager, "s1", ws)
|
|
manager.session_store.names().name_current("board", "b1", "/somewhere")
|
|
assert client.put(
|
|
"/v1/sessions/s1/bindings", json={"kind": "board", "name": "b1"}
|
|
).json()["ok"]
|
|
|
|
# simulate a turn-save rebuilding the record WITHOUT bindings
|
|
rec = manager.session_store.load("s1")
|
|
manager.session_store.save(
|
|
SessionRecord(
|
|
session_id="s1",
|
|
workspace=rec.workspace,
|
|
model=rec.model,
|
|
mode=rec.mode,
|
|
messages=rec.messages,
|
|
)
|
|
)
|
|
assert manager.session_store.load("s1").bindings == {"board": "b1"}
|
|
|
|
|
|
def test_board_space_follows_binding(tmp_path):
|
|
client, manager = _fixture(tmp_path)
|
|
ws = tmp_path / "p"
|
|
ws.mkdir()
|
|
_seed_session(manager, "s1", ws)
|
|
manager.session_store.names().name_current("board", "shared", "/the/shared/space")
|
|
client.put("/v1/sessions/s1/bindings", json={"kind": "board", "name": "shared"})
|
|
assert manager._board_space("s1") == "/the/shared/space"
|
|
|
|
|
|
def test_worktree_sessions_share_memory_key(tmp_path):
|
|
_, manager = _fixture(tmp_path)
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
for args in (["init", "-q"],):
|
|
subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True)
|
|
(repo / "f").write_text("x")
|
|
subprocess.run(
|
|
["git", "-c", "user.name=t", "-c", "user.email=t@t", "add", "f"],
|
|
cwd=repo, check=True, capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "i"],
|
|
cwd=repo, check=True, capture_output=True,
|
|
)
|
|
wt = tmp_path / "wt"
|
|
subprocess.run(
|
|
["git", "worktree", "add", "-q", str(wt)],
|
|
cwd=repo, check=True, capture_output=True,
|
|
)
|
|
_seed_session(manager, "a", repo)
|
|
_seed_session(manager, "b", wt)
|
|
ra, rb = manager.session_store.load("a"), manager.session_store.load("b")
|
|
ka = manager._memory_key_for(ra, ra.workspace)
|
|
kb = manager._memory_key_for(rb, rb.workspace)
|
|
assert ka == kb == str(repo.resolve())
|
|
|
|
|
|
def test_grant_notice_fires_on_existing_memory(tmp_path):
|
|
_, manager = _fixture(tmp_path)
|
|
granted = tmp_path / "known"
|
|
granted.mkdir()
|
|
manager.memory_store.add(
|
|
"fact", scope=Scope.WORKSPACE, workspace=str(granted.resolve())
|
|
)
|
|
notice = manager._project_notice(str(granted))
|
|
assert notice is not None and "project memory (1 entries)" in notice
|
|
|
|
empty = tmp_path / "fresh"
|
|
empty.mkdir()
|
|
assert manager._project_notice(str(empty)) is None
|