Phases 2 to 8 of the AI assistant overhaul plus fixes from real-model chats: one tool registry with native tool calls, server-kept chat logs with prompt caching, an FTS5 library index with opt-in meaning search, keys bound to their address and a guard on every AI route, page tools and opt-in web search, saved chats with streaming, Try again, a context meter and compaction, chat images, delete/category/tag permissions with Undo, constraint and test cards, card fixes and previews, questions with answers to click, the derivation NOT toggle (schema v10, community catalog 0021), request fixes for the newest Claude models, and OpenAI's Responses API. Review fixes: Try again clears the old turn's fixes and answers, category Undo removes only what it made, a failed delete rolls back in order, not_matches patterns are checked, busy chats refuse a second request, and chat search ignores stored JSON.
65 lines
2.2 KiB
Python
65 lines
2.2 KiB
Python
"""Migration 021: pinned AI chats."""
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import sqlite3
|
|
|
|
from engine.db.migrations import migrate
|
|
from engine.db.repositories import MAX_AI_CHATS, ChatRepository
|
|
|
|
|
|
def _conn(_tmp_path):
|
|
conn = sqlite3.connect(":memory:")
|
|
conn.row_factory = sqlite3.Row
|
|
migrate(conn)
|
|
return conn
|
|
|
|
|
|
def test_column_exists_with_default(tmp_path):
|
|
conn = _conn(tmp_path)
|
|
cols = {r[1]: r for r in conn.execute("PRAGMA table_info(ai_chats);").fetchall()}
|
|
assert "pinned" in cols
|
|
chat = ChatRepository(conn).create(title="t")
|
|
assert chat["pinned"] is False
|
|
|
|
|
|
def test_rerun_is_idempotent(tmp_path):
|
|
conn = _conn(tmp_path)
|
|
mod = importlib.import_module("engine.db.migrations_sql.021_ai_chat_pins")
|
|
mod.up(conn)
|
|
mod.up(conn)
|
|
|
|
|
|
def test_list_rename_pin_and_prune_keeps_pins(tmp_path):
|
|
conn = _conn(tmp_path)
|
|
repo = ChatRepository(conn)
|
|
first = repo.create(title="Hats")
|
|
repo.append(first["id"], [{"kind": "user", "text": "make hats", "sent": "make hats"}],
|
|
meta={})
|
|
before = repo.get(first["id"])["updated_at"]
|
|
repo.update(first["id"], title="Hat ideas", pinned=True)
|
|
got = repo.get(first["id"])
|
|
assert got["title"] == "Hat ideas" and got["pinned"] is True
|
|
# Renaming and pinning aren't activity.
|
|
assert got["updated_at"] == before
|
|
for i in range(MAX_AI_CHATS + 3):
|
|
repo.create(title=f"c{i}")
|
|
rows = repo.list()
|
|
assert rows[0]["id"] == first["id"] and rows[0]["turns"] == 1
|
|
assert len(rows) == MAX_AI_CHATS + 1
|
|
assert [r["id"] for r in repo.list("make hat")] == [first["id"]]
|
|
assert repo.list("100%") == []
|
|
|
|
|
|
def test_truncate_drops_the_tail_only(tmp_path):
|
|
conn = _conn(tmp_path)
|
|
repo = ChatRepository(conn)
|
|
chat = repo.create(title="t")
|
|
repo.append(chat["id"], [{"kind": "user", "text": str(i)} for i in range(5)], meta={})
|
|
repo.truncate(chat["id"], 3, meta={"x": 1})
|
|
assert [e["text"] for e in repo.messages(chat["id"])] == ["0", "1", "2"]
|
|
repo.truncate(chat["id"], 10, meta={})
|
|
assert len(repo.messages(chat["id"])) == 3
|
|
repo.append(chat["id"], [{"kind": "user", "text": "new"}], meta={})
|
|
assert repo.messages(chat["id"])[-1]["text"] == "new"
|