178 lines
7.6 KiB
Python
178 lines
7.6 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from biliup_next.app.serializers import ControlPlaneSerializer
|
|
from biliup_next.core.models import ActionRecord, Artifact, Task, TaskContext, TaskStep
|
|
|
|
|
|
class FakeSerializerRepo:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
task: Task,
|
|
context: TaskContext | None = None,
|
|
steps: list[TaskStep] | None = None,
|
|
artifacts: list[Artifact] | None = None,
|
|
actions: list[ActionRecord] | None = None,
|
|
) -> None:
|
|
self.task = task
|
|
self.context = context
|
|
self.steps = steps or []
|
|
self.artifacts = artifacts or []
|
|
self.actions = actions or []
|
|
|
|
def get_task(self, task_id: str) -> Task | None:
|
|
return self.task if task_id == self.task.id else None
|
|
|
|
def get_task_context(self, task_id: str) -> TaskContext | None:
|
|
return self.context if task_id == self.task.id else None
|
|
|
|
def list_task_contexts_for_task_ids(self, task_ids: list[str]) -> dict[str, TaskContext]:
|
|
if self.context and self.context.task_id in task_ids:
|
|
return {self.context.task_id: self.context}
|
|
return {}
|
|
|
|
def list_steps_for_task_ids(self, task_ids: list[str]) -> dict[str, list[TaskStep]]:
|
|
if self.task.id in task_ids:
|
|
return {self.task.id: list(self.steps)}
|
|
return {}
|
|
|
|
def list_steps(self, task_id: str) -> list[TaskStep]:
|
|
return list(self.steps) if task_id == self.task.id else []
|
|
|
|
def list_task_contexts_by_session_key(self, session_key: str) -> list[TaskContext]:
|
|
if self.context and self.context.session_key == session_key:
|
|
return [self.context]
|
|
return []
|
|
|
|
def list_artifacts(self, task_id: str) -> list[Artifact]:
|
|
return list(self.artifacts) if task_id == self.task.id else []
|
|
|
|
def list_action_records(self, task_id: str, limit: int = 200) -> list[ActionRecord]:
|
|
return list(self.actions)[:limit] if task_id == self.task.id else []
|
|
|
|
|
|
class SerializerTests(unittest.TestCase):
|
|
def test_task_payload_includes_context_retry_and_delivery_state(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
task = Task("task-1", "local_file", str(Path(tmpdir) / "session" / "task-title" / "source.mp4"), "task-title", "running", "2026-01-01T00:00:00+00:00", "2026-01-01T00:01:00+00:00")
|
|
session_dir = Path(tmpdir) / "session" / "task-title"
|
|
session_dir.mkdir(parents=True, exist_ok=True)
|
|
(session_dir / "full_video_bvid.txt").write_text("BVFULL123", encoding="utf-8")
|
|
(session_dir / "bvid.txt").write_text("BVSPLIT123", encoding="utf-8")
|
|
steps = [
|
|
TaskStep(None, "task-1", "publish", "failed_retryable", "ERR", "upload failed", 1, None, "2099-01-01T00:00:00+00:00"),
|
|
]
|
|
context = TaskContext(
|
|
id=None,
|
|
task_id="task-1",
|
|
session_key="session-1",
|
|
streamer="streamer",
|
|
room_id="room",
|
|
source_title="task-title",
|
|
segment_started_at=None,
|
|
segment_duration_seconds=None,
|
|
full_video_bvid=None,
|
|
created_at="2026-01-01T00:00:00+00:00",
|
|
updated_at="2026-01-01T00:00:00+00:00",
|
|
)
|
|
repo = FakeSerializerRepo(task=task, context=context, steps=steps)
|
|
state = {
|
|
"repo": repo,
|
|
"settings": {
|
|
"paths": {"session_dir": str(Path(tmpdir) / "session")},
|
|
"comment": {"post_split_comment": True, "post_full_video_timeline_comment": True},
|
|
"cleanup": {},
|
|
"publish": {"retry_schedule_minutes": [10]},
|
|
},
|
|
}
|
|
|
|
payload = ControlPlaneSerializer(state).task_payload("task-1")
|
|
|
|
self.assertIsNotNone(payload)
|
|
self.assertEqual(payload["session_context"]["session_key"], "session-1")
|
|
self.assertEqual(payload["session_context"]["full_video_bvid"], "BVFULL123")
|
|
self.assertEqual(payload["retry_state"]["step_name"], "publish")
|
|
self.assertEqual(payload["delivery_state"]["split_comment"], "pending")
|
|
|
|
def test_session_payload_reuses_task_payload_serialization(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
task = Task("task-1", "local_file", str(Path(tmpdir) / "session" / "task-title" / "source.mp4"), "task-title", "published", "2026-01-01T00:00:00+00:00", "2026-01-01T00:01:00+00:00")
|
|
context = TaskContext(
|
|
id=None,
|
|
task_id="task-1",
|
|
session_key="session-1",
|
|
streamer="streamer",
|
|
room_id="room",
|
|
source_title="task-title",
|
|
segment_started_at=None,
|
|
segment_duration_seconds=None,
|
|
full_video_bvid="BVFULL123",
|
|
created_at="2026-01-01T00:00:00+00:00",
|
|
updated_at="2026-01-01T00:00:00+00:00",
|
|
)
|
|
repo = FakeSerializerRepo(task=task, context=context)
|
|
state = {
|
|
"repo": repo,
|
|
"settings": {
|
|
"paths": {"session_dir": str(Path(tmpdir) / "session")},
|
|
"comment": {"post_split_comment": True, "post_full_video_timeline_comment": True},
|
|
"cleanup": {},
|
|
"publish": {},
|
|
},
|
|
}
|
|
|
|
payload = ControlPlaneSerializer(state).session_payload("session-1")
|
|
|
|
self.assertIsNotNone(payload)
|
|
self.assertEqual(payload["session_key"], "session-1")
|
|
self.assertEqual(payload["task_count"], 1)
|
|
self.assertEqual(payload["full_video_url"], "https://www.bilibili.com/video/BVFULL123")
|
|
self.assertEqual(payload["tasks"][0]["id"], "task-1")
|
|
|
|
def test_timeline_payload_includes_task_step_artifact_and_action_entries(self) -> None:
|
|
task = Task("task-1", "local_file", "/tmp/source.mp4", "task-title", "published", "2026-01-01T00:00:00+00:00", "2026-01-01T00:02:00+00:00")
|
|
steps = [
|
|
TaskStep(None, "task-1", "comment", "succeeded", None, None, 0, "2026-01-01T00:01:00+00:00", "2026-01-01T00:01:30+00:00"),
|
|
]
|
|
artifacts = [
|
|
Artifact(None, "task-1", "publish_bvid", "/tmp/bvid.txt", "{}", "2026-01-01T00:01:40+00:00"),
|
|
]
|
|
actions = [
|
|
ActionRecord(
|
|
id=None,
|
|
task_id="task-1",
|
|
action_name="comment",
|
|
status="ok",
|
|
summary="comment succeeded",
|
|
details_json=json.dumps({"split": {"status": "ok"}, "full": {"status": "skipped"}}),
|
|
created_at="2026-01-01T00:01:50+00:00",
|
|
)
|
|
]
|
|
repo = FakeSerializerRepo(task=task, steps=steps, artifacts=artifacts, actions=actions)
|
|
state = {
|
|
"repo": repo,
|
|
"settings": {
|
|
"paths": {"session_dir": "/tmp/session"},
|
|
"comment": {"post_split_comment": True, "post_full_video_timeline_comment": True},
|
|
"cleanup": {},
|
|
"publish": {},
|
|
},
|
|
}
|
|
|
|
payload = ControlPlaneSerializer(state).timeline_payload("task-1")
|
|
|
|
self.assertIsNotNone(payload)
|
|
action_item = next(item for item in payload["items"] if item["kind"] == "action")
|
|
self.assertIn("split=ok", action_item["summary"])
|
|
kinds = {item["kind"] for item in payload["items"]}
|
|
self.assertTrue({"task", "step", "artifact", "action"}.issubset(kinds))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|