feat(core,backends,frontends,storage): conversations, inject queue, session pool, gateway mcp tools, api frontend
This commit is contained in:
@@ -93,6 +93,9 @@ class FakeClient:
|
||||
async def query(self, prompt: str) -> None:
|
||||
self.prompts.append(prompt)
|
||||
|
||||
async def interrupt(self) -> None:
|
||||
self.interrupted = True
|
||||
|
||||
async def receive_response(self):
|
||||
start = {"type": "message_start", "message": {}}
|
||||
yield StreamEvent(uuid="u", session_id="s", event=start)
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
import asyncio
|
||||
import tempfile
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
InMemorySessionStore,
|
||||
ResultMessage,
|
||||
StreamEvent,
|
||||
TextBlock,
|
||||
project_key_for_directory,
|
||||
)
|
||||
|
||||
from beaver_gateway.agents.claude import ClaudeAgent, ClaudeOptions
|
||||
from beaver_gateway.backends.claude_sdk import ClaudeSdkBackend
|
||||
from beaver_gateway.core.bus import EventBus
|
||||
from beaver_gateway.core.conversations import Conversations, ConversationTexts, parse_at
|
||||
from beaver_gateway.core.registry import AgentRegistry
|
||||
from beaver_gateway.core.sessions import SessionPool
|
||||
from beaver_gateway.core.transcript import (
|
||||
build_entries,
|
||||
close_open_tool_uses,
|
||||
open_tool_uses,
|
||||
render_messages,
|
||||
strip_tool_entries,
|
||||
window_entries,
|
||||
)
|
||||
from beaver_gateway.storage import Database
|
||||
from beaver_gateway.storage.models import InjectQueueItem
|
||||
|
||||
|
||||
class ScriptedClient:
|
||||
instances: list["ScriptedClient"] = []
|
||||
hold: asyncio.Event | None = None
|
||||
|
||||
def __init__(self, options: Any) -> None:
|
||||
self.options = options
|
||||
self.prompts: list[str] = []
|
||||
self.session_id = options.resume or str(uuid.uuid4())
|
||||
self.interrupted = False
|
||||
self.connected = False
|
||||
ScriptedClient.instances.append(self)
|
||||
|
||||
async def connect(self) -> None:
|
||||
self.connected = True
|
||||
|
||||
async def query(self, prompt: str) -> None:
|
||||
self.prompts.append(prompt)
|
||||
|
||||
async def receive_response(self):
|
||||
prompt = self.prompts[-1]
|
||||
yield StreamEvent(
|
||||
uuid="u", session_id="s", event={"type": "message_start", "message": {}}
|
||||
)
|
||||
yield StreamEvent(
|
||||
uuid="u",
|
||||
session_id="s",
|
||||
event={
|
||||
"type": "content_block_start",
|
||||
"index": 0,
|
||||
"content_block": {"type": "text", "text": ""},
|
||||
},
|
||||
)
|
||||
yield StreamEvent(
|
||||
uuid="u",
|
||||
session_id="s",
|
||||
event={
|
||||
"type": "content_block_delta",
|
||||
"index": 0,
|
||||
"delta": {"type": "text_delta", "text": f"ok:{prompt}"},
|
||||
},
|
||||
)
|
||||
yield StreamEvent(
|
||||
uuid="u", session_id="s", event={"type": "content_block_stop", "index": 0}
|
||||
)
|
||||
yield AssistantMessage(content=[TextBlock(text=f"ok:{prompt}")], model="m")
|
||||
hold = ScriptedClient.hold
|
||||
if hold is not None and not self.interrupted:
|
||||
await hold.wait()
|
||||
cut = self.interrupted
|
||||
self.interrupted = False
|
||||
yield ResultMessage(
|
||||
subtype="error_during_execution" if cut else "success",
|
||||
duration_ms=1,
|
||||
duration_api_ms=1,
|
||||
is_error=cut,
|
||||
num_turns=1,
|
||||
session_id=self.session_id,
|
||||
stop_reason="end_turn",
|
||||
total_cost_usd=0.0,
|
||||
usage={"input_tokens": 1, "output_tokens": 1},
|
||||
)
|
||||
|
||||
async def interrupt(self) -> None:
|
||||
self.interrupted = True
|
||||
if ScriptedClient.hold is not None:
|
||||
ScriptedClient.hold.set()
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
self.connected = False
|
||||
|
||||
|
||||
class World:
|
||||
def __init__(self, root: Path) -> None:
|
||||
ScriptedClient.instances.clear()
|
||||
ScriptedClient.hold = None
|
||||
self.root = root
|
||||
self.db = Database(f"sqlite:///{root / 'w.db'}")
|
||||
self.store = InMemorySessionStore()
|
||||
self.agent = ClaudeAgent(
|
||||
name="a",
|
||||
model="m",
|
||||
system_prompt="hi",
|
||||
cwd=root,
|
||||
gateway_tools=("say",),
|
||||
options=ClaudeOptions(effort="low"),
|
||||
)
|
||||
self.pool = SessionPool(rss_limit=1 << 40, max_live=100)
|
||||
self.backend = ClaudeSdkBackend(
|
||||
agent=self.agent,
|
||||
mcp_internal_urls={},
|
||||
session_store=self.store,
|
||||
client_factory=ScriptedClient,
|
||||
work_dir=root / "work",
|
||||
pool=self.pool,
|
||||
)
|
||||
self.bus = EventBus()
|
||||
self.conversations = Conversations(
|
||||
db=self.db,
|
||||
agents=AgentRegistry([self.agent]),
|
||||
backends={"a": self.backend},
|
||||
bus=self.bus,
|
||||
pool=self.pool,
|
||||
store=self.store,
|
||||
texts=ConversationTexts(),
|
||||
idle_interval=3600,
|
||||
)
|
||||
|
||||
async def setup(self) -> "World":
|
||||
await self.db.create_all()
|
||||
return self
|
||||
|
||||
def key(self, session_id: str) -> dict[str, str]:
|
||||
return {
|
||||
"project_key": project_key_for_directory(str(self.root)),
|
||||
"session_id": session_id,
|
||||
}
|
||||
|
||||
async def statuses(self, conv) -> list[tuple[str, str]]:
|
||||
rows = await self.conversations.queue.recent(conv.id)
|
||||
return [(r.priority, r.status) for r in reversed(rows)]
|
||||
|
||||
async def settle(self, conv, expected: int, timeout: float = 5.0) -> None:
|
||||
deadline = asyncio.get_running_loop().time() + timeout
|
||||
while asyncio.get_running_loop().time() < deadline:
|
||||
rows = await self.conversations.queue.recent(conv.id)
|
||||
if sum(1 for r in rows if r.status == "done") >= expected:
|
||||
return
|
||||
await asyncio.sleep(0.02)
|
||||
msg = f"queue did not settle: {await self.statuses(conv)}"
|
||||
raise AssertionError(msg)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def world() -> World:
|
||||
root = Path(tempfile.mkdtemp(prefix="beaver-conv-"))
|
||||
w = await World(root).setup()
|
||||
yield w
|
||||
await w.conversations.stop()
|
||||
await w.pool.close_all()
|
||||
await w.db.dispose()
|
||||
|
||||
|
||||
async def test_two_messages_run_one_at_a_time_in_order(world: World) -> None:
|
||||
conv = await world.conversations.create(kind="master", agent="a", origin="test")
|
||||
ScriptedClient.hold = asyncio.Event()
|
||||
await world.conversations.post(conv, "first")
|
||||
await asyncio.sleep(0.2)
|
||||
await world.conversations.post(conv, "second")
|
||||
await asyncio.sleep(0.2)
|
||||
assert await world.statuses(conv) == [("user", "running"), ("user", "queued")]
|
||||
live = world.pool.get(conv.external_id)
|
||||
assert live is not None and live.busy
|
||||
assert (await world.conversations.get(conv.external_id)).running_turn is not None
|
||||
ScriptedClient.hold.set()
|
||||
await world.settle(conv, 2)
|
||||
assert len(ScriptedClient.instances) == 1
|
||||
assert ScriptedClient.instances[0].prompts == ["first", "second"]
|
||||
row = await world.conversations.get(conv.external_id)
|
||||
assert row.running_turn is None
|
||||
assert row.session_id == ScriptedClient.instances[0].session_id
|
||||
|
||||
|
||||
async def test_urgent_interrupts_and_goes_first(world: World) -> None:
|
||||
conv = await world.conversations.create(kind="master", agent="a", origin="test")
|
||||
ScriptedClient.hold = asyncio.Event()
|
||||
await world.conversations.post(conv, "first")
|
||||
await asyncio.sleep(0.2)
|
||||
await world.conversations.post(conv, "second")
|
||||
await world.conversations.inject(conv, "ALERT", urgency="urgent", origin="крон")
|
||||
await world.settle(conv, 2)
|
||||
client = ScriptedClient.instances[0]
|
||||
assert await world.statuses(conv) == [
|
||||
("user", "interrupted"),
|
||||
("user", "done"),
|
||||
("urgent", "done"),
|
||||
]
|
||||
assert await world.statuses(conv) == [
|
||||
("user", "interrupted"),
|
||||
("user", "done"),
|
||||
("urgent", "done"),
|
||||
]
|
||||
assert client.prompts[0] == "first"
|
||||
assert client.prompts[1].startswith("[инжект: крон")
|
||||
assert client.prompts[1].endswith("ALERT")
|
||||
assert client.prompts[2] == "second"
|
||||
|
||||
|
||||
async def test_normal_injects_ride_with_the_next_user_message(world: World) -> None:
|
||||
conv = await world.conversations.create(kind="master", agent="a", origin="test")
|
||||
await world.conversations.inject(
|
||||
conv, "vault changed", urgency="normal", origin="watch"
|
||||
)
|
||||
await asyncio.sleep(0.2)
|
||||
assert await world.statuses(conv) == [("normal", "queued")]
|
||||
await world.conversations.post(conv, "hello")
|
||||
await world.settle(conv, 2)
|
||||
prompts = ScriptedClient.instances[0].prompts
|
||||
assert len(prompts) == 1
|
||||
assert prompts[0].startswith("hello\n\n[инжекты")
|
||||
assert "- [watch] vault changed" in prompts[0]
|
||||
|
||||
|
||||
async def test_inject_turn_reply_is_not_routed(world: World) -> None:
|
||||
conv = await world.conversations.create(kind="master", agent="a", origin="test")
|
||||
seen: list[dict[str, Any]] = []
|
||||
|
||||
async def collect() -> None:
|
||||
async for event in world.bus.stream(conversation_id=conv.external_id):
|
||||
seen.append(event)
|
||||
|
||||
task = asyncio.create_task(collect())
|
||||
world.conversations._normal_window = 0.05
|
||||
await world.conversations.inject(conv, "tick", urgency="normal", origin="крон")
|
||||
await world.settle(conv, 1)
|
||||
await asyncio.sleep(0.05)
|
||||
task.cancel()
|
||||
types = [e["type"] for e in seen]
|
||||
assert "turn.start" in types and "turn.end" in types
|
||||
assert "reply" not in types
|
||||
assert all(e.get("origin") == "inject" for e in seen if e["type"] == "turn.start")
|
||||
|
||||
|
||||
async def test_spawn_seeds_first_user_message(world: World) -> None:
|
||||
conv = await world.conversations.spawn(
|
||||
kind="branch", agent="a", seed="brief", text="do X", title="t"
|
||||
)
|
||||
await world.settle(conv, 1)
|
||||
prompt = ScriptedClient.instances[0].prompts[0]
|
||||
assert prompt.startswith("[сид: brief] branch «t», ")
|
||||
assert prompt.endswith("\n\ndo X")
|
||||
assert ScriptedClient.instances[0].options.system_prompt == "hi"
|
||||
|
||||
|
||||
async def test_fork_leaves_original_untouched(world: World) -> None:
|
||||
sid = str(uuid.uuid4())
|
||||
history = [
|
||||
{"role": "user", "content": "one"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{"type": "tool_use", "id": "t1", "name": "Read", "input": {}}],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "tool_result", "tool_use_id": "t1", "content": "x"}],
|
||||
},
|
||||
{"role": "assistant", "content": "done one"},
|
||||
{"role": "user", "content": "two"},
|
||||
{"role": "assistant", "content": "done two"},
|
||||
{"role": "user", "content": "three"},
|
||||
{"role": "assistant", "content": "done three"},
|
||||
]
|
||||
entries = build_entries(history, session_id=sid, cwd=str(world.root), model="m")
|
||||
await world.store.append(world.key(sid), entries)
|
||||
conv = await world.conversations.create(
|
||||
kind="branch", agent="a", origin="test", session_id=sid
|
||||
)
|
||||
before = [dict(e) for e in await world.store.load(world.key(sid))]
|
||||
|
||||
result = await world.conversations.fork(
|
||||
conv, "summarize", window=2, strip_tools=True
|
||||
)
|
||||
|
||||
after = await world.store.load(world.key(sid))
|
||||
assert after == before
|
||||
assert result.text == "ok:summarize"
|
||||
assert result.conversation.status == "closed"
|
||||
assert result.conversation.parent_id == conv.id
|
||||
fork_entries = await world.store.load(world.key(result.conversation.session_id))
|
||||
assert not {e["uuid"] for e in fork_entries} & {e["uuid"] for e in before}
|
||||
texts = [e["message"]["content"] for e in fork_entries]
|
||||
assert texts[0] == "two"
|
||||
assert len(fork_entries) == 4
|
||||
assert fork_entries[0]["parentUuid"] is None
|
||||
assert all(
|
||||
b["type"] == "text"
|
||||
for e in fork_entries[1:]
|
||||
for b in e["message"]["content"]
|
||||
if isinstance(e["message"]["content"], list)
|
||||
)
|
||||
client = ScriptedClient.instances[0]
|
||||
assert client.options.resume == result.conversation.session_id
|
||||
assert client.options.mcp_servers == {}
|
||||
assert world.pool.get(result.conversation.external_id) is None
|
||||
|
||||
|
||||
async def test_copy_seed_forks_parent_with_window(world: World) -> None:
|
||||
sid = str(uuid.uuid4())
|
||||
history = [
|
||||
{"role": "user", "content": f"q{i}"}
|
||||
if i % 2 == 0
|
||||
else {"role": "assistant", "content": f"a{i}"}
|
||||
for i in range(8)
|
||||
]
|
||||
await world.store.append(
|
||||
world.key(sid),
|
||||
build_entries(history, session_id=sid, cwd=str(world.root), model="m"),
|
||||
)
|
||||
parent = await world.conversations.create(
|
||||
kind="master", agent="a", origin="test", session_id=sid
|
||||
)
|
||||
child = await world.conversations.spawn(
|
||||
kind="branch", agent="a", seed="copy", parent=parent, window=1
|
||||
)
|
||||
assert child.session_id and child.session_id != sid
|
||||
copied = await world.store.load(world.key(child.session_id))
|
||||
assert [e["message"]["content"] for e in copied] == [
|
||||
"q6",
|
||||
[{"type": "text", "text": "a7"}],
|
||||
]
|
||||
await world.settle(child, 1)
|
||||
assert ScriptedClient.instances[0].options.resume == child.session_id
|
||||
|
||||
|
||||
async def test_merge_injects_summary_into_parent(world: World) -> None:
|
||||
sid = str(uuid.uuid4())
|
||||
await world.store.append(
|
||||
world.key(sid),
|
||||
build_entries(
|
||||
[{"role": "user", "content": "q"}, {"role": "assistant", "content": "a"}],
|
||||
session_id=sid,
|
||||
cwd=str(world.root),
|
||||
model="m",
|
||||
),
|
||||
)
|
||||
master = await world.conversations.create(kind="master", agent="a", origin="test")
|
||||
branch = await world.conversations.create(
|
||||
kind="branch", agent="a", origin="test", parent=master, session_id=sid
|
||||
)
|
||||
result = await world.conversations.merge(branch)
|
||||
assert result.text.startswith("ok:")
|
||||
assert (await world.conversations.get(branch.external_id)).status == "merged"
|
||||
assert await world.statuses(master) == [("normal", "queued")]
|
||||
item = (await world.conversations.queue.recent(master.id))[0]
|
||||
assert item.origin == "слив" and item.text == result.text
|
||||
|
||||
|
||||
async def test_recover_closes_open_tool_use_and_injects_interrupted(
|
||||
world: World,
|
||||
) -> None:
|
||||
sid = str(uuid.uuid4())
|
||||
history = [
|
||||
{"role": "user", "content": "go"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "t9",
|
||||
"name": "Bash",
|
||||
"input": {"command": "sleep"},
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
await world.store.append(
|
||||
world.key(sid),
|
||||
build_entries(history, session_id=sid, cwd=str(world.root), model="m"),
|
||||
)
|
||||
conv = await world.conversations.create(
|
||||
kind="master", agent="a", origin="test", session_id=sid
|
||||
)
|
||||
async with world.db.session() as session:
|
||||
row = await session.get(type(conv), conv.id)
|
||||
row.running_turn = "turn_dead"
|
||||
session.add(row)
|
||||
session.add(
|
||||
InjectQueueItem(
|
||||
conversation_id=conv.id,
|
||||
priority="user",
|
||||
origin="user",
|
||||
text="go",
|
||||
status="running",
|
||||
turn_id="turn_dead",
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
cut = await world.conversations.recover()
|
||||
|
||||
assert [c.external_id for c in cut] == [conv.external_id]
|
||||
entries = await world.store.load(world.key(sid))
|
||||
assert not open_tool_uses(entries)
|
||||
tail = entries[-1]
|
||||
assert tail["type"] == "user"
|
||||
assert tail["message"]["content"][0] == {
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "t9",
|
||||
"content": "прервано",
|
||||
"is_error": True,
|
||||
}
|
||||
assert tail["parentUuid"] == entries[-2]["uuid"]
|
||||
assert (await world.conversations.get(conv.external_id)).running_turn is None
|
||||
assert await world.statuses(conv) == [("user", "interrupted"), ("normal", "queued")]
|
||||
note = (await world.conversations.queue.recent(conv.id))[0]
|
||||
assert (
|
||||
"turn_dead" in note.text
|
||||
and "оборван" in note.text
|
||||
and "1 незакрытых" in note.text
|
||||
)
|
||||
await asyncio.sleep(0.2)
|
||||
assert ScriptedClient.instances == []
|
||||
|
||||
|
||||
async def test_read_and_bindings(world: World) -> None:
|
||||
sid = str(uuid.uuid4())
|
||||
history = [
|
||||
{"role": "user", "content": "q1"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "text", "text": "a1"},
|
||||
{"type": "tool_use", "id": "t", "name": "Read", "input": {}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "tool_result", "tool_use_id": "t", "content": "x"}],
|
||||
},
|
||||
{"role": "assistant", "content": "a1b"},
|
||||
{"role": "user", "content": "q2"},
|
||||
{"role": "assistant", "content": "a2"},
|
||||
]
|
||||
await world.store.append(
|
||||
world.key(sid),
|
||||
build_entries(history, session_id=sid, cwd=str(world.root), model="m"),
|
||||
)
|
||||
conv = await world.conversations.create(
|
||||
kind="deep", agent="a", origin="test", session_id=sid
|
||||
)
|
||||
assert (
|
||||
await world.conversations.read(conv)
|
||||
== "user:\nq1\n\nassistant:\na1\n\na1b\n(tools: Read)\n\nuser:\nq2\n\nassistant:\na2"
|
||||
)
|
||||
assert (
|
||||
await world.conversations.read(conv, window=1) == "user:\nq2\n\nassistant:\na2"
|
||||
)
|
||||
|
||||
await world.conversations.bind(conv, frontend="markdown", external_id="a.md")
|
||||
await world.conversations.bind(conv, frontend="markdown", external_id="b.md")
|
||||
bindings = await world.conversations.bindings(conv)
|
||||
assert [(b.external_id, b.visible) for b in bindings] == [
|
||||
("a.md", False),
|
||||
("b.md", True),
|
||||
]
|
||||
found = await world.conversations.find_bound(
|
||||
frontend="markdown", external_id="b.md"
|
||||
)
|
||||
assert found is not None and found.id == conv.id
|
||||
assert (
|
||||
await world.conversations.find_bound(frontend="markdown", external_id="a.md")
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_schedule_rows_and_parse_at(world: World) -> None:
|
||||
conv = await world.conversations.create(kind="master", agent="a", origin="test")
|
||||
row = await world.conversations.schedule(conv, "+15m", "push X")
|
||||
delta = (row.execute_at.replace(tzinfo=UTC) - datetime.now(UTC)).total_seconds()
|
||||
assert 14 * 60 < delta <= 15 * 60
|
||||
assert [s.text for s in await world.conversations.schedules(conv)] == ["push X"]
|
||||
assert parse_at("2026-09-01T10:00:00+02:00") == datetime(
|
||||
2026, 9, 1, 8, 0, tzinfo=UTC
|
||||
)
|
||||
with pytest.raises(ValueError, match="Invalid isoformat"):
|
||||
parse_at("tomorrow")
|
||||
|
||||
|
||||
def test_transcript_helpers() -> None:
|
||||
history = [
|
||||
{"role": "user", "content": "q1"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "hm", "signature": "s"},
|
||||
{"type": "tool_use", "id": "t", "name": "Read", "input": {}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "tool_result", "tool_use_id": "t", "content": "x"}],
|
||||
},
|
||||
{"role": "assistant", "content": "a1"},
|
||||
{"role": "user", "content": "q2"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{"type": "tool_use", "id": "t2", "name": "Bash", "input": {}}],
|
||||
},
|
||||
]
|
||||
entries = build_entries(history, session_id="s", cwd="/v", model="m")
|
||||
assert [b["id"] for _, b in open_tool_uses(entries)] == ["t2"]
|
||||
fixes = close_open_tool_uses(entries)
|
||||
assert len(fixes) == 1 and fixes[0]["sessionId"] == "s" and fixes[0]["cwd"] == "/v"
|
||||
assert not open_tool_uses([*entries, *fixes])
|
||||
stripped = strip_tool_entries(entries)
|
||||
assert [e["message"]["content"] for e in stripped] == [
|
||||
"q1",
|
||||
[{"type": "text", "text": "a1"}],
|
||||
"q2",
|
||||
]
|
||||
assert (
|
||||
stripped[0]["parentUuid"] is None
|
||||
and stripped[1]["parentUuid"] == stripped[0]["uuid"]
|
||||
)
|
||||
windowed = window_entries(entries, window=1)
|
||||
assert windowed[0]["message"]["content"] == "q2" and len(windowed) == 2
|
||||
assert (
|
||||
render_messages(
|
||||
[{"role": "user", "content": "q"}, {"role": "assistant", "content": "a"}]
|
||||
)
|
||||
== "user:\nq\n\nassistant:\na"
|
||||
)
|
||||
@@ -31,3 +31,31 @@ async def test_create_all_adds_missing_columns() -> None:
|
||||
session.add(conv)
|
||||
await session.commit()
|
||||
await db.dispose()
|
||||
|
||||
|
||||
async def test_create_all_backfills_defaults_for_old_rows() -> None:
|
||||
path = Path(tempfile.mkdtemp(prefix="beaver-migrate-")) / "old.db"
|
||||
raw = sqlite3.connect(path)
|
||||
raw.execute(
|
||||
"CREATE TABLE conversations (id INTEGER PRIMARY KEY, frontend VARCHAR NOT NULL, "
|
||||
"external_id VARCHAR NOT NULL, agent_name VARCHAR NOT NULL, session_id VARCHAR, "
|
||||
"created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL)"
|
||||
)
|
||||
raw.execute(
|
||||
"INSERT INTO conversations VALUES (1, 'markdown', 'x', 'a', NULL, '2026-01-01', '2026-01-01')"
|
||||
)
|
||||
raw.commit()
|
||||
raw.close()
|
||||
|
||||
db = Database(f"sqlite:///{path}")
|
||||
await db.create_all()
|
||||
async with db.session() as session:
|
||||
conv = (await session.exec(select(Conversation))).one()
|
||||
assert (conv.kind, conv.status, conv.pending_question, conv.flags) == (
|
||||
"deep",
|
||||
"open",
|
||||
False,
|
||||
{},
|
||||
)
|
||||
assert conv.running_turn is None
|
||||
await db.dispose()
|
||||
|
||||
Reference in New Issue
Block a user