feat: OpenMesh 基础平台与 MD/PDF 转换技能
- 后端: coworker 智能体框架, WS API, 文件上传, 附件处理 - 前端: Open WebUI, 文件全量走 upload API (含 MD/TXT/JSON 等文本类) - 技能: md-to-office (pandoc + wkhtmltopdf) - 修复: 上传文件路径丢失, Agent 搜索浪费, 输出文件跑到 uploads/ - 打包: PyInstaller one-dir, 预打包 pandoc/wkhtmltopdf/chromium
This commit is contained in:
178
tests/test_project_bindings.py
Normal file
178
tests/test_project_bindings.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user