feat(ui,api): strip board redesign - island, rail by day, context view, server search, vault graph
This commit is contained in:
+89
-1
@@ -29,7 +29,7 @@ from beaver_gateway.frontends.api import ApiFrontend
|
||||
from beaver_gateway.frontends.api.frontend import build_app as build_api
|
||||
from beaver_gateway.frontends.base import Frontend, GatewayRuntime
|
||||
from beaver_gateway.frontends.root import build_root_app
|
||||
from beaver_gateway.storage.models import RateLimit, Usage
|
||||
from beaver_gateway.storage.models import RateLimit, TranscriptEntry, Usage
|
||||
|
||||
TOKEN = "tok"
|
||||
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
|
||||
@@ -525,3 +525,91 @@ async def test_bind_without_a_window_materializes_one(world: World) -> None:
|
||||
headers=HEADERS,
|
||||
)
|
||||
assert unknown.status_code == 400
|
||||
|
||||
|
||||
async def test_search_finds_conversations_and_memory_files(world: World) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="beaver-mem-"))
|
||||
(root / "notes").mkdir()
|
||||
(root / "notes" / "studio.md").write_text("# studio\nthe kiln is warm\n")
|
||||
api = Api(world, memory_root=root)
|
||||
conv = await world.conversations.create(kind="master", agent="a", origin="test")
|
||||
await world.conversations.set_title(conv, "kiln schedule")
|
||||
other = await world.conversations.create(kind="deep", agent="d", origin="test")
|
||||
await world.conversations.post(other, "go")
|
||||
await world.settle(other, 1)
|
||||
other = await world.conversations.get(other.external_id)
|
||||
assert other is not None and other.session_id is not None
|
||||
async with world.db.session() as db:
|
||||
db.add(
|
||||
TranscriptEntry(
|
||||
project_key="p",
|
||||
session_id=other.session_id,
|
||||
seq=0,
|
||||
entry={"type": "assistant", "message": {"content": "warm kiln"}},
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
found = await api.get("/search", {"q": "kiln"})
|
||||
assert {c["id"] for c in found["conversations"]} == {
|
||||
conv.external_id,
|
||||
other.external_id,
|
||||
}
|
||||
assert found["files"] == [
|
||||
{"path": "notes/studio.md", "line": 2, "snippet": "the kiln is warm"}
|
||||
]
|
||||
short = await api.http.get("/search", params={"q": "k"}, headers=HEADERS)
|
||||
assert short.status_code == 400
|
||||
|
||||
|
||||
async def test_context_endpoint_reports_prompt_and_files(world: World) -> None:
|
||||
api = Api(world)
|
||||
conv = await world.conversations.create(kind="master", agent="a", origin="test")
|
||||
await world.conversations.post(conv, "go")
|
||||
await world.settle(conv, 1)
|
||||
conv = await world.conversations.get(conv.external_id)
|
||||
assert conv is not None and conv.session_id is not None
|
||||
await world.store.append(
|
||||
world.key(conv.session_id),
|
||||
build_entries(
|
||||
[
|
||||
{"role": "user", "content": "go"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "t1",
|
||||
"name": "Read",
|
||||
"input": {"file_path": str(world.root / "note.md")},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "tool_result", "tool_use_id": "t1", "content": "x"}
|
||||
],
|
||||
},
|
||||
],
|
||||
session_id=conv.session_id,
|
||||
cwd=str(world.root),
|
||||
model="m",
|
||||
),
|
||||
)
|
||||
ctx = await api.get(f"/conversations/{conv.external_id}/context", {"prompt": 1})
|
||||
assert ctx["agent"]["name"] == "a"
|
||||
assert ctx["prompt"]["granules"][0]["bytes"] == len("hi")
|
||||
assert ctx["prompt_text"] == "hi"
|
||||
assert ctx["files"] == [
|
||||
{
|
||||
"path": "note.md",
|
||||
"reads": 1,
|
||||
"writes": 0,
|
||||
"other": 0,
|
||||
"last_at": ctx["files"][0]["last_at"],
|
||||
}
|
||||
]
|
||||
assert ctx["tool_counts"] == {"Read": 1}
|
||||
assert ctx["turns"] == 1
|
||||
graph = await api.http.get("/vault/graph", headers=HEADERS)
|
||||
assert graph.status_code == 404
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from beaver_gateway.agents.claude import ClaudeAgent, Prompts
|
||||
from beaver_gateway.conversations.context import compose, files_touched, granules
|
||||
from beaver_gateway.vault.links import LinkIndex
|
||||
|
||||
|
||||
def entry(name: str, tool_input: dict, stamp: str = "2026-09-02T10:00:00Z") -> dict:
|
||||
return {
|
||||
"type": "assistant",
|
||||
"timestamp": stamp,
|
||||
"message": {
|
||||
"content": [{"type": "tool_use", "name": name, "input": tool_input}]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_files_touched_groups_by_path_relative_to_cwd() -> None:
|
||||
cwd = Path("/vault")
|
||||
files, counts = files_touched(
|
||||
[
|
||||
entry("Read", {"file_path": "/vault/a.md"}, "2026-09-02T10:00:00Z"),
|
||||
entry("Edit", {"file_path": "/vault/a.md"}, "2026-09-02T10:05:00Z"),
|
||||
entry("Grep", {"pattern": "x", "path": "/vault"}),
|
||||
entry("Bash", {"command": "ls"}),
|
||||
entry("Read", {"file_path": "/elsewhere/b.md"}, "2026-09-02T09:00:00Z"),
|
||||
],
|
||||
cwd=cwd,
|
||||
)
|
||||
assert [f["path"] for f in files] == ["a.md", "/elsewhere/b.md"]
|
||||
assert files[0] == {
|
||||
"path": "a.md",
|
||||
"reads": 1,
|
||||
"writes": 1,
|
||||
"other": 0,
|
||||
"last_at": "2026-09-02T10:05:00Z",
|
||||
}
|
||||
assert counts == {"Read": 2, "Edit": 1, "Grep": 1, "Bash": 1}
|
||||
|
||||
|
||||
def test_compose_reads_granules_and_skills() -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="beaver-ctx-"))
|
||||
(root / "voice.md").write_text("be brief " * 40)
|
||||
(root / "env.md").write_text("you are in the master thread")
|
||||
skills = root / "skills" / "common"
|
||||
(skills / "ops").mkdir(parents=True)
|
||||
(skills / "ops" / "SKILL.md").write_text(
|
||||
"---\nname: ops\ndescription: servers\n---\n"
|
||||
)
|
||||
agent = ClaudeAgent(
|
||||
name="a",
|
||||
model="m",
|
||||
cwd=root,
|
||||
prompts=Prompts(master=(("voice", root / "voice.md"), root / "env.md")),
|
||||
skill_sets=(skills,),
|
||||
gateway_tools=("say",),
|
||||
)
|
||||
out = compose(
|
||||
agent,
|
||||
"master",
|
||||
[entry("Read", {"file_path": str(root / "env.md")})],
|
||||
context_tokens=50_000,
|
||||
)
|
||||
assert [g["path"] for g in out["prompt"]["granules"]] == ["voice.md", "env.md"]
|
||||
assert out["prompt"]["granules"][0]["tag"] == "voice"
|
||||
assert out["prompt"]["tokens_est"] > 0
|
||||
assert out["skills"] == [
|
||||
{
|
||||
"set": "common",
|
||||
"path": "skills/common",
|
||||
"skills": [
|
||||
{"name": "ops", "description": "servers", "path": "skills/common/ops"}
|
||||
],
|
||||
}
|
||||
]
|
||||
assert out["tools"]["gateway"] == ["say"]
|
||||
assert out["files"][0]["path"] == "env.md"
|
||||
assert out["history_tokens_est"] < 50_000
|
||||
assert granules(agent, "deep")["granules"] == []
|
||||
|
||||
|
||||
def test_link_index_resolves_stems_and_neighbours() -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="beaver-links-"))
|
||||
(root / "people").mkdir()
|
||||
(root / "people" / "Marta.md").write_text("works at [[Studio]] with [[Ilya|him]]")
|
||||
(root / "Studio.md").write_text("see [[people/Marta]] and [[nowhere]]")
|
||||
(root / "Ilya.md").write_text("plain")
|
||||
(root / ".obsidian").mkdir()
|
||||
(root / ".obsidian" / "x.md").write_text("[[Studio]]")
|
||||
index = LinkIndex(root)
|
||||
graph = index.neighbours([str(root / "people" / "Marta.md")])
|
||||
nodes = {n["path"]: n for n in graph["nodes"]}
|
||||
assert nodes["people/Marta.md"]["touched"] is True
|
||||
assert set(nodes) == {"people/Marta.md", "Studio.md", "Ilya.md"}
|
||||
assert {(e["from"], e["to"]) for e in graph["edges"]} == {
|
||||
("people/Marta.md", "Studio.md"),
|
||||
("people/Marta.md", "Ilya.md"),
|
||||
("Studio.md", "people/Marta.md"),
|
||||
}
|
||||
missing = index.neighbours(["gone.md"])
|
||||
assert missing["nodes"] == [
|
||||
{"path": "gone.md", "title": "gone", "touched": True, "exists": False}
|
||||
]
|
||||
Reference in New Issue
Block a user