mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 16:07:01 +08:00
feat(governance): unify durable event retention
This commit is contained in:
+8
-2
@@ -13,8 +13,8 @@
|
||||
"runtime_to_db": [],
|
||||
"workflow_to_db": []
|
||||
},
|
||||
"edge_count": 6767,
|
||||
"edge_sha256": "659804b4d1c0f3ff4d96e8c9a059df0afb74c91f368a73188c740558114efa61",
|
||||
"edge_count": 6773,
|
||||
"edge_sha256": "3ba25180753ef7e15d9d08b7af25b66191306e8c0b465a27bed5613ed8d30c5d",
|
||||
"edges": [
|
||||
"app -> app.runtime",
|
||||
"app -> app.runtime.compat",
|
||||
@@ -3724,11 +3724,17 @@
|
||||
"app.db.health -> app.db",
|
||||
"app.db.health -> app.db.session",
|
||||
"app.db.maintenance -> app.db",
|
||||
"app.db.maintenance -> app.db.base",
|
||||
"app.db.maintenance -> app.db.models",
|
||||
"app.db.maintenance -> app.db.models.agentchat",
|
||||
"app.db.maintenance -> app.db.models.agenttask",
|
||||
"app.db.maintenance -> app.db.models.agenttaskrun",
|
||||
"app.db.maintenance -> app.db.models.downloadfailure",
|
||||
"app.db.maintenance -> app.db.models.downloadhistory",
|
||||
"app.db.maintenance -> app.db.models.message",
|
||||
"app.db.maintenance -> app.db.models.outbox",
|
||||
"app.db.maintenance -> app.db.models.siteuserdata",
|
||||
"app.db.maintenance -> app.db.models.subscribehistory",
|
||||
"app.db.maintenance -> app.db.models.transferhistory",
|
||||
"app.db.maintenance -> app.db.uow",
|
||||
"app.db.models -> app.db",
|
||||
|
||||
+24
-8
@@ -1514,7 +1514,7 @@
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.AudioTransferComplete": {
|
||||
"delivery": "ephemeral",
|
||||
"delivery": "durable_required",
|
||||
"error_behavior": "notify",
|
||||
"input_contract": "TransferResultContractData",
|
||||
"legacy_reason": null,
|
||||
@@ -1529,7 +1529,7 @@
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.AudioTransferFailed": {
|
||||
"delivery": "ephemeral",
|
||||
"delivery": "durable_required",
|
||||
"error_behavior": "notify",
|
||||
"input_contract": "TransferResultContractData",
|
||||
"legacy_reason": null,
|
||||
@@ -1799,7 +1799,7 @@
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.SubscribeComplete": {
|
||||
"delivery": "ephemeral",
|
||||
"delivery": "durable_required",
|
||||
"error_behavior": "notify",
|
||||
"input_contract": "SubscribeCompleteEventData",
|
||||
"legacy_reason": null,
|
||||
@@ -1844,7 +1844,7 @@
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.SubtitleTransferComplete": {
|
||||
"delivery": "ephemeral",
|
||||
"delivery": "durable_required",
|
||||
"error_behavior": "notify",
|
||||
"input_contract": "TransferResultContractData",
|
||||
"legacy_reason": null,
|
||||
@@ -1859,7 +1859,7 @@
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.SubtitleTransferFailed": {
|
||||
"delivery": "ephemeral",
|
||||
"delivery": "durable_required",
|
||||
"error_behavior": "notify",
|
||||
"input_contract": "TransferResultContractData",
|
||||
"legacy_reason": null,
|
||||
@@ -2253,6 +2253,10 @@
|
||||
{
|
||||
"caller": "app.chain.transfer",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"caller": "app.startup.initializers.modules",
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2262,6 +2266,10 @@
|
||||
{
|
||||
"caller": "app.chain.transfer",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"caller": "app.startup.initializers.modules",
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2521,6 +2529,10 @@
|
||||
{
|
||||
"caller": "app.chain.transfer",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"caller": "app.startup.initializers.modules",
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2530,6 +2542,10 @@
|
||||
{
|
||||
"caller": "app.chain.transfer",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"caller": "app.startup.initializers.modules",
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2555,7 +2571,7 @@
|
||||
"producers": [
|
||||
{
|
||||
"caller": "app.chain.transfer",
|
||||
"count": 2
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"caller": "app.startup.initializers.modules",
|
||||
@@ -2568,7 +2584,7 @@
|
||||
"producers": [
|
||||
{
|
||||
"caller": "app.chain.transfer",
|
||||
"count": 2
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"caller": "app.startup.initializers.modules",
|
||||
@@ -2609,7 +2625,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"producer_count": 78
|
||||
"producer_count": 80
|
||||
},
|
||||
"module_method_specs": {
|
||||
"anilist_credits": {
|
||||
|
||||
@@ -9,7 +9,6 @@ from sqlalchemy.schema import CreateTable
|
||||
from app.db.models.agenttask import AgentTask
|
||||
from app.db.models.agenttaskrun import AgentTaskRun
|
||||
|
||||
|
||||
MIGRATION = "database.versions.f4c8d2a7b1e6_3_0_6"
|
||||
|
||||
|
||||
@@ -84,7 +83,18 @@ def test_agent_task_run_migration_accepts_fresh_current_schema(monkeypatch) -> N
|
||||
assert {
|
||||
column["name"] for column in inspector.get_columns("agenttaskrun")
|
||||
} == {column.name for column in AgentTaskRun.__table__.columns}
|
||||
assert len(inspector.get_indexes("agenttaskrun")) == 2
|
||||
actual_indexes = {
|
||||
index["name"]: (tuple(index["column_names"]), index["unique"])
|
||||
for index in inspector.get_indexes("agenttaskrun")
|
||||
}
|
||||
expected_indexes = {
|
||||
index.name: (
|
||||
tuple(column.name for column in index.columns),
|
||||
int(index.unique),
|
||||
)
|
||||
for index in AgentTaskRun.__table__.indexes
|
||||
}
|
||||
assert actual_indexes == expected_indexes
|
||||
|
||||
|
||||
def test_agent_task_run_migration_matches_postgresql_identity() -> None:
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""历史表清理索引迁移测试。"""
|
||||
|
||||
import importlib
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
|
||||
|
||||
MIGRATION_MODULE = "database.versions.a6c8e2f4b1d3_3_0_11"
|
||||
INDEXES = {
|
||||
"subscribehistory": (
|
||||
"ix_subscribehistory_date_id",
|
||||
("date", "id"),
|
||||
),
|
||||
"agentchat": (
|
||||
"ix_agentchat_updated_id",
|
||||
("updated_at", "id"),
|
||||
),
|
||||
"agenttaskrun": (
|
||||
"ix_agenttaskrun_status_started_id",
|
||||
("status", "started_at", "id"),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _index_columns(connection, table_name: str) -> dict[str, tuple[str, ...]]:
|
||||
"""返回测试表的索引字段签名。"""
|
||||
return {
|
||||
index["name"]: tuple(index.get("column_names") or ())
|
||||
for index in sa.inspect(connection).get_indexes(table_name)
|
||||
}
|
||||
|
||||
|
||||
def test_cleanup_index_migration_is_idempotent_and_reversible(monkeypatch) -> None:
|
||||
"""升级可重复执行并创建准确索引,降级只移除新增索引。"""
|
||||
engine = sa.create_engine("sqlite://")
|
||||
metadata = sa.MetaData()
|
||||
sa.Table(
|
||||
"subscribehistory",
|
||||
metadata,
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("date", sa.String()),
|
||||
)
|
||||
sa.Table(
|
||||
"agentchat",
|
||||
metadata,
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("updated_at", sa.String()),
|
||||
)
|
||||
sa.Table(
|
||||
"agenttaskrun",
|
||||
metadata,
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("status", sa.String()),
|
||||
sa.Column("started_at", sa.String()),
|
||||
)
|
||||
|
||||
with engine.begin() as connection:
|
||||
metadata.create_all(connection)
|
||||
migration = importlib.import_module(MIGRATION_MODULE)
|
||||
monkeypatch.setattr(
|
||||
migration,
|
||||
"op",
|
||||
Operations(MigrationContext.configure(connection)),
|
||||
)
|
||||
|
||||
migration.upgrade()
|
||||
migration.upgrade()
|
||||
for table_name, (index_name, columns) in INDEXES.items():
|
||||
assert _index_columns(connection, table_name)[index_name] == columns
|
||||
|
||||
migration.downgrade()
|
||||
for table_name, (index_name, _columns) in INDEXES.items():
|
||||
assert index_name not in _index_columns(connection, table_name)
|
||||
@@ -0,0 +1,156 @@
|
||||
"""统一数据维护对追加型历史表的安全清理测试。"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.application.maintenance import CleanupPolicy, DataCleanupService
|
||||
from app.db.base import Base
|
||||
from app.db.maintenance import DatabaseCleanupRepository
|
||||
from app.db.models.agentchat import AgentChat
|
||||
from app.db.models.agenttask import AgentTask
|
||||
from app.db.models.agenttaskrun import AgentTaskRun
|
||||
from app.db.models.subscribehistory import SubscribeHistory
|
||||
|
||||
|
||||
def _cleanup_policy() -> CleanupPolicy:
|
||||
"""只启用本组新增历史表的 30 天保留期。"""
|
||||
return CleanupPolicy(
|
||||
enabled=True,
|
||||
message_days=0,
|
||||
download_history_days=0,
|
||||
site_userdata_days=0,
|
||||
transfer_history_days=0,
|
||||
download_failure_days=0,
|
||||
subscribe_history_days=30,
|
||||
agent_chat_days=30,
|
||||
agent_task_run_days=30,
|
||||
outbox_completed_days=0,
|
||||
outbox_dead_days=0,
|
||||
)
|
||||
|
||||
|
||||
def test_growth_table_cleanup_preserves_live_agent_recovery_state() -> None:
|
||||
"""旧历史可回收,但任务引用会话、最后运行和运行中记录必须保留。"""
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
Base.metadata.create_all(engine)
|
||||
factory = sessionmaker(bind=engine)
|
||||
old_time = "2026-06-01 12:00:00"
|
||||
recent_time = "2026-08-20 12:00:00"
|
||||
|
||||
with factory() as session:
|
||||
session.add_all([
|
||||
SubscribeHistory(name="old-subscribe", date=old_time),
|
||||
SubscribeHistory(name="recent-subscribe", date=recent_time),
|
||||
AgentChat(
|
||||
session_id="old-unreferenced",
|
||||
title="old-unreferenced",
|
||||
created_at=old_time,
|
||||
updated_at=old_time,
|
||||
),
|
||||
AgentChat(
|
||||
session_id="task-context",
|
||||
title="task-context",
|
||||
created_at=old_time,
|
||||
updated_at=old_time,
|
||||
),
|
||||
AgentChat(
|
||||
session_id="recent-chat",
|
||||
title="recent-chat",
|
||||
created_at=recent_time,
|
||||
updated_at=recent_time,
|
||||
),
|
||||
])
|
||||
task = AgentTask(
|
||||
name="cleanup-protected-task",
|
||||
content="test",
|
||||
trigger_type="cron",
|
||||
enabled=True,
|
||||
user_id="1",
|
||||
session_id="task-context",
|
||||
last_status="success",
|
||||
last_run_id="latest-run",
|
||||
run_count=2,
|
||||
created_at=old_time,
|
||||
updated_at=old_time,
|
||||
)
|
||||
session.add(task)
|
||||
session.flush()
|
||||
session.add_all([
|
||||
AgentTaskRun(
|
||||
run_id="expired-run",
|
||||
task_id=task.id,
|
||||
trigger_source="scheduled",
|
||||
name=task.name,
|
||||
content=task.content,
|
||||
trigger_type=task.trigger_type,
|
||||
user_id=task.user_id,
|
||||
session_id=task.session_id,
|
||||
status="success",
|
||||
started_at=old_time,
|
||||
finished_at=old_time,
|
||||
),
|
||||
AgentTaskRun(
|
||||
run_id="latest-run",
|
||||
task_id=task.id,
|
||||
trigger_source="scheduled",
|
||||
name=task.name,
|
||||
content=task.content,
|
||||
trigger_type=task.trigger_type,
|
||||
user_id=task.user_id,
|
||||
session_id=task.session_id,
|
||||
status="success",
|
||||
started_at=old_time,
|
||||
finished_at=old_time,
|
||||
),
|
||||
AgentTaskRun(
|
||||
run_id="running-run",
|
||||
task_id=task.id,
|
||||
trigger_source="manual",
|
||||
name=task.name,
|
||||
content=task.content,
|
||||
trigger_type=task.trigger_type,
|
||||
user_id=task.user_id,
|
||||
session_id=task.session_id,
|
||||
status="running",
|
||||
started_at=old_time,
|
||||
),
|
||||
AgentTaskRun(
|
||||
run_id="recent-run",
|
||||
task_id=task.id,
|
||||
trigger_source="manual",
|
||||
name=task.name,
|
||||
content=task.content,
|
||||
trigger_type=task.trigger_type,
|
||||
user_id=task.user_id,
|
||||
session_id=task.session_id,
|
||||
status="failed",
|
||||
started_at=recent_time,
|
||||
finished_at=recent_time,
|
||||
),
|
||||
])
|
||||
session.commit()
|
||||
|
||||
report = DataCleanupService(
|
||||
repository=DatabaseCleanupRepository(session_factory=factory),
|
||||
policy_reader=_cleanup_policy,
|
||||
clock=lambda: datetime(2026, 8, 26, 12, 0, 0),
|
||||
).execute(batch_size=1)
|
||||
|
||||
assert report["tables"]["subscribehistory"]["deleted"] == 1
|
||||
assert report["tables"]["agentchat"]["deleted"] == 1
|
||||
assert report["tables"]["agenttaskrun"]["deleted"] == 1
|
||||
with factory() as session:
|
||||
assert set(session.execute(select(SubscribeHistory.name)).scalars()) == {
|
||||
"recent-subscribe"
|
||||
}
|
||||
assert set(session.execute(select(AgentChat.session_id)).scalars()) == {
|
||||
"task-context",
|
||||
"recent-chat",
|
||||
}
|
||||
assert set(session.execute(select(AgentTaskRun.run_id)).scalars()) == {
|
||||
"latest-run",
|
||||
"running-run",
|
||||
"recent-run",
|
||||
}
|
||||
@@ -70,6 +70,26 @@ class FakeCleanupRepository:
|
||||
"""模拟下载失败记录删除。"""
|
||||
return self._delete("downloadfailure")
|
||||
|
||||
def delete_subscribe_history(self, db, cutoff: str, limit: int) -> int:
|
||||
"""模拟订阅历史删除。"""
|
||||
return self._delete("subscribehistory")
|
||||
|
||||
def delete_agent_chats(self, db, cutoff: str, limit: int) -> int:
|
||||
"""模拟 Agent 会话删除。"""
|
||||
return self._delete("agentchat")
|
||||
|
||||
def delete_agent_task_runs(self, db, cutoff: str, limit: int) -> int:
|
||||
"""模拟 Agent 运行历史删除。"""
|
||||
return self._delete("agenttaskrun")
|
||||
|
||||
def delete_outbox_completed(self, db, cutoff: str, limit: int) -> int:
|
||||
"""模拟 Outbox 已完成记录删除。"""
|
||||
return self._delete("outbox_completed")
|
||||
|
||||
def delete_outbox_dead(self, db, cutoff: str, limit: int) -> int:
|
||||
"""模拟 Outbox 死信记录删除。"""
|
||||
return self._delete("outbox_dead")
|
||||
|
||||
|
||||
def _policy(**overrides) -> CleanupPolicy:
|
||||
"""构造所有表默认启用的测试策略。"""
|
||||
@@ -80,6 +100,11 @@ def _policy(**overrides) -> CleanupPolicy:
|
||||
"site_userdata_days": 1,
|
||||
"transfer_history_days": 1,
|
||||
"download_failure_days": 1,
|
||||
"subscribe_history_days": 1,
|
||||
"agent_chat_days": 1,
|
||||
"agent_task_run_days": 1,
|
||||
"outbox_completed_days": 1,
|
||||
"outbox_dead_days": 1,
|
||||
}
|
||||
values.update(overrides)
|
||||
return CleanupPolicy(**values)
|
||||
@@ -112,6 +137,11 @@ def test_cleanup_service_owns_batching_report_and_progress() -> None:
|
||||
"siteuserdata",
|
||||
"transferhistory",
|
||||
"downloadfailure",
|
||||
"subscribehistory",
|
||||
"agentchat",
|
||||
"agenttaskrun",
|
||||
"outbox_completed",
|
||||
"outbox_dead",
|
||||
]
|
||||
assert progress.call_args.kwargs["value"] == 100
|
||||
|
||||
@@ -128,7 +158,7 @@ def test_cleanup_service_finishes_other_tables_before_raising_partial_failure()
|
||||
with pytest.raises(RuntimeError, match="downloadhistory: boom"):
|
||||
service.execute(batch_size=2)
|
||||
|
||||
assert repository.calls[-1] == "downloadfailure"
|
||||
assert repository.calls[-1] == "outbox_dead"
|
||||
assert repository.rollbacks == 1
|
||||
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ def test_selected_user_side_effects_are_marked_durable_required() -> None:
|
||||
}
|
||||
|
||||
assert set(DURABLE_EVENT_TOPICS) == durable_events
|
||||
assert len(durable_events) == 11
|
||||
assert len(set(DURABLE_EVENT_TOPICS.values())) == len(durable_events)
|
||||
|
||||
|
||||
|
||||
@@ -7,10 +7,12 @@ import pytest
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.application.maintenance import CleanupPolicy, DataCleanupService
|
||||
from app.application.outbox import ClaimedOutboxMessage, OutboxDispatcher, OutboxIntent
|
||||
from app.application.subscription.write import CreateSubscriptionCommand
|
||||
from app.db.adapters.outbox import SqlAlchemyOutboxRepository
|
||||
from app.db.base import Base
|
||||
from app.db.maintenance import DatabaseCleanupRepository
|
||||
from app.db.models.outbox import OutboxMessage
|
||||
|
||||
|
||||
@@ -171,3 +173,97 @@ def test_sync_outbox_claim_is_exclusive_for_event_key() -> None:
|
||||
assert message.status == "processing"
|
||||
assert message.attempt == 1
|
||||
assert message.lease_until == lease_until.isoformat()
|
||||
|
||||
|
||||
def test_outbox_cleanup_removes_only_expired_terminal_history_in_batches() -> None:
|
||||
"""清理只删除超过各自保留期的终态记录,并按批次持续收口。"""
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
Base.metadata.create_all(engine)
|
||||
factory = sessionmaker(bind=engine)
|
||||
now = datetime(2026, 8, 26, tzinfo=timezone.utc)
|
||||
|
||||
def message(
|
||||
event_key: str,
|
||||
status: str,
|
||||
*,
|
||||
completed_at: datetime | None = None,
|
||||
next_retry_at: datetime | None = None,
|
||||
) -> OutboxMessage:
|
||||
"""构造指定终态时间的最小 Outbox 测试记录。"""
|
||||
return OutboxMessage(
|
||||
event_key=event_key,
|
||||
topic="test",
|
||||
payload_version=1,
|
||||
payload={},
|
||||
status=status,
|
||||
attempt=1,
|
||||
next_retry_at=(next_retry_at or now).isoformat(),
|
||||
created_at=(now - timedelta(days=120)).isoformat(),
|
||||
completed_at=completed_at.isoformat() if completed_at else None,
|
||||
)
|
||||
|
||||
with factory() as session:
|
||||
session.add_all([
|
||||
message(
|
||||
"completed-expired-1",
|
||||
"completed",
|
||||
completed_at=now - timedelta(days=31),
|
||||
),
|
||||
message(
|
||||
"completed-expired-2",
|
||||
"completed",
|
||||
completed_at=now - timedelta(days=40),
|
||||
),
|
||||
message(
|
||||
"completed-boundary",
|
||||
"completed",
|
||||
completed_at=now - timedelta(days=30),
|
||||
),
|
||||
message(
|
||||
"dead-expired",
|
||||
"dead",
|
||||
next_retry_at=now - timedelta(days=91),
|
||||
),
|
||||
message(
|
||||
"dead-recent",
|
||||
"dead",
|
||||
next_retry_at=now - timedelta(days=20),
|
||||
),
|
||||
message("pending-old", "pending"),
|
||||
message("processing-old", "processing"),
|
||||
])
|
||||
session.commit()
|
||||
|
||||
cleanup = DataCleanupService(
|
||||
repository=DatabaseCleanupRepository(session_factory=factory),
|
||||
policy_reader=lambda: CleanupPolicy(
|
||||
enabled=True,
|
||||
message_days=0,
|
||||
download_history_days=0,
|
||||
site_userdata_days=0,
|
||||
transfer_history_days=0,
|
||||
download_failure_days=0,
|
||||
subscribe_history_days=0,
|
||||
agent_chat_days=0,
|
||||
agent_task_run_days=0,
|
||||
outbox_completed_days=30,
|
||||
outbox_dead_days=90,
|
||||
),
|
||||
clock=lambda: now,
|
||||
)
|
||||
report = cleanup.execute(batch_size=2)
|
||||
|
||||
assert report["tables"]["outbox_completed"]["deleted"] == 2
|
||||
assert report["tables"]["outbox_dead"]["deleted"] == 1
|
||||
assert report["total_deleted"] == 3
|
||||
|
||||
with factory() as session:
|
||||
remaining = set(
|
||||
session.execute(select(OutboxMessage.event_key)).scalars().all()
|
||||
)
|
||||
assert remaining == {
|
||||
"completed-boundary",
|
||||
"dead-recent",
|
||||
"pending-old",
|
||||
"processing-old",
|
||||
}
|
||||
|
||||
@@ -9,10 +9,17 @@ __is_overwrite_declined 用于识别这一场景,__default_callback 失败分
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.schemas.transfer import TransferInfo
|
||||
from app.schemas.types import EventType
|
||||
from tests.test_transfer_job_manager import FakeMedia, make_task, make_transfer_chain
|
||||
from tests.test_transfer_job_manager import (
|
||||
FakeMedia,
|
||||
make_fileitem,
|
||||
make_task,
|
||||
make_transfer_chain,
|
||||
)
|
||||
|
||||
|
||||
def make_history_oper(history=None, success_history=None, raise_on_query: bool = False):
|
||||
@@ -271,3 +278,49 @@ def test_default_callback_delegates_primary_failure_to_durable_writer():
|
||||
event_type, event_payload = chain.eventmanager.send_event.call_args.args
|
||||
assert event_type == EventType.TransferFailed
|
||||
assert event_payload["idempotency_key"] == "transfer.failed:1:v1"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("path", "success", "expected_topic", "expected_event"),
|
||||
[
|
||||
(
|
||||
"/downloads/demo.srt",
|
||||
True,
|
||||
"transfer.subtitle.completed",
|
||||
EventType.SubtitleTransferComplete,
|
||||
),
|
||||
(
|
||||
"/downloads/demo.srt",
|
||||
False,
|
||||
"transfer.subtitle.failed",
|
||||
EventType.SubtitleTransferFailed,
|
||||
),
|
||||
(
|
||||
"/downloads/demo.flac",
|
||||
True,
|
||||
"transfer.audio.completed",
|
||||
EventType.AudioTransferComplete,
|
||||
),
|
||||
(
|
||||
"/downloads/demo.flac",
|
||||
False,
|
||||
"transfer.audio.failed",
|
||||
EventType.AudioTransferFailed,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_subtitle_and_audio_results_use_durable_topics(
|
||||
path: str,
|
||||
success: bool,
|
||||
expected_topic: str,
|
||||
expected_event: EventType,
|
||||
) -> None:
|
||||
"""字幕与音频结果必须和主要媒体结果共用 durable writer 分类。"""
|
||||
chain = make_transfer_chain()
|
||||
task = make_task(1)
|
||||
task.fileitem = make_fileitem(path)
|
||||
|
||||
assert chain._durable_transfer_event(task, success=success) == (
|
||||
expected_topic,
|
||||
expected_event,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user