Files
OpenOPC/tests/test_store_task_deletion.py
T
2026-07-01 17:56:31 +08:00

727 lines
33 KiB
Python

from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from opc.core.models import SessionLinkRecord, SessionMessageRecord, SessionRecord, Task
from opc.database.store import OPCStore
class TestOPCStoreTaskDeletion(unittest.IsolatedAsyncioTestCase):
async def _count(self, store: OPCStore, table: str, where: str = "", params: tuple = ()) -> int:
assert store._db is not None
query = f"SELECT COUNT(*) FROM {table}"
if where:
query += f" WHERE {where}"
async with store._db.execute(query, params) as cursor:
row = await cursor.fetchone()
return int(row[0] or 0)
async def test_hard_delete_task_removes_task_and_session_state(self) -> None:
"""Hard delete should remove the task row and linked session state."""
with tempfile.TemporaryDirectory() as tmpdir:
store = OPCStore(Path(tmpdir) / "tasks.db")
await store.initialize()
try:
task = Task(
id="task-1",
session_id="session-1",
title="New Chat",
project_id="default",
)
await store.save_task(task)
await store.save_session(
SessionRecord(
session_id="session-1",
project_id="default",
title="New Chat",
)
)
await store.save_session_message(
SessionMessageRecord(
message_id="msg-1",
session_id="session-1",
task_id="task-1",
role="user",
)
)
await store.save_session_link(
SessionLinkRecord(
link_id="link-1",
project_id="default",
session_id="parent-session",
linked_session_id="session-1",
task_id="task-1",
)
)
await store.hard_delete_task(task.id, task.session_id)
self.assertIsNone(await store.get_task(task.id))
self.assertIsNone(await store.get_session(task.session_id))
self.assertEqual(await store.get_session_links("parent-session"), [])
async with store._db.execute(
"SELECT COUNT(*) FROM session_messages WHERE session_id = ?",
(task.session_id,),
) as cursor:
row = await cursor.fetchone()
self.assertEqual(row[0], 0)
finally:
await store.close()
async def test_hard_delete_task_removes_company_runtime_artifacts(self) -> None:
"""Deleting a top-level chat should remove its company runtime graph."""
with tempfile.TemporaryDirectory() as tmpdir:
store = OPCStore(Path(tmpdir) / "tasks.db")
await store.initialize()
try:
task = Task(
id="task-runtime",
session_id="session-runtime",
title="Company Chat",
project_id="default",
)
await store.save_task(task)
await store.save_session(
SessionRecord(
session_id=task.session_id or "",
project_id="default",
title="Company Chat",
)
)
assert store._db is not None
now = "2026-05-12T00:00:00"
run_id = "run-runtime"
cell_id = "cell-runtime"
work_item_id = "wi-runtime"
role_session_id = "role-runtime"
member_session_id = "member-runtime"
await store._db.execute(
"INSERT INTO delegation_runs (run_id, session_id, created_at, updated_at) VALUES (?, ?, ?, ?)",
(run_id, task.session_id, now, now),
)
await store._db.execute(
"INSERT INTO delegation_cells (cell_id, run_id, created_at, updated_at) VALUES (?, ?, ?, ?)",
(cell_id, run_id, now, now),
)
await store._db.execute(
"""
INSERT INTO delegation_work_items
(work_item_id, run_id, cell_id, phase, role_runtime_session_id,
claimed_by_role_runtime_session_id, metadata, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
work_item_id,
run_id,
cell_id,
"running",
role_session_id,
role_session_id,
'{"runtime_task_id":"task-runtime"}',
now,
now,
),
)
await store._db.execute(
"""
INSERT INTO work_item_runtime_links
(work_item_id, runtime_task_id, created_at, updated_at)
VALUES (?, ?, ?, ?)
""",
(work_item_id, task.id, now, now),
)
await store._db.execute(
"INSERT INTO delegation_events (event_id, run_id, work_item_id, event_type, created_at) VALUES (?, ?, ?, ?, ?)",
("event-delegation", run_id, work_item_id, "work_started", now),
)
await store._db.execute(
"INSERT INTO delegation_role_sessions (role_session_id, run_id, role_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
(role_session_id, run_id, "engineer", now, now),
)
await store._db.execute(
"INSERT INTO role_runtime_sessions (role_session_id, run_id, role_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
(role_session_id, run_id, "engineer", now, now),
)
await store._db.execute(
"INSERT INTO team_instances (team_instance_id, run_id, team_id, session_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
("team-runtime", run_id, "team", task.session_id, now, now),
)
await store._db.execute(
"""
INSERT INTO seat_states
(seat_state_id, team_instance_id, run_id, team_id, seat_id,
member_session_id, role_runtime_session_id, current_task_id,
current_work_item_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"seat-runtime",
"team-runtime",
run_id,
"team",
"seat",
member_session_id,
role_session_id,
task.id,
work_item_id,
now,
now,
),
)
await store._db.execute(
"""
INSERT INTO runtime_sessions
(runtime_session_id, session_id, task_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
""",
(role_session_id, None, None, now, now),
)
await store._db.execute(
"""
INSERT INTO runtime_sessions
(runtime_session_id, session_id, task_id, metadata, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
member_session_id,
None,
None,
'{"role_session_id":"role-runtime","run_id":"run-runtime"}',
now,
now,
),
)
await store._db.execute(
"INSERT INTO runtime_events (event_id, runtime_session_id, event_type, created_at) VALUES (?, ?, ?, ?)",
("event-runtime", role_session_id, "message", now),
)
await store._db.execute(
"INSERT INTO runtime_events (event_id, runtime_session_id, event_type, created_at) VALUES (?, ?, ?, ?)",
("event-member-runtime", member_session_id, "message", now),
)
await store._db.execute(
"INSERT INTO runtime_transcript_entries (entry_id, runtime_session_id, content, created_at) VALUES (?, ?, ?, ?)",
("transcript-runtime", role_session_id, "hello", now),
)
await store._db.execute(
"""
INSERT INTO runtime_tool_calls
(call_record_id, runtime_session_id, tool_call_id, tool_name, created_at)
VALUES (?, ?, ?, ?, ?)
""",
("tool-call-runtime", role_session_id, "call-1", "shell", now),
)
await store._db.execute(
"""
INSERT INTO runtime_tool_results
(result_record_id, runtime_session_id, tool_call_id, tool_name, created_at)
VALUES (?, ?, ?, ?, ?)
""",
("tool-result-runtime", role_session_id, "call-1", "shell", now),
)
await store._db.execute(
"""
INSERT INTO runtime_permission_grants
(grant_id, runtime_session_id, scope, tool_name, candidate, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
("grant-runtime", role_session_id, "once", "shell", "godot", now),
)
await store._db.execute(
"""
INSERT INTO runtime_subagent_runs
(subagent_run_id, runtime_session_id, task_id, agent_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
("subagent-runtime", role_session_id, task.id, "codex", now, now),
)
await store._db.execute(
"""
INSERT INTO runtime_worktree_sessions
(worktree_session_id, runtime_session_id, task_id, path, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
("worktree-runtime", role_session_id, task.id, "/tmp/work", now, now),
)
await store._db.execute(
"""
INSERT INTO runtime_compaction_boundaries
(boundary_id, runtime_session_id, task_id, created_at)
VALUES (?, ?, ?, ?)
""",
("boundary-runtime", role_session_id, task.id, now),
)
await store._db.execute(
"""
INSERT INTO execution_checkpoints
(checkpoint_id, session_id, checkpoint_type, task_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
("checkpoint-runtime", task.session_id, "company_delivery_feedback", "", now, now),
)
await store._db.execute(
"""
INSERT INTO external_sessions
(session_key, agent_type, session_id, opc_session_id, task_id, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
("ext-runtime", "codex", "provider-session", task.session_id, "", now),
)
await store._db.execute(
"""
INSERT INTO role_memory
(memory_id, project_id, role_id, scope, summary, details, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
"role-memory-runtime",
"default",
"engineer",
"project",
"Keep durable role memory",
'{"task_id":"task-runtime","run_id":"run-runtime"}',
now,
),
)
await store._db.execute(
"""
INSERT INTO agent_memory_snapshots
(snapshot_id, project_id, session_id, employee_id, role_id,
memory_scope, memory_kind, summary_message_id,
source_boundary_message_id, summary_text, memory_text,
metadata, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"agent-memory-runtime",
"default",
"",
"employee-engineer",
"engineer",
"project",
"final",
"",
"",
"Keep durable employee memory",
"Long-lived employee memory should survive chat deletion.",
'{"task_ids":["task-runtime"],"session_ids":["session-runtime"],"run_id":"run-runtime"}',
now,
now,
),
)
for event_id, payload in (
("evt-task", '{"task_id":"task-runtime"}'),
("evt-run", '{"run_id":"run-runtime"}'),
("evt-work", '{"work_item_id":"wi-runtime"}'),
("evt-runtime", '{"runtime_session_id":"role-runtime"}'),
("evt-role", '{"member_session_id":"role-runtime"}'),
("evt-member-session", '{"member_session_id":"member-runtime"}'),
):
await store._db.execute(
"INSERT INTO events (event_id, event_type, payload, timestamp) VALUES (?, ?, ?, ?)",
(event_id, "test", payload, now),
)
await store._db.commit()
await store.hard_delete_task(task.id, task.session_id)
for table in (
"tasks",
"sessions",
"delegation_runs",
"delegation_cells",
"delegation_work_items",
"work_item_runtime_links",
"delegation_events",
"delegation_role_sessions",
"role_runtime_sessions",
"team_instances",
"seat_states",
"runtime_sessions",
"runtime_events",
"runtime_transcript_entries",
"runtime_tool_calls",
"runtime_tool_results",
"runtime_permission_grants",
"runtime_subagent_runs",
"runtime_worktree_sessions",
"runtime_compaction_boundaries",
"execution_checkpoints",
"external_sessions",
"events",
):
self.assertEqual(await self._count(store, table), 0, table)
self.assertEqual(
await self._count(store, "role_memory", "memory_id = ?", ("role-memory-runtime",)),
1,
)
self.assertEqual(
await self._count(store, "agent_memory_snapshots", "snapshot_id = ?", ("agent-memory-runtime",)),
1,
)
finally:
await store.close()
async def test_delete_company_runtime_artifacts_for_session_cleans_orphan_run(self) -> None:
"""Session cleanup should work even after task/session rows are gone."""
with tempfile.TemporaryDirectory() as tmpdir:
store = OPCStore(Path(tmpdir) / "tasks.db")
await store.initialize()
try:
assert store._db is not None
now = "2026-05-12T00:00:00"
deleted_session_id = "session-deleted"
run_id = "run-deleted"
work_item_id = "wi-deleted"
role_session_id = "role-runtime-deleted"
member_session_id = "role-session::default::session-deleted::ceo::employee"
await store._db.execute(
"INSERT INTO delegation_runs (run_id, session_id, status, lifecycle_status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
(run_id, deleted_session_id, "running", "active", now, now),
)
await store._db.execute(
"INSERT INTO delegation_runs (run_id, session_id, status, lifecycle_status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
("run-keep", "session-keep", "running", "active", now, now),
)
await store._db.execute(
"INSERT INTO delegation_cells (cell_id, run_id, created_at, updated_at) VALUES (?, ?, ?, ?)",
("cell-deleted", run_id, now, now),
)
await store._db.execute(
"""
INSERT INTO delegation_work_items
(work_item_id, run_id, cell_id, phase, role_runtime_session_id,
claimed_by_role_runtime_session_id, metadata, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
work_item_id,
run_id,
"cell-deleted",
"running",
role_session_id,
role_session_id,
'{"session_scope_id":"session-deleted"}',
now,
now,
),
)
await store._db.execute(
"INSERT INTO delegation_events (event_id, run_id, work_item_id, event_type, created_at) VALUES (?, ?, ?, ?, ?)",
("delegation-event-deleted", run_id, work_item_id, "work_started", now),
)
await store._db.execute(
"INSERT INTO delegation_role_sessions (role_session_id, run_id, role_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
(role_session_id, run_id, "ceo", now, now),
)
await store._db.execute(
"INSERT INTO role_runtime_sessions (role_session_id, run_id, role_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
(role_session_id, run_id, "ceo", now, now),
)
await store._db.execute(
"INSERT INTO team_instances (team_instance_id, run_id, team_id, session_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
("team-deleted", run_id, "team", deleted_session_id, now, now),
)
await store._db.execute(
"""
INSERT INTO seat_states
(seat_state_id, team_instance_id, run_id, team_id, seat_id,
member_session_id, role_runtime_session_id, current_work_item_id,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"seat-deleted",
"team-deleted",
run_id,
"team",
"seat",
member_session_id,
role_session_id,
work_item_id,
now,
now,
),
)
await store._db.execute(
"""
INSERT INTO runtime_sessions
(runtime_session_id, metadata, created_at, updated_at)
VALUES (?, ?, ?, ?)
""",
(
member_session_id,
'{"role_session_id":"role-runtime-deleted","run_id":"run-deleted"}',
now,
now,
),
)
await store._db.execute(
"INSERT INTO runtime_events (event_id, runtime_session_id, event_type, created_at) VALUES (?, ?, ?, ?)",
("runtime-event-deleted", member_session_id, "message", now),
)
await store._db.execute(
"INSERT INTO runtime_events (event_id, runtime_session_id, event_type, created_at) VALUES (?, ?, ?, ?)",
("runtime-event-run-deleted", run_id, "serial_queue_reconciled", now),
)
await store._db.execute(
"""
INSERT INTO execution_checkpoints
(checkpoint_id, session_id, checkpoint_type, payload, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
"checkpoint-deleted",
deleted_session_id,
"company_recruitment_confirmation",
'{"run_id":"run-deleted"}',
now,
now,
),
)
await store._db.execute(
"""
INSERT INTO external_sessions
(session_key, agent_type, session_id, opc_session_id, task_id, metadata, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
"external-deleted",
"codex",
"provider",
deleted_session_id,
"",
'{"run_id":"run-deleted"}',
now,
),
)
await store._db.execute(
"""
INSERT INTO role_memory
(memory_id, project_id, role_id, scope, summary, details, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
"role-memory-keep",
"default",
"ceo",
"project",
"Keep durable role memory",
'{"session_id":"session-deleted","run_id":"run-deleted"}',
now,
),
)
await store._db.execute(
"""
INSERT INTO agent_memory_snapshots
(snapshot_id, project_id, session_id, employee_id, role_id,
memory_scope, memory_kind, summary_message_id,
source_boundary_message_id, summary_text, memory_text,
metadata, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"agent-memory-keep",
"default",
"",
"employee-ceo",
"ceo",
"project",
"final",
"",
"",
"Keep durable employee memory",
"Long-lived employee memory should survive chat deletion.",
'{"session_ids":["session-deleted"],"run_id":"run-deleted"}',
now,
now,
),
)
for event_id, payload in (
("evt-deleted-session", '{"member_session_id":"role-session::default::session-deleted::ceo::employee"}'),
("evt-deleted-run", '{"run_id":"run-deleted"}'),
("evt-keep", '{"run_id":"run-keep"}'),
):
await store._db.execute(
"INSERT INTO events (event_id, event_type, payload, timestamp) VALUES (?, ?, ?, ?)",
(event_id, "test", payload, now),
)
await store._db.commit()
await store.delete_company_runtime_artifacts_for_session(deleted_session_id)
self.assertEqual(
await self._count(store, "delegation_runs", "run_id = ?", (run_id,)),
0,
)
self.assertEqual(
await self._count(store, "delegation_runs", "run_id = ?", ("run-keep",)),
1,
)
for table in (
"delegation_cells",
"delegation_work_items",
"delegation_events",
"delegation_role_sessions",
"role_runtime_sessions",
"team_instances",
"seat_states",
"runtime_sessions",
"runtime_events",
"execution_checkpoints",
"external_sessions",
):
self.assertEqual(await self._count(store, table), 0, table)
self.assertEqual(await self._count(store, "events"), 1)
self.assertEqual(
await self._count(store, "events", "event_id = ?", ("evt-keep",)),
1,
)
self.assertEqual(
await self._count(store, "role_memory", "memory_id = ?", ("role-memory-keep",)),
1,
)
self.assertEqual(
await self._count(store, "agent_memory_snapshots", "snapshot_id = ?", ("agent-memory-keep",)),
1,
)
finally:
await store.close()
async def test_hard_delete_child_task_preserves_parent_company_run(self) -> None:
"""Deleting a child work-item chat must not delete the parent run."""
with tempfile.TemporaryDirectory() as tmpdir:
store = OPCStore(Path(tmpdir) / "tasks.db")
await store.initialize()
try:
parent = Task(
id="task-parent",
session_id="session-parent",
title="Parent Chat",
project_id="default",
)
child = Task(
id="task-child",
session_id="session-child",
parent_session_id=parent.session_id,
title="Child Work Item",
project_id="default",
)
await store.save_task(parent)
await store.save_task(child)
await store.save_session(SessionRecord(session_id=parent.session_id or "", project_id="default"))
await store.save_session(
SessionRecord(
session_id=child.session_id or "",
project_id="default",
parent_session_id=parent.session_id,
)
)
assert store._db is not None
now = "2026-05-12T00:00:00"
await store._db.execute(
"INSERT INTO delegation_runs (run_id, session_id, created_at, updated_at) VALUES (?, ?, ?, ?)",
("run-parent", parent.session_id, now, now),
)
await store._db.execute(
"INSERT INTO delegation_cells (cell_id, run_id, created_at, updated_at) VALUES (?, ?, ?, ?)",
("cell-parent", "run-parent", now, now),
)
await store._db.execute(
"""
INSERT INTO delegation_work_items
(work_item_id, run_id, cell_id, phase, role_runtime_session_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
("wi-child", "run-parent", "cell-parent", "running", "role-child", now, now),
)
await store._db.execute(
"""
INSERT INTO delegation_work_items
(work_item_id, run_id, cell_id, phase, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
("wi-keep", "run-parent", "cell-parent", "ready", now, now),
)
await store._db.execute(
"INSERT INTO work_item_runtime_links (work_item_id, runtime_task_id, created_at, updated_at) VALUES (?, ?, ?, ?)",
("wi-child", child.id, now, now),
)
await store._db.execute(
"INSERT INTO runtime_sessions (runtime_session_id, created_at, updated_at) VALUES (?, ?, ?)",
("role-child", now, now),
)
await store._db.execute(
"""
INSERT INTO runtime_sessions
(runtime_session_id, session_id, task_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
""",
("runtime-child", child.session_id, child.id, now, now),
)
await store._db.execute(
"INSERT INTO runtime_events (event_id, runtime_session_id, event_type, created_at) VALUES (?, ?, ?, ?)",
("event-role-child", "role-child", "message", now),
)
await store._db.execute(
"INSERT INTO runtime_events (event_id, runtime_session_id, event_type, created_at) VALUES (?, ?, ?, ?)",
("event-runtime-child", "runtime-child", "message", now),
)
await store._db.execute(
"""
INSERT INTO runtime_transcript_entries
(entry_id, runtime_session_id, task_id, session_id, content, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
("transcript-child", "role-child", child.id, child.session_id, "child output", now),
)
await store._db.execute(
"""
INSERT INTO seat_states
(seat_state_id, team_instance_id, run_id, team_id, seat_id,
current_task_id, current_work_item_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
("seat-parent", "team-parent", "run-parent", "team", "seat", child.id, "wi-child", now, now),
)
await store._db.commit()
await store.hard_delete_task(child.id, child.session_id)
self.assertIsNone(await store.get_task(child.id))
self.assertIsNotNone(await store.get_task(parent.id))
self.assertEqual(await self._count(store, "delegation_runs"), 1)
self.assertEqual(await self._count(store, "delegation_cells"), 1)
self.assertEqual(await self._count(store, "delegation_work_items"), 1)
self.assertEqual(
await self._count(store, "delegation_work_items", "work_item_id = ?", ("wi-keep",)),
1,
)
self.assertEqual(await self._count(store, "work_item_runtime_links"), 0)
self.assertEqual(await self._count(store, "runtime_sessions"), 1)
self.assertEqual(
await self._count(store, "runtime_sessions", "runtime_session_id = ?", ("role-child",)),
1,
)
self.assertEqual(await self._count(store, "runtime_events"), 1)
self.assertEqual(await self._count(store, "runtime_transcript_entries"), 0)
self.assertEqual(await self._count(store, "seat_states"), 1)
async with store._db.execute(
"SELECT current_task_id, current_work_item_id FROM seat_states WHERE seat_state_id = ?",
("seat-parent",),
) as cursor:
row = await cursor.fetchone()
self.assertEqual(tuple(row), ("", ""))
finally:
await store.close()
if __name__ == "__main__":
unittest.main()