chore: import upstream snapshot with attribution
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
"""Tests for migration 0010: Add chat tables.
|
||||
|
||||
Migration 0010 introduces the chat schema in its final clean shape:
|
||||
(originally numbered 0009; renumbered to 0010 when main's
|
||||
0009_default_fetch_mode_summary landed first.)
|
||||
|
||||
- chat_sessions with status as Enum (ChatSessionStatus)
|
||||
- chat_messages with content NOT NULL, no CHECK
|
||||
- chat_progress_steps as a separate table for transient research
|
||||
progress events (no longer mixed into chat_messages)
|
||||
- research_history.chat_session_id (FK SET NULL) + step_count
|
||||
|
||||
The migration is fresh-install only; legacy 0007-shape dev DBs must
|
||||
be recreated (or the chat tables dropped manually) before running.
|
||||
|
||||
Tests cover:
|
||||
- Fresh-install path: chat tables exist, content is NOT NULL, no
|
||||
CHECK constraint, status is Enum-typed.
|
||||
- Idempotency: re-running migrations on a head DB is a no-op.
|
||||
- Downgrade is NotImplementedError.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from alembic import command
|
||||
from sqlalchemy import create_engine, event, inspect
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from local_deep_research.database.alembic_runner import (
|
||||
get_alembic_config,
|
||||
run_migrations,
|
||||
stamp_database,
|
||||
)
|
||||
|
||||
|
||||
_PARTIAL_UNIQUE_INDEX_NAME = "ux_research_history_chat_session_in_progress"
|
||||
|
||||
|
||||
def _run_downgrade_to(engine, revision):
|
||||
config = get_alembic_config(engine)
|
||||
with engine.begin() as conn:
|
||||
config.attributes["connection"] = conn
|
||||
command.downgrade(config, revision)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fresh_engine(tmp_path):
|
||||
db_path = tmp_path / "fresh_0010.db"
|
||||
engine = create_engine(f"sqlite:///{db_path}")
|
||||
yield engine
|
||||
engine.dispose()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fully_migrated_engine(tmp_path):
|
||||
db_path = tmp_path / "fully_migrated_0010.db"
|
||||
engine = create_engine(f"sqlite:///{db_path}")
|
||||
run_migrations(engine)
|
||||
yield engine
|
||||
engine.dispose()
|
||||
|
||||
|
||||
class TestSchemaShape:
|
||||
"""Chat schema invariants are in place after 0010 runs."""
|
||||
|
||||
def test_chat_tables_exist(self, fully_migrated_engine):
|
||||
insp = inspect(fully_migrated_engine)
|
||||
for table in ("chat_sessions", "chat_messages", "chat_progress_steps"):
|
||||
assert insp.has_table(table), f"{table} missing"
|
||||
|
||||
def test_chat_messages_content_is_not_null(self, fully_migrated_engine):
|
||||
insp = inspect(fully_migrated_engine)
|
||||
cols = {c["name"]: c for c in insp.get_columns("chat_messages")}
|
||||
assert "content" in cols
|
||||
assert cols["content"]["nullable"] is False
|
||||
|
||||
def test_chat_messages_has_no_legacy_check(self, fully_migrated_engine):
|
||||
insp = inspect(fully_migrated_engine)
|
||||
checks = insp.get_check_constraints("chat_messages")
|
||||
names = {c.get("name") for c in checks}
|
||||
assert "ck_chat_message_has_content_source" not in names
|
||||
|
||||
def test_chat_session_status_typed(self, fully_migrated_engine):
|
||||
"""status is typed VARCHAR with default 'active'.
|
||||
|
||||
Note: SQLAlchemy's Enum on SQLite stores as VARCHAR sized to
|
||||
the longest enum value but does NOT emit a DB-level CHECK
|
||||
unless `create_constraint=True` is explicitly set. The
|
||||
codebase relies on ORM-layer validation (ChatSessionStatus(value))
|
||||
for value enforcement — same pattern as ChatRole/ChatMessageType.
|
||||
"""
|
||||
insp = inspect(fully_migrated_engine)
|
||||
cols = {c["name"]: c for c in insp.get_columns("chat_sessions")}
|
||||
assert "status" in cols
|
||||
# VARCHAR sized to the longest enum value ('archived' = 8 chars)
|
||||
type_str = str(cols["status"]["type"]).upper()
|
||||
assert "VARCHAR" in type_str
|
||||
|
||||
def test_research_history_chat_session_id_present(
|
||||
self, fully_migrated_engine
|
||||
):
|
||||
insp = inspect(fully_migrated_engine)
|
||||
cols = {c["name"] for c in insp.get_columns("research_history")}
|
||||
assert "chat_session_id" in cols
|
||||
assert "step_count" in cols
|
||||
|
||||
def test_chat_progress_steps_unique_per_research_seq(
|
||||
self, fully_migrated_engine
|
||||
):
|
||||
insp = inspect(fully_migrated_engine)
|
||||
uniques = insp.get_unique_constraints("chat_progress_steps")
|
||||
names = {u.get("name") for u in uniques}
|
||||
assert "uq_chat_progress_step_research_seq" in names
|
||||
|
||||
def test_composite_indexes_exist_after_upgrade(self, fully_migrated_engine):
|
||||
"""Composite (session_id, created_at) indexes serve the load-older
|
||||
pagination query in chat/service.py::get_session_messages. Without
|
||||
them, SQLite uses the single-column session_id index and sorts in
|
||||
memory — break-even at ~500 rows/session.
|
||||
"""
|
||||
insp = inspect(fully_migrated_engine)
|
||||
|
||||
msg_idx = {
|
||||
i["name"]: i["column_names"]
|
||||
for i in insp.get_indexes("chat_messages")
|
||||
}
|
||||
assert "ix_chat_messages_session_created" in msg_idx
|
||||
assert msg_idx["ix_chat_messages_session_created"] == [
|
||||
"session_id",
|
||||
"created_at",
|
||||
]
|
||||
|
||||
step_idx = {
|
||||
i["name"]: i["column_names"]
|
||||
for i in insp.get_indexes("chat_progress_steps")
|
||||
}
|
||||
assert "ix_chat_progress_steps_session_created" in step_idx
|
||||
assert step_idx["ix_chat_progress_steps_session_created"] == [
|
||||
"session_id",
|
||||
"created_at",
|
||||
]
|
||||
|
||||
|
||||
class TestIdempotency:
|
||||
"""Re-running migrations on a head DB is a no-op."""
|
||||
|
||||
def test_double_migrate_no_error(self, fresh_engine):
|
||||
run_migrations(fresh_engine)
|
||||
# Second run must not raise.
|
||||
run_migrations(fresh_engine)
|
||||
insp = inspect(fresh_engine)
|
||||
assert insp.has_table("chat_progress_steps")
|
||||
|
||||
|
||||
class TestDowngrade:
|
||||
"""Downgrade is not supported and raises NotImplementedError.
|
||||
|
||||
Why: SQLite ALTER TABLE forbids dropping a column that is the
|
||||
target of a FOREIGN KEY definition, and alembic's batch_alter_table
|
||||
cannot rebuild research_history due to unnamed legacy constraints
|
||||
on that table. The project is dev-stage; recreate the DB to roll
|
||||
back. The parametrized stairway/down-leaves-no-residual tests in
|
||||
test_alembic_migrations.py exempt 0010 via NON_REVERSIBLE_REVISIONS.
|
||||
"""
|
||||
|
||||
def test_downgrade_raises_not_implemented(self, fully_migrated_engine):
|
||||
with pytest.raises(NotImplementedError):
|
||||
_run_downgrade_to(fully_migrated_engine, "0008")
|
||||
|
||||
|
||||
class TestExistingDataBackfill:
|
||||
"""Verify 0010 leaves pre-existing research_history rows in a sane state.
|
||||
|
||||
Note: we cannot use `run_migrations(target="0008")` to land at the
|
||||
pre-0010 state because 0001 uses `Base.metadata.create_all` against the
|
||||
live `Base`, which already includes `chat_session_id` and `step_count`.
|
||||
Instead we hand-build a minimal pre-0010 `research_history` and stamp
|
||||
the DB at 0009 so 0010 forward runs the actual ADD COLUMN path.
|
||||
"""
|
||||
|
||||
def test_step_count_backfilled_for_existing_rows(self, tmp_path):
|
||||
engine = create_engine(f"sqlite:///{tmp_path}/m.db")
|
||||
|
||||
# Hand-build pre-0010 research_history (subset of NOT NULL cols).
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"CREATE TABLE research_history ("
|
||||
" id TEXT PRIMARY KEY, "
|
||||
" query TEXT NOT NULL, "
|
||||
" mode TEXT NOT NULL, "
|
||||
" status TEXT NOT NULL, "
|
||||
" created_at TEXT NOT NULL"
|
||||
")"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO research_history (id, query, mode, status, "
|
||||
"created_at) VALUES ('r1', 'q', 'quick', 'completed', "
|
||||
"'2026-01-01T00:00:00')"
|
||||
)
|
||||
)
|
||||
|
||||
# Stamp at 0009 (main's settings-only fetch_mode migration) so
|
||||
# 0010 (chat tables) is the next forward step we exercise. 0010
|
||||
# is the migration that actually ADD COLUMNs onto our hand-built
|
||||
# research_history shape.
|
||||
stamp_database(engine, "0009")
|
||||
run_migrations(engine, target="head")
|
||||
|
||||
with engine.connect() as conn:
|
||||
row = conn.execute(
|
||||
text(
|
||||
"SELECT step_count, chat_session_id "
|
||||
"FROM research_history WHERE id = 'r1'"
|
||||
)
|
||||
).first()
|
||||
assert row is not None
|
||||
# Relies on 0010 using server_default="0" (SQL-side, applied by
|
||||
# SQLite ADD COLUMN at DDL time). If a future refactor switches
|
||||
# to Python-side default=0, this assertion fails for the pre-
|
||||
# existing 'r1' row — exactly the regression we want to catch.
|
||||
assert row.step_count == 0
|
||||
assert row.chat_session_id is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Partial unique index: at-most-one-in-progress per chat_session_id
|
||||
#
|
||||
# Originally lived in a separate migration 0011 with a separate test file;
|
||||
# folded into 0010 to keep the chat schema landing in a single migration
|
||||
# (chat is unreleased; one migration is easier to maintain than two).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fully_migrated_engine_with_fk(tmp_path):
|
||||
"""Fully migrated SQLite engine with FK enforcement on every connection.
|
||||
|
||||
Required for the partial-unique-index tests because they depend on
|
||||
SQLite enforcing the constraint at INSERT time, which only happens
|
||||
when ``PRAGMA foreign_keys = ON`` is active.
|
||||
"""
|
||||
db_path = tmp_path / "0010_partial_unique_test.db"
|
||||
engine = create_engine(f"sqlite:///{db_path}")
|
||||
|
||||
@event.listens_for(engine, "connect")
|
||||
def _enable_fk(dbapi_connection, _):
|
||||
dbapi_connection.execute("PRAGMA foreign_keys = ON")
|
||||
|
||||
run_migrations(engine, target="head")
|
||||
yield engine
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def _seed_chat_session(conn, sid):
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO chat_sessions "
|
||||
"(id, status, message_count, created_at) "
|
||||
"VALUES (:id, 'active', 0, '2026-01-01T00:00:00')"
|
||||
),
|
||||
{"id": sid},
|
||||
)
|
||||
|
||||
|
||||
def _insert_research(conn, *, rid, sid, status):
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO research_history "
|
||||
"(id, query, mode, status, created_at, chat_session_id) "
|
||||
"VALUES (:rid, 'q', 'quick', :status, "
|
||||
"'2026-01-01T00:00:00', :sid)"
|
||||
),
|
||||
{"rid": rid, "sid": sid, "status": status},
|
||||
)
|
||||
|
||||
|
||||
class TestPartialUniqueInProgressIndex:
|
||||
"""The partial unique index closes a SELECT-then-INSERT race in
|
||||
chat/routes.py. Verify the constraint actually fires at the DB."""
|
||||
|
||||
def test_partial_unique_index_exists_after_upgrade(
|
||||
self, fully_migrated_engine_with_fk
|
||||
):
|
||||
inspector = inspect(fully_migrated_engine_with_fk)
|
||||
indexes = {
|
||||
idx["name"]: idx
|
||||
for idx in inspector.get_indexes("research_history")
|
||||
}
|
||||
assert _PARTIAL_UNIQUE_INDEX_NAME in indexes
|
||||
idx = indexes[_PARTIAL_UNIQUE_INDEX_NAME]
|
||||
# SQLAlchemy's SQLite inspector returns 1 / 0 rather than True /
|
||||
# False for the unique flag, so compare on truthiness.
|
||||
assert bool(idx["unique"])
|
||||
assert idx["column_names"] == ["chat_session_id"]
|
||||
|
||||
def test_second_in_progress_for_same_chat_session_blocked(
|
||||
self, fully_migrated_engine_with_fk
|
||||
):
|
||||
engine = fully_migrated_engine_with_fk
|
||||
with engine.begin() as conn:
|
||||
_seed_chat_session(conn, "s1")
|
||||
_insert_research(conn, rid="r1", sid="s1", status="in_progress")
|
||||
|
||||
with engine.connect() as conn:
|
||||
with pytest.raises(IntegrityError):
|
||||
with conn.begin():
|
||||
_insert_research(
|
||||
conn, rid="r2", sid="s1", status="in_progress"
|
||||
)
|
||||
|
||||
def test_completed_runs_for_same_chat_session_allowed(
|
||||
self, fully_migrated_engine_with_fk
|
||||
):
|
||||
"""Partial: only in_progress rows are unique; completed history
|
||||
of arbitrarily many runs per chat session must remain allowed."""
|
||||
engine = fully_migrated_engine_with_fk
|
||||
with engine.begin() as conn:
|
||||
_seed_chat_session(conn, "s1")
|
||||
_insert_research(conn, rid="r1", sid="s1", status="completed")
|
||||
_insert_research(conn, rid="r2", sid="s1", status="completed")
|
||||
_insert_research(conn, rid="r3", sid="s1", status="failed")
|
||||
|
||||
with engine.connect() as conn:
|
||||
count = conn.execute(
|
||||
text(
|
||||
"SELECT COUNT(*) FROM research_history "
|
||||
"WHERE chat_session_id='s1'"
|
||||
)
|
||||
).scalar()
|
||||
assert count == 3
|
||||
|
||||
def test_in_progress_with_null_chat_session_id_unconstrained(
|
||||
self, fully_migrated_engine_with_fk
|
||||
):
|
||||
"""Partial: NULL chat_session_id rows must be unconstrained so
|
||||
non-chat research (news, scheduler, direct API) is unaffected."""
|
||||
engine = fully_migrated_engine_with_fk
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO research_history "
|
||||
"(id, query, mode, status, created_at) "
|
||||
"VALUES (:rid, 'q', 'quick', 'in_progress', "
|
||||
"'2026-01-01T00:00:00')"
|
||||
),
|
||||
{"rid": "r1"},
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO research_history "
|
||||
"(id, query, mode, status, created_at) "
|
||||
"VALUES (:rid, 'q', 'quick', 'in_progress', "
|
||||
"'2026-01-01T00:00:00')"
|
||||
),
|
||||
{"rid": "r2"},
|
||||
)
|
||||
|
||||
with engine.connect() as conn:
|
||||
count = conn.execute(
|
||||
text(
|
||||
"SELECT COUNT(*) FROM research_history "
|
||||
"WHERE chat_session_id IS NULL"
|
||||
)
|
||||
).scalar()
|
||||
assert count == 2
|
||||
|
||||
def test_completing_a_run_releases_the_in_progress_slot(
|
||||
self, fully_migrated_engine_with_fk
|
||||
):
|
||||
"""After r1 transitions away from in_progress, r2 must be able
|
||||
to claim the slot."""
|
||||
engine = fully_migrated_engine_with_fk
|
||||
with engine.begin() as conn:
|
||||
_seed_chat_session(conn, "s1")
|
||||
_insert_research(conn, rid="r1", sid="s1", status="in_progress")
|
||||
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"UPDATE research_history "
|
||||
"SET status='completed' WHERE id='r1'"
|
||||
)
|
||||
)
|
||||
|
||||
with engine.begin() as conn:
|
||||
_insert_research(conn, rid="r2", sid="s1", status="in_progress")
|
||||
|
||||
with engine.connect() as conn:
|
||||
count = conn.execute(
|
||||
text(
|
||||
"SELECT COUNT(*) FROM research_history "
|
||||
"WHERE chat_session_id='s1' AND status='in_progress'"
|
||||
)
|
||||
).scalar()
|
||||
assert count == 1
|
||||
Reference in New Issue
Block a user