feat: fixes on pg2

This commit is contained in:
Alex committed 2026-04-12 13:51:29 +01:00
1 parent 530761d08c
commit cb30a24e05
19 files changed
+355 -237

No files matched your search

@@ -0,0 +1,57 @@
"""0002 add unique constraints for notes and connector_sessions.
The memories table already has ``memories_user_tool_path_uidx`` from the
0001 baseline. Notes and connector_sessions were missing unique constraints
that their repository upsert logic depends on.
Before creating the indexes, duplicate rows are cleaned up — keeping only
the row with the latest ``id`` (UUID, lexicographic max) per group.
Revision ID: 0002_add_unique_constraints
Revises: 0001_initial
Create Date: 2026-04-12
"""
from typing import Sequence, Union
from alembic import op
revision: str = "0002_add_unique_constraints"
down_revision: Union[str, None] = "0001_initial"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Deduplicate notes: keep one row per (user_id, tool_id)
op.execute("""
DELETE FROM notes
WHERE id NOT IN (
SELECT DISTINCT ON (user_id, tool_id) id
FROM notes
ORDER BY user_id, tool_id, created_at DESC
);
""")
op.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS notes_user_tool_uidx "
"ON notes (user_id, tool_id);"
)
# Deduplicate connector_sessions: keep one row per (user_id, provider)
op.execute("""
DELETE FROM connector_sessions
WHERE id NOT IN (
SELECT DISTINCT ON (user_id, provider) id
FROM connector_sessions
ORDER BY user_id, provider, created_at DESC
);
""")
op.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS connector_sessions_user_provider_uidx "
"ON connector_sessions (user_id, provider);"
)
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS connector_sessions_user_provider_uidx;")
op.execute("DROP INDEX IF EXISTS notes_user_tool_uidx;")
+4
View File
@@ -15,6 +15,7 @@ from sqlalchemy import (
ForeignKey,
Integer,
MetaData,
UniqueConstraint,
Table,
Text,
func,
@@ -190,6 +191,7 @@ memories_table = Table(
Column("path", Text, nullable=False),
Column("content", Text, nullable=False),
Column("updated_at", DateTime(timezone=True), nullable=False, server_default=func.now()),
UniqueConstraint("user_id", "tool_id", "path", name="memories_user_tool_path_uidx"),
)
todos_table = Table(
@@ -214,6 +216,7 @@ notes_table = Table(
Column("content", Text, nullable=False),
Column("created_at", DateTime(timezone=True), nullable=False, server_default=func.now()),
Column("updated_at", DateTime(timezone=True), nullable=False, server_default=func.now()),
UniqueConstraint("user_id", "tool_id", name="notes_user_tool_uidx"),
)
connector_sessions_table = Table(
@@ -225,4 +228,5 @@ connector_sessions_table = Table(
Column("session_data", JSONB, nullable=False),
Column("expires_at", DateTime(timezone=True)),
Column("created_at", DateTime(timezone=True), nullable=False, server_default=func.now()),
UniqueConstraint("user_id", "provider", name="connector_sessions_user_provider_uidx"),
)
@@ -13,16 +13,16 @@ class AgentFoldersRepository:
def __init__(self, conn: Connection) -> None:
self._conn = conn
def create(self, user_id: str, name: str, *, parent_id: Optional[str] = None) -> dict:
def create(self, user_id: str, name: str, *, description: Optional[str] = None) -> dict:
result = self._conn.execute(
text(
"""
INSERT INTO agent_folders (user_id, name, description)
VALUES (:user_id, :name, :parent_id)
VALUES (:user_id, :name, :description)
RETURNING *
"""
),
{"user_id": user_id, "name": name, "parent_id": parent_id},
{"user_id": user_id, "name": name, "description": description},
)
return row_to_dict(result.fetchone())
@@ -46,13 +46,38 @@ class AgentFoldersRepository:
filtered = {k: v for k, v in fields.items() if k in allowed}
if not filtered:
return False
set_clauses = [f"{col} = :val_{col}" for col in filtered]
set_clauses.append("updated_at = now()")
params: dict = {"id": folder_id, "user_id": user_id}
for col, val in filtered.items():
params[f"val_{col}"] = val
sql = f"UPDATE agent_folders SET {', '.join(set_clauses)} WHERE id = CAST(:id AS uuid) AND user_id = :user_id"
result = self._conn.execute(text(sql), params)
if "name" in filtered and "description" in filtered:
params["name"] = filtered["name"]
params["description"] = filtered["description"]
result = self._conn.execute(
text(
"UPDATE agent_folders "
"SET name = :name, description = :description, updated_at = now() "
"WHERE id = CAST(:id AS uuid) AND user_id = :user_id"
),
params,
)
elif "name" in filtered:
params["name"] = filtered["name"]
result = self._conn.execute(
text(
"UPDATE agent_folders "
"SET name = :name, updated_at = now() "
"WHERE id = CAST(:id AS uuid) AND user_id = :user_id"
),
params,
)
else:
params["description"] = filtered["description"]
result = self._conn.execute(
text(
"UPDATE agent_folders "
"SET description = :description, updated_at = now() "
"WHERE id = CAST(:id AS uuid) AND user_id = :user_id"
),
params,
)
return result.rowcount > 0
def delete(self, folder_id: str, user_id: str) -> bool:
+42 -57
View File
@@ -15,9 +15,11 @@ from __future__ import annotations
import json
from typing import Optional
from sqlalchemy import Connection, text
from sqlalchemy import Connection, func, text
from sqlalchemy.dialects.postgresql import insert as pg_insert
from application.storage.db.base_repository import row_to_dict
from application.storage.db.models import agents_table
class AgentsRepository:
@@ -25,56 +27,36 @@ class AgentsRepository:
self._conn = conn
def create(self, user_id: str, name: str, status: str, **kwargs) -> dict:
cols = ["user_id", "name", "status"]
vals = [":user_id", ":name", ":status"]
params: dict = {"user_id": user_id, "name": name, "status": status}
values: dict = {"user_id": user_id, "name": name, "status": status}
_TEXT_COLS = {"description", "agent_type", "key", "retriever",
"default_model_id", "incoming_webhook_token"}
_UUID_COLS = {"source_id", "prompt_id", "folder_id"}
_INT_COLS = {"chunks", "token_limit", "request_limit"}
_BOOL_COLS = {"limited_token_mode", "limited_request_mode", "shared"}
_JSONB_COLS = {"tools", "json_schema", "models"}
_ALLOWED = {
"description", "agent_type", "key", "retriever",
"default_model_id", "incoming_webhook_token",
"source_id", "prompt_id", "folder_id",
"chunks", "token_limit", "request_limit",
"limited_token_mode", "limited_request_mode", "shared",
"tools", "json_schema", "models",
}
for col in _TEXT_COLS:
if col in kwargs and kwargs[col] is not None:
cols.append(col)
vals.append(f":{col}")
params[col] = kwargs[col]
for col in _UUID_COLS:
if col in kwargs and kwargs[col] is not None:
cols.append(col)
vals.append(f"CAST(:{col} AS uuid)")
params[col] = str(kwargs[col])
for col in _INT_COLS:
if col in kwargs and kwargs[col] is not None:
cols.append(col)
vals.append(f":{col}")
params[col] = int(kwargs[col])
for col in _BOOL_COLS:
if col in kwargs:
cols.append(col)
vals.append(f":{col}")
params[col] = bool(kwargs[col])
for col in _JSONB_COLS:
if col in kwargs and kwargs[col] is not None:
cols.append(col)
vals.append(f"CAST(:{col} AS jsonb)")
params[col] = json.dumps(kwargs[col])
for col, val in kwargs.items():
if col not in _ALLOWED or val is None:
continue
if col in ("tools", "json_schema", "models"):
values[col] = json.dumps(val)
elif col in ("chunks", "token_limit", "request_limit"):
values[col] = int(val)
elif col in ("limited_token_mode", "limited_request_mode", "shared"):
values[col] = bool(val)
elif col in ("source_id", "prompt_id", "folder_id"):
values[col] = str(val)
else:
values[col] = val
sql = f"INSERT INTO agents ({', '.join(cols)}) VALUES ({', '.join(vals)}) RETURNING *"
result = self._conn.execute(text(sql), params)
stmt = pg_insert(agents_table).values(**values).returning(agents_table)
result = self._conn.execute(stmt)
return row_to_dict(result.fetchone())
def get(self, agent_id: str) -> Optional[dict]:
result = self._conn.execute(
text("SELECT * FROM agents WHERE id = CAST(:id AS uuid)"),
{"id": agent_id},
)
row = result.fetchone()
return row_to_dict(row) if row is not None else None
def get_for_user(self, agent_id: str, user_id: str) -> Optional[dict]:
def get(self, agent_id: str, user_id: str) -> Optional[dict]:
result = self._conn.execute(
text("SELECT * FROM agents WHERE id = CAST(:id AS uuid) AND user_id = :user_id"),
{"id": agent_id, "user_id": user_id},
@@ -123,21 +105,24 @@ class AgentsRepository:
if not filtered:
return False
set_clauses = []
params: dict = {"id": agent_id, "user_id": user_id}
values: dict = {}
for col, val in filtered.items():
if col in ("tools", "json_schema", "models"):
set_clauses.append(f"{col} = CAST(:val_{col} AS jsonb)")
params[f"val_{col}"] = json.dumps(val) if not isinstance(val, str) else val
values[col] = json.dumps(val) if not isinstance(val, str) else val
elif col in ("source_id", "prompt_id", "folder_id"):
set_clauses.append(f"{col} = CAST(:val_{col} AS uuid)")
params[f"val_{col}"] = str(val) if val else None
values[col] = str(val) if val else None
else:
set_clauses.append(f"{col} = :val_{col}")
params[f"val_{col}"] = val
set_clauses.append("updated_at = now()")
sql = f"UPDATE agents SET {', '.join(set_clauses)} WHERE id = CAST(:id AS uuid) AND user_id = :user_id"
result = self._conn.execute(text(sql), params)
values[col] = val
values["updated_at"] = func.now()
t = agents_table
stmt = (
t.update()
.where(t.c.id == agent_id)
.where(t.c.user_id == user_id)
.values(**values)
)
result = self._conn.execute(stmt)
return result.rowcount > 0
def delete(self, agent_id: str, user_id: str) -> bool:
@@ -33,10 +33,12 @@ class AttachmentsRepository:
)
return row_to_dict(result.fetchone())
def get(self, attachment_id: str) -> Optional[dict]:
def get(self, attachment_id: str, user_id: str) -> Optional[dict]:
result = self._conn.execute(
text("SELECT * FROM attachments WHERE id = CAST(:id AS uuid)"),
{"id": attachment_id},
text(
"SELECT * FROM attachments WHERE id = CAST(:id AS uuid) AND user_id = :user_id"
),
{"id": attachment_id, "user_id": user_id},
)
row = result.fetchone()
return row_to_dict(row) if row is not None else None
@@ -27,7 +27,8 @@ class ConnectorSessionsRepository:
"""
INSERT INTO connector_sessions (user_id, provider, session_data)
VALUES (:user_id, :provider, CAST(:session_data AS jsonb))
ON CONFLICT DO NOTHING
ON CONFLICT (user_id, provider)
DO UPDATE SET session_data = EXCLUDED.session_data
RETURNING *
"""
),
@@ -37,18 +38,7 @@ class ConnectorSessionsRepository:
"session_data": json.dumps(session_data),
},
)
row = result.fetchone()
if row is not None:
return row_to_dict(row)
# Conflict — update existing row.
self._conn.execute(
text(
"UPDATE connector_sessions SET session_data = CAST(:session_data AS jsonb) "
"WHERE user_id = :user_id AND provider = :provider"
),
{"user_id": user_id, "provider": provider, "session_data": json.dumps(session_data)},
)
return self.get_by_user_provider(user_id, provider) or {}
return row_to_dict(result.fetchone())
def get_by_user_provider(self, user_id: str, provider: str) -> Optional[dict]:
result = self._conn.execute(
+6 -16
View File
@@ -25,24 +25,14 @@ class NotesRepository:
"""
INSERT INTO notes (user_id, tool_id, title, content)
VALUES (:user_id, CAST(:tool_id AS uuid), :title, :content)
ON CONFLICT DO NOTHING
ON CONFLICT (user_id, tool_id)
DO UPDATE SET content = EXCLUDED.content, title = EXCLUDED.title, updated_at = now()
RETURNING *
"""
),
{"user_id": user_id, "tool_id": tool_id, "title": title, "content": content},
)
row = result.fetchone()
if row is not None:
return row_to_dict(row)
# Row already existed — update instead.
self._conn.execute(
text(
"UPDATE notes SET content = :content, updated_at = now() "
"WHERE user_id = :user_id AND tool_id = CAST(:tool_id AS uuid)"
),
{"user_id": user_id, "tool_id": tool_id, "content": content},
)
return self.get_for_user_tool(user_id, tool_id) or {}
return row_to_dict(result.fetchone())
def get_for_user_tool(self, user_id: str, tool_id: str) -> Optional[dict]:
result = self._conn.execute(
@@ -54,10 +44,10 @@ class NotesRepository:
row = result.fetchone()
return row_to_dict(row) if row is not None else None
def get(self, note_id: str) -> Optional[dict]:
def get(self, note_id: str, user_id: str) -> Optional[dict]:
result = self._conn.execute(
text("SELECT * FROM notes WHERE id = CAST(:id AS uuid)"),
{"id": note_id},
text("SELECT * FROM notes WHERE id = CAST(:id AS uuid) AND user_id = :user_id"),
{"id": note_id, "user_id": user_id},
)
row = result.fetchone()
return row_to_dict(row) if row is not None else None
+18 -11
View File
@@ -40,17 +40,24 @@ class PromptsRepository:
)
return row_to_dict(result.fetchone())
def get(self, prompt_id: str, user_id: Optional[str] = None) -> Optional[dict]:
if user_id is not None:
result = self._conn.execute(
text("SELECT * FROM prompts WHERE id = CAST(:id AS uuid) AND user_id = :user_id"),
{"id": prompt_id, "user_id": user_id},
)
else:
result = self._conn.execute(
text("SELECT * FROM prompts WHERE id = CAST(:id AS uuid)"),
{"id": prompt_id},
)
def get(self, prompt_id: str, user_id: str) -> Optional[dict]:
result = self._conn.execute(
text("SELECT * FROM prompts WHERE id = CAST(:id AS uuid) AND user_id = :user_id"),
{"id": prompt_id, "user_id": user_id},
)
row = result.fetchone()
return row_to_dict(row) if row is not None else None
def get_for_rendering(self, prompt_id: str) -> Optional[dict]:
"""Fetch prompt content by ID without user scoping.
Used only by stream_processor to render a prompt whose owner is
not known at call time. Do NOT use in user-facing routes.
"""
result = self._conn.execute(
text("SELECT * FROM prompts WHERE id = CAST(:id AS uuid)"),
{"id": prompt_id},
)
row = result.fetchone()
return row_to_dict(row) if row is not None else None
+21 -23
View File
@@ -5,9 +5,10 @@ from __future__ import annotations
import json
from typing import Optional
from sqlalchemy import Connection, text
from sqlalchemy import Connection, func, text
from application.storage.db.base_repository import row_to_dict
from application.storage.db.models import sources_table
class SourcesRepository:
@@ -33,15 +34,7 @@ class SourcesRepository:
)
return row_to_dict(result.fetchone())
def get(self, source_id: str) -> Optional[dict]:
result = self._conn.execute(
text("SELECT * FROM sources WHERE id = CAST(:id AS uuid)"),
{"id": source_id},
)
row = result.fetchone()
return row_to_dict(row) if row is not None else None
def get_for_user(self, source_id: str, user_id: str) -> Optional[dict]:
def get(self, source_id: str, user_id: str) -> Optional[dict]:
result = self._conn.execute(
text("SELECT * FROM sources WHERE id = CAST(:id AS uuid) AND user_id = :user_id"),
{"id": source_id, "user_id": user_id},
@@ -56,27 +49,32 @@ class SourcesRepository:
)
return [row_to_dict(r) for r in result.fetchall()]
def update(self, source_id: str, fields: dict) -> None:
def update(self, source_id: str, user_id: str, fields: dict) -> None:
allowed = {"name", "type", "metadata"}
filtered = {k: v for k, v in fields.items() if k in allowed}
if not filtered:
return
set_clauses = []
params: dict = {"id": source_id}
values: dict = {}
for col, val in filtered.items():
if col == "metadata":
set_clauses.append(f"{col} = CAST(:val_{col} AS jsonb)")
params[f"val_{col}"] = json.dumps(val) if isinstance(val, dict) else val
values[col] = json.dumps(val) if isinstance(val, dict) else val
else:
set_clauses.append(f"{col} = :val_{col}")
params[f"val_{col}"] = val
set_clauses.append("updated_at = now()")
sql = f"UPDATE sources SET {', '.join(set_clauses)} WHERE id = CAST(:id AS uuid)"
self._conn.execute(text(sql), params)
values[col] = val
values["updated_at"] = func.now()
def delete(self, source_id: str) -> bool:
t = sources_table
stmt = (
t.update()
.where(t.c.id == source_id)
.where(t.c.user_id == user_id)
.values(**values)
)
self._conn.execute(stmt)
def delete(self, source_id: str, user_id: str) -> bool:
result = self._conn.execute(
text("DELETE FROM sources WHERE id = CAST(:id AS uuid)"),
{"id": source_id},
text("DELETE FROM sources WHERE id = CAST(:id AS uuid) AND user_id = :user_id"),
{"id": source_id, "user_id": user_id},
)
return result.rowcount > 0
+20 -16
View File
@@ -32,10 +32,10 @@ class TodosRepository:
)
return row_to_dict(result.fetchone())
def get(self, todo_id: str) -> Optional[dict]:
def get(self, todo_id: str, user_id: str) -> Optional[dict]:
result = self._conn.execute(
text("SELECT * FROM todos WHERE id = CAST(:id AS uuid)"),
{"id": todo_id},
text("SELECT * FROM todos WHERE id = CAST(:id AS uuid) AND user_id = :user_id"),
{"id": todo_id, "user_id": user_id},
)
row = result.fetchone()
return row_to_dict(row) if row is not None else None
@@ -50,25 +50,29 @@ class TodosRepository:
)
return [row_to_dict(r) for r in result.fetchall()]
def update_title(self, todo_id: str, title: str) -> bool:
result = self._conn.execute(
text("UPDATE todos SET title = :title, updated_at = now() WHERE id = CAST(:id AS uuid)"),
{"id": todo_id, "title": title},
)
return result.rowcount > 0
def set_completed(self, todo_id: str, completed: bool = True) -> bool:
def update_title(self, todo_id: str, user_id: str, title: str) -> bool:
result = self._conn.execute(
text(
"UPDATE todos SET completed = :completed, updated_at = now() WHERE id = CAST(:id AS uuid)"
"UPDATE todos SET title = :title, updated_at = now() "
"WHERE id = CAST(:id AS uuid) AND user_id = :user_id"
),
{"id": todo_id, "completed": completed},
{"id": todo_id, "user_id": user_id, "title": title},
)
return result.rowcount > 0
def delete(self, todo_id: str) -> bool:
def set_completed(self, todo_id: str, user_id: str, completed: bool = True) -> bool:
result = self._conn.execute(
text("DELETE FROM todos WHERE id = CAST(:id AS uuid)"),
{"id": todo_id},
text(
"UPDATE todos SET completed = :completed, updated_at = now() "
"WHERE id = CAST(:id AS uuid) AND user_id = :user_id"
),
{"id": todo_id, "user_id": user_id, "completed": completed},
)
return result.rowcount > 0
def delete(self, todo_id: str, user_id: str) -> bool:
result = self._conn.execute(
text("DELETE FROM todos WHERE id = CAST(:id AS uuid) AND user_id = :user_id"),
{"id": todo_id, "user_id": user_id},
)
return result.rowcount > 0
@@ -53,10 +53,10 @@ class UserToolsRepository:
)
return row_to_dict(result.fetchone())
def get(self, tool_id: str) -> Optional[dict]:
def get(self, tool_id: str, user_id: str) -> Optional[dict]:
result = self._conn.execute(
text("SELECT * FROM user_tools WHERE id = CAST(:id AS uuid)"),
{"id": tool_id},
text("SELECT * FROM user_tools WHERE id = CAST(:id AS uuid) AND user_id = :user_id"),
{"id": tool_id, "user_id": user_id},
)
row = result.fetchone()
return row_to_dict(row) if row is not None else None
+4 -1
View File
@@ -703,6 +703,8 @@ def _backfill_notes(
"""
INSERT INTO notes (user_id, tool_id, title, content)
VALUES (:user_id, CAST(:tool_id AS uuid), :title, :content)
ON CONFLICT (user_id, tool_id) DO UPDATE
SET content = EXCLUDED.content, title = EXCLUDED.title
"""
)
cursor = mongo_db["notes"].find({}, no_cursor_timeout=True).batch_size(batch_size)
@@ -743,7 +745,8 @@ def _backfill_connector_sessions(
"""
INSERT INTO connector_sessions (user_id, provider, session_data)
VALUES (:user_id, :provider, CAST(:session_data AS jsonb))
ON CONFLICT DO NOTHING
ON CONFLICT (user_id, provider) DO UPDATE
SET session_data = EXCLUDED.session_data
"""
)
cursor = mongo_db["connector_sessions"].find({}, no_cursor_timeout=True).batch_size(batch_size)
+13 -21
View File
@@ -48,26 +48,18 @@ class TestCreate:
class TestGet:
def test_get_existing(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("u", "a", "draft")
fetched = repo.get(created["id"])
created = repo.create("user-1", "a", "draft")
fetched = repo.get(created["id"], "user-1")
assert fetched["id"] == created["id"]
def test_get_nonexistent_returns_none(self, pg_conn):
repo = _repo(pg_conn)
assert repo.get("00000000-0000-0000-0000-000000000000") is None
assert repo.get("00000000-0000-0000-0000-000000000000", "user-1") is None
class TestGetForUser:
def test_get_for_correct_user(self, pg_conn):
def test_get_wrong_user_returns_none(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "a", "draft")
fetched = repo.get_for_user(created["id"], "user-1")
assert fetched["id"] == created["id"]
def test_get_for_wrong_user_returns_none(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "a", "draft")
assert repo.get_for_user(created["id"], "user-other") is None
assert repo.get(created["id"], "user-other") is None
class TestFindByKey:
@@ -99,7 +91,7 @@ class TestUpdate:
created = repo.create("user-1", "old", "draft")
updated = repo.update(created["id"], "user-1", {"name": "new"})
assert updated is True
fetched = repo.get(created["id"])
fetched = repo.get(created["id"], "user-1")
assert fetched["name"] == "new"
def test_update_wrong_user_returns_false(self, pg_conn):
@@ -107,7 +99,7 @@ class TestUpdate:
created = repo.create("user-1", "old", "draft")
updated = repo.update(created["id"], "user-other", {"name": "new"})
assert updated is False
fetched = repo.get(created["id"])
fetched = repo.get(created["id"], "user-1")
assert fetched["name"] == "old"
def test_update_disallowed_field_returns_false(self, pg_conn):
@@ -123,14 +115,14 @@ class TestDelete:
created = repo.create("user-1", "a", "draft")
deleted = repo.delete(created["id"], "user-1")
assert deleted is True
assert repo.get(created["id"]) is None
assert repo.get(created["id"], "user-1") is None
def test_delete_wrong_user_returns_false(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "a", "draft")
deleted = repo.delete(created["id"], "user-other")
assert deleted is False
assert repo.get(created["id"]) is not None
assert repo.get(created["id"], "user-1") is not None
class TestSetFolder:
@@ -142,7 +134,7 @@ class TestSetFolder:
repo = _repo(pg_conn)
agent = repo.create("user-1", "a", "draft")
repo.set_folder(agent["id"], "user-1", folder["id"])
fetched = repo.get(agent["id"])
fetched = repo.get(agent["id"], "user-1")
assert str(fetched["folder_id"]) == str(folder["id"])
def test_clear_folder(self, pg_conn):
@@ -153,7 +145,7 @@ class TestSetFolder:
repo = _repo(pg_conn)
agent = repo.create("user-1", "a", "draft", folder_id=folder["id"])
repo.set_folder(agent["id"], "user-1", None)
fetched = repo.get(agent["id"])
fetched = repo.get(agent["id"], "user-1")
assert fetched["folder_id"] is None
@@ -167,5 +159,5 @@ class TestClearFolderForAll:
a1 = repo.create("user-1", "a1", "draft", folder_id=folder["id"])
a2 = repo.create("user-1", "a2", "draft", folder_id=folder["id"])
repo.clear_folder_for_all(folder["id"], "user-1")
assert repo.get(a1["id"])["folder_id"] is None
assert repo.get(a2["id"])["folder_id"] is None
assert repo.get(a1["id"], "user-1")["folder_id"] is None
assert repo.get(a2["id"], "user-1")["folder_id"] is None
@@ -42,12 +42,17 @@ class TestGet:
def test_get_existing(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("u", "f", "/p")
fetched = repo.get(created["id"])
fetched = repo.get(created["id"], "u")
assert fetched["id"] == created["id"]
def test_get_nonexistent_returns_none(self, pg_conn):
repo = _repo(pg_conn)
assert repo.get("00000000-0000-0000-0000-000000000000") is None
assert repo.get("00000000-0000-0000-0000-000000000000", "u") is None
def test_get_wrong_user_returns_none(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("u", "f", "/p")
assert repo.get(created["id"], "other") is None
class TestListForUser:
+8 -2
View File
@@ -70,12 +70,18 @@ class TestGetById:
repo = _repo(pg_conn)
tool_id = _make_tool(pg_conn)
created = repo.upsert("u", tool_id, "t", "c")
fetched = repo.get(created["id"])
fetched = repo.get(created["id"], "u")
assert fetched["id"] == created["id"]
def test_get_nonexistent_returns_none(self, pg_conn):
repo = _repo(pg_conn)
assert repo.get("00000000-0000-0000-0000-000000000000") is None
assert repo.get("00000000-0000-0000-0000-000000000000", "u") is None
def test_get_wrong_user_returns_none(self, pg_conn):
repo = _repo(pg_conn)
tool_id = _make_tool(pg_conn)
created = repo.upsert("u", tool_id, "t", "c")
assert repo.get(created["id"], "other") is None
class TestDelete:
+20 -13
View File
@@ -35,23 +35,30 @@ class TestGet:
def test_get_by_id_and_user(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "p", "c")
fetched = repo.get(created["id"], user_id="user-1")
fetched = repo.get(created["id"], "user-1")
assert fetched["id"] == created["id"]
def test_get_by_id_only(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "p", "c")
fetched = repo.get(created["id"])
assert fetched is not None
def test_get_wrong_user_returns_none(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "p", "c")
assert repo.get(created["id"], user_id="user-other") is None
assert repo.get(created["id"], "user-other") is None
def test_get_nonexistent_returns_none(self, pg_conn):
repo = _repo(pg_conn)
assert repo.get("00000000-0000-0000-0000-000000000000") is None
assert repo.get("00000000-0000-0000-0000-000000000000", "user-1") is None
class TestGetForRendering:
def test_returns_prompt_without_user_scoping(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "p", "c")
fetched = repo.get_for_rendering(created["id"])
assert fetched is not None
assert fetched["id"] == created["id"]
def test_nonexistent_returns_none(self, pg_conn):
repo = _repo(pg_conn)
assert repo.get_for_rendering("00000000-0000-0000-0000-000000000000") is None
class TestListForUser:
@@ -70,7 +77,7 @@ class TestUpdate:
repo = _repo(pg_conn)
created = repo.create("user-1", "old", "old-content")
repo.update(created["id"], "user-1", "new", "new-content")
fetched = repo.get(created["id"])
fetched = repo.get(created["id"], "user-1")
assert fetched["name"] == "new"
assert fetched["content"] == "new-content"
@@ -78,7 +85,7 @@ class TestUpdate:
repo = _repo(pg_conn)
created = repo.create("user-1", "old", "old-content")
repo.update(created["id"], "user-other", "new", "new-content")
fetched = repo.get(created["id"])
fetched = repo.get(created["id"], "user-1")
assert fetched["name"] == "old"
@@ -87,13 +94,13 @@ class TestDelete:
repo = _repo(pg_conn)
created = repo.create("user-1", "p", "c")
repo.delete(created["id"], "user-1")
assert repo.get(created["id"]) is None
assert repo.get(created["id"], "user-1") is None
def test_delete_wrong_user_is_noop(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "p", "c")
repo.delete(created["id"], "user-other")
assert repo.get(created["id"]) is not None
assert repo.get(created["id"], "user-1") is not None
class TestFindOrCreate:
+32 -26
View File
@@ -43,28 +43,20 @@ class TestCreate:
class TestGet:
def test_get_by_id(self, pg_conn):
def test_get_existing(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("s")
fetched = repo.get(created["id"])
created = repo.create("s", user_id="user-1")
fetched = repo.get(created["id"], "user-1")
assert fetched["id"] == created["id"]
def test_get_nonexistent_returns_none(self, pg_conn):
repo = _repo(pg_conn)
assert repo.get("00000000-0000-0000-0000-000000000000") is None
assert repo.get("00000000-0000-0000-0000-000000000000", "user-1") is None
class TestGetForUser:
def test_get_for_correct_user(self, pg_conn):
def test_get_wrong_user_returns_none(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("s", user_id="user-1")
fetched = repo.get_for_user(created["id"], "user-1")
assert fetched["id"] == created["id"]
def test_get_for_wrong_user_returns_none(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("s", user_id="user-1")
assert repo.get_for_user(created["id"], "user-other") is None
assert repo.get(created["id"], "user-other") is None
class TestListForUser:
@@ -82,34 +74,48 @@ class TestUpdate:
def test_updates_name(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("old", user_id="u")
repo.update(created["id"], {"name": "new"})
fetched = repo.get(created["id"])
repo.update(created["id"], "u", {"name": "new"})
fetched = repo.get(created["id"], "u")
assert fetched["name"] == "new"
def test_updates_metadata(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("s", metadata={"a": 1})
repo.update(created["id"], {"metadata": {"a": 2, "b": 3}})
fetched = repo.get(created["id"])
created = repo.create("s", user_id="u", metadata={"a": 1})
repo.update(created["id"], "u", {"metadata": {"a": 2, "b": 3}})
fetched = repo.get(created["id"], "u")
assert fetched["metadata"] == {"a": 2, "b": 3}
def test_update_disallowed_field_is_noop(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("s")
repo.update(created["id"], {"id": "00000000-0000-0000-0000-000000000000"})
fetched = repo.get(created["id"])
created = repo.create("s", user_id="u")
repo.update(created["id"], "u", {"id": "00000000-0000-0000-0000-000000000000"})
fetched = repo.get(created["id"], "u")
assert fetched["id"] == created["id"]
def test_update_wrong_user_is_noop(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("old", user_id="u")
repo.update(created["id"], "other-user", {"name": "new"})
fetched = repo.get(created["id"], "u")
assert fetched["name"] == "old"
class TestDelete:
def test_deletes_source(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("s")
deleted = repo.delete(created["id"])
created = repo.create("s", user_id="u")
deleted = repo.delete(created["id"], "u")
assert deleted is True
assert repo.get(created["id"]) is None
assert repo.get(created["id"], "u") is None
def test_delete_nonexistent_returns_false(self, pg_conn):
repo = _repo(pg_conn)
deleted = repo.delete("00000000-0000-0000-0000-000000000000")
deleted = repo.delete("00000000-0000-0000-0000-000000000000", "u")
assert deleted is False
def test_delete_wrong_user_returns_false(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("s", user_id="u")
deleted = repo.delete(created["id"], "other-user")
assert deleted is False
assert repo.get(created["id"], "u") is not None
+45 -13
View File
@@ -51,12 +51,18 @@ class TestGet:
repo = _repo(pg_conn)
tool_id = _make_tool(pg_conn)
created = repo.create("u", tool_id, "t")
fetched = repo.get(created["id"])
fetched = repo.get(created["id"], "u")
assert fetched["id"] == created["id"]
def test_get_nonexistent_returns_none(self, pg_conn):
repo = _repo(pg_conn)
assert repo.get("00000000-0000-0000-0000-000000000000") is None
assert repo.get("00000000-0000-0000-0000-000000000000", "u") is None
def test_get_wrong_user_returns_none(self, pg_conn):
repo = _repo(pg_conn)
tool_id = _make_tool(pg_conn)
created = repo.create("u", tool_id, "t")
assert repo.get(created["id"], "other") is None
class TestListForUserTool:
@@ -83,14 +89,23 @@ class TestUpdateTitle:
repo = _repo(pg_conn)
tool_id = _make_tool(pg_conn)
created = repo.create("u", tool_id, "old")
updated = repo.update_title(created["id"], "new")
updated = repo.update_title(created["id"], "u", "new")
assert updated is True
fetched = repo.get(created["id"])
fetched = repo.get(created["id"], "u")
assert fetched["title"] == "new"
def test_update_nonexistent_returns_false(self, pg_conn):
repo = _repo(pg_conn)
assert repo.update_title("00000000-0000-0000-0000-000000000000", "x") is False
assert repo.update_title("00000000-0000-0000-0000-000000000000", "u", "x") is False
def test_update_wrong_user_returns_false(self, pg_conn):
repo = _repo(pg_conn)
tool_id = _make_tool(pg_conn)
created = repo.create("u", tool_id, "old")
updated = repo.update_title(created["id"], "other", "new")
assert updated is False
fetched = repo.get(created["id"], "u")
assert fetched["title"] == "old"
class TestSetCompleted:
@@ -98,17 +113,26 @@ class TestSetCompleted:
repo = _repo(pg_conn)
tool_id = _make_tool(pg_conn)
created = repo.create("u", tool_id, "t")
repo.set_completed(created["id"], True)
fetched = repo.get(created["id"])
repo.set_completed(created["id"], "u", True)
fetched = repo.get(created["id"], "u")
assert fetched["completed"] is True
def test_unmarks_completed(self, pg_conn):
repo = _repo(pg_conn)
tool_id = _make_tool(pg_conn)
created = repo.create("u", tool_id, "t")
repo.set_completed(created["id"], True)
repo.set_completed(created["id"], False)
fetched = repo.get(created["id"])
repo.set_completed(created["id"], "u", True)
repo.set_completed(created["id"], "u", False)
fetched = repo.get(created["id"], "u")
assert fetched["completed"] is False
def test_set_completed_wrong_user_returns_false(self, pg_conn):
repo = _repo(pg_conn)
tool_id = _make_tool(pg_conn)
created = repo.create("u", tool_id, "t")
result = repo.set_completed(created["id"], "other", True)
assert result is False
fetched = repo.get(created["id"], "u")
assert fetched["completed"] is False
@@ -117,10 +141,18 @@ class TestDelete:
repo = _repo(pg_conn)
tool_id = _make_tool(pg_conn)
created = repo.create("u", tool_id, "t")
deleted = repo.delete(created["id"])
deleted = repo.delete(created["id"], "u")
assert deleted is True
assert repo.get(created["id"]) is None
assert repo.get(created["id"], "u") is None
def test_delete_nonexistent_returns_false(self, pg_conn):
repo = _repo(pg_conn)
assert repo.delete("00000000-0000-0000-0000-000000000000") is False
assert repo.delete("00000000-0000-0000-0000-000000000000", "u") is False
def test_delete_wrong_user_returns_false(self, pg_conn):
repo = _repo(pg_conn)
tool_id = _make_tool(pg_conn)
created = repo.create("u", tool_id, "t")
deleted = repo.delete(created["id"], "other")
assert deleted is False
assert repo.get(created["id"], "u") is not None
@@ -36,12 +36,17 @@ class TestGet:
def test_get_existing(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "t")
fetched = repo.get(created["id"])
fetched = repo.get(created["id"], "user-1")
assert fetched["id"] == created["id"]
def test_get_nonexistent(self, pg_conn):
repo = _repo(pg_conn)
assert repo.get("00000000-0000-0000-0000-000000000000") is None
assert repo.get("00000000-0000-0000-0000-000000000000", "user-1") is None
def test_get_wrong_user_returns_none(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "t")
assert repo.get(created["id"], "user-other") is None
class TestListForUser:
@@ -59,28 +64,28 @@ class TestUpdate:
repo = _repo(pg_conn)
created = repo.create("user-1", "old_name")
repo.update(created["id"], "user-1", {"name": "new_name"})
fetched = repo.get(created["id"])
fetched = repo.get(created["id"], "user-1")
assert fetched["name"] == "new_name"
def test_updates_config(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "t", config={"a": 1})
repo.update(created["id"], "user-1", {"config": {"a": 2, "b": 3}})
fetched = repo.get(created["id"])
fetched = repo.get(created["id"], "user-1")
assert fetched["config"] == {"a": 2, "b": 3}
def test_update_wrong_user_is_noop(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "old")
repo.update(created["id"], "user-other", {"name": "new"})
fetched = repo.get(created["id"])
fetched = repo.get(created["id"], "user-1")
assert fetched["name"] == "old"
def test_ignores_disallowed_fields(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "t")
repo.update(created["id"], "user-1", {"id": "00000000-0000-0000-0000-000000000000"})
fetched = repo.get(created["id"])
fetched = repo.get(created["id"], "user-1")
assert fetched["id"] == created["id"]
@@ -90,11 +95,11 @@ class TestDelete:
created = repo.create("user-1", "t")
deleted = repo.delete(created["id"], "user-1")
assert deleted is True
assert repo.get(created["id"]) is None
assert repo.get(created["id"], "user-1") is None
def test_delete_wrong_user_returns_false(self, pg_conn):
repo = _repo(pg_conn)
created = repo.create("user-1", "t")
deleted = repo.delete(created["id"], "user-other")
assert deleted is False
assert repo.get(created["id"]) is not None
assert repo.get(created["id"], "user-1") is not None