feat(titles): complete title selection workflow
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
Deploy / Deploy Staging (push) Has been cancelled
Deploy / Build Production Runtime Images (push) Failing after 2030h35m46s
Deploy / Production Browser E2E (push) Failing after 2030h26m37s
Deploy / Deploy Production (push) Failing after 2030h26m39s
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
Deploy / Deploy Staging (push) Has been cancelled
Deploy / Build Production Runtime Images (push) Failing after 2030h35m46s
Deploy / Production Browser E2E (push) Failing after 2030h26m37s
Deploy / Deploy Production (push) Failing after 2030h26m39s
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
"""Add project title favorite flag.
|
||||
|
||||
Revision ID: 004
|
||||
Revises: 003
|
||||
Create Date: 2026-06-24
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "004"
|
||||
down_revision: Union[str, None] = "003"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("project_titles", sa.Column("favorite", sa.Boolean(), nullable=False, server_default=sa.false()))
|
||||
op.create_index(op.f("ix_project_titles_favorite"), "project_titles", ["favorite"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(op.f("ix_project_titles_favorite"), table_name="project_titles")
|
||||
op.drop_column("project_titles", "favorite")
|
||||
@@ -77,6 +77,14 @@ def _ensure_library_has_ready_video_assets(assets) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _select_title_id(project_title_repository: Any, project_id: str) -> str:
|
||||
active_titles = project_title_repository.list_by_project(project_id, active_only=True)
|
||||
if not active_titles:
|
||||
return ""
|
||||
selected = sorted(active_titles, key=lambda title: (0 if getattr(title, "favorite", False) else 1, int(title.usage_count or 0), title.created_at))[0]
|
||||
return selected.id
|
||||
|
||||
|
||||
@router.post("/tasks", response_model=GenerationTaskResponse)
|
||||
def create_generation_task(
|
||||
request: CreateGenerationTaskRequest,
|
||||
@@ -104,16 +112,11 @@ def create_generation_task(
|
||||
workspace_id=request.workspace_id,
|
||||
project_id=request.project_id,
|
||||
asset_library_id=request.asset_library_id,
|
||||
strategy_id=request.strategy_id,
|
||||
strategy_id=request.strategy_id or _select_title_id(project_title_repository, request.project_id),
|
||||
voice_library_id=request.voice_library_id,
|
||||
created_by_user_id=authenticated_user.user.id,
|
||||
)
|
||||
)
|
||||
if request.strategy_id:
|
||||
title = project_title_repository.get(request.strategy_id)
|
||||
if title is not None and title.workspace_id == request.workspace_id and title.project_id == request.project_id:
|
||||
title.usage_count = int(title.usage_count or 0) + 1
|
||||
project_title_repository.update(title)
|
||||
celery_app.send_task("worker.generate_video", args=[task.id])
|
||||
return _to_generation_task_response(task)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ def _to_response(item) -> ProjectTitleResponse:
|
||||
project_id=item.project_id,
|
||||
text=item.text,
|
||||
category=item.category,
|
||||
favorite=bool(getattr(item, "favorite", False)),
|
||||
usage_count=int(item.usage_count or 0),
|
||||
is_active=bool(item.is_active),
|
||||
created_at=item.created_at,
|
||||
@@ -73,6 +74,7 @@ def create_project_title(
|
||||
project_id=project_id,
|
||||
text=request.text,
|
||||
category=request.category,
|
||||
favorite=request.favorite,
|
||||
created_by_user_id=authenticated_user.user.id,
|
||||
)
|
||||
return _to_response(item)
|
||||
@@ -94,6 +96,8 @@ def update_project_title(
|
||||
item.text = request.text.strip()
|
||||
if request.category is not None:
|
||||
item.category = request.category
|
||||
if request.favorite is not None:
|
||||
item.favorite = request.favorite
|
||||
if request.is_active is not None:
|
||||
item.is_active = request.is_active
|
||||
return _to_response(title_repository.update(item))
|
||||
|
||||
@@ -12,6 +12,7 @@ class ProjectTitleResponse(BaseModel):
|
||||
project_id: str
|
||||
text: str
|
||||
category: str
|
||||
favorite: bool
|
||||
usage_count: int
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
@@ -26,9 +27,11 @@ class CreateProjectTitleRequest(BaseModel):
|
||||
workspace_id: str
|
||||
text: str = Field(min_length=1, max_length=200)
|
||||
category: TitleCategory = "default"
|
||||
favorite: bool = False
|
||||
|
||||
|
||||
class UpdateProjectTitleRequest(BaseModel):
|
||||
text: str | None = Field(default=None, min_length=1, max_length=200)
|
||||
category: TitleCategory | None = None
|
||||
favorite: bool | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
@@ -11,7 +11,8 @@ type WorkspaceResponse = { id?: string; workspace_id?: string };
|
||||
type ProjectResponse = { id: string };
|
||||
type LibraryResponse = { id: string };
|
||||
type AssetListResponse = { items: Array<{ name: string; status: string; mime_type?: string; file_type?: string }> };
|
||||
type GenerationTaskResponse = { id: string; status: string; progress: number; result_count: number; error_message?: string | null };
|
||||
type GenerationTaskResponse = { id: string; status: string; progress: number; result_count: number; error_message?: string | null; strategy_id?: string | null };
|
||||
type ProjectTitleResponse = { id: string; text: string; usage_count: number };
|
||||
type GeneratedVideoResponse = { id: string; name: string; file_url: string; file_size: number };
|
||||
|
||||
test.describe('Core generation and download flow', () => {
|
||||
@@ -59,6 +60,14 @@ test.describe('Core generation and download flow', () => {
|
||||
expect(library.status(), await library.text()).toBe(200);
|
||||
const libraryData = (await library.json()) as LibraryResponse;
|
||||
|
||||
const projectTitleText = `E2E 生成标题 ${suffix}`;
|
||||
const title = await request.post(`${apiBase}/projects/${projectData.id}/titles`, {
|
||||
headers,
|
||||
data: { workspace_id: workspaceId, text: projectTitleText, category: 'marketing', favorite: true },
|
||||
});
|
||||
expect(title.status(), await title.text()).toBe(200);
|
||||
const titleData = (await title.json()) as ProjectTitleResponse;
|
||||
|
||||
const fixture = fs.readFileSync(path.join(currentDir, 'fixtures', 'sample.mp4'));
|
||||
const upload = await request.post(`${apiBase}/upload`, {
|
||||
headers,
|
||||
@@ -103,6 +112,8 @@ test.describe('Core generation and download flow', () => {
|
||||
await expect(page.getByText('项目生成任务')).toBeVisible({ timeout: 20_000 });
|
||||
await page.locator('.ant-select-selector').first().click();
|
||||
await page.getByText(`${libraryName} (video)`).click();
|
||||
await page.getByLabel('标题').locator('.ant-select-selector').click();
|
||||
await page.getByText(projectTitleText).click();
|
||||
|
||||
await expect(page.getByText(/素材准备度:/)).toBeVisible({ timeout: 20_000 });
|
||||
await expect(page.getByRole('button', { name: '发起生成' })).toBeEnabled({ timeout: 20_000 });
|
||||
@@ -127,11 +138,11 @@ test.describe('Core generation and download flow', () => {
|
||||
return `http_${task.status()}`;
|
||||
}
|
||||
const data = (await task.json()) as GenerationTaskResponse;
|
||||
return `${data.status}:${data.result_count}:${data.error_message || ''}`;
|
||||
return `${data.status}:${data.result_count}:${data.strategy_id || ''}:${data.error_message || ''}`;
|
||||
},
|
||||
{ timeout: 90_000, intervals: [1_000, 2_000, 5_000] }
|
||||
)
|
||||
.toMatch(/^completed:[1-9]\d*:/);
|
||||
.toMatch(new RegExp(`^completed:[1-9]\\d*:${titleData.id}:`));
|
||||
|
||||
const results = await request.get(`${apiBase}/generation/tasks/${createdTask.id}/results`, { headers });
|
||||
expect(results.status(), await results.text()).toBe(200);
|
||||
@@ -161,6 +172,9 @@ test.describe('Core generation and download flow', () => {
|
||||
const sourceAsset = assetsAfterGenerationData.items.find((item) => item.name === 'e2e-generation-source.mp4');
|
||||
expect(sourceAsset?.metadata.generation_use_count).toBe(1);
|
||||
expect(sourceAsset?.metadata.review_status).toBe('pending_review');
|
||||
expect(sourceAsset?.metadata.last_used_at).toBeTruthy();
|
||||
const titleAfterGeneration = await request.get(`${apiBase}/projects/${projectData.id}/titles`, { headers });
|
||||
expect(titleAfterGeneration.status(), await titleAfterGeneration.text()).toBe(200);
|
||||
const titlesData = (await titleAfterGeneration.json()) as { items: ProjectTitleResponse[] };
|
||||
expect(titlesData.items.find((item) => item.id === titleData.id)?.usage_count).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,9 +43,13 @@ test.describe('Project title library flow', () => {
|
||||
await page.goto(`/projects/${projectData.id}/titles`);
|
||||
await expect(page.getByText('标题库')).toBeVisible({ timeout: 20_000 });
|
||||
await page.getByPlaceholder('输入可复用标题').fill(`E2E 标题 ${suffix}`);
|
||||
await page.getByRole('switch', { name: /普通|常用/ }).first().click();
|
||||
await page.getByRole('button', { name: '新增标题' }).click();
|
||||
await expect(page.getByText('标题已加入标题库')).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByText(`E2E 标题 ${suffix}`)).toBeVisible({ timeout: 20_000 });
|
||||
await expect(page.getByText('常用')).toBeVisible();
|
||||
await page.getByPlaceholder('搜索标题').fill(`E2E 标题 ${suffix}`);
|
||||
await expect(page.getByText(`E2E 标题 ${suffix}`)).toBeVisible();
|
||||
await expect(page.getByText('使用次数:0')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface ProjectTitleItem {
|
||||
project_id: string;
|
||||
text: string;
|
||||
category: 'default' | 'marketing' | 'tutorial' | 'story' | 'promo';
|
||||
favorite: boolean;
|
||||
usage_count: number;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
@@ -17,12 +18,12 @@ export const getProjectTitles = async (projectId: string, activeOnly = false): P
|
||||
return response.data.items;
|
||||
};
|
||||
|
||||
export const createProjectTitle = async (projectId: string, data: { workspace_id: string; text: string; category?: ProjectTitleItem['category'] }): Promise<ProjectTitleItem> => {
|
||||
export const createProjectTitle = async (projectId: string, data: { workspace_id: string; text: string; category?: ProjectTitleItem['category']; favorite?: boolean }): Promise<ProjectTitleItem> => {
|
||||
const response = await apiClient.post(`/projects/${projectId}/titles`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateProjectTitle = async (titleId: string, data: { text?: string; category?: ProjectTitleItem['category']; is_active?: boolean }): Promise<ProjectTitleItem> => {
|
||||
export const updateProjectTitle = async (titleId: string, data: { text?: string; category?: ProjectTitleItem['category']; favorite?: boolean; is_active?: boolean }): Promise<ProjectTitleItem> => {
|
||||
const response = await apiClient.patch(`/project-titles/${titleId}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button, Card, Form, Input, List, Select, Space, Switch, Tag, message } from 'antd';
|
||||
import { useParams } from 'react-router-dom';
|
||||
@@ -24,7 +24,10 @@ const categoryLabels: Record<string, string> = {
|
||||
const ProjectTitles: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const projectId = id || '';
|
||||
const [form] = Form.useForm<{ text: string; category: 'default' | 'marketing' | 'tutorial' | 'story' | 'promo' }>();
|
||||
const [form] = Form.useForm<{ text: string; category: 'default' | 'marketing' | 'tutorial' | 'story' | 'promo'; favorite: boolean }>();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState<string>('all');
|
||||
const [favoriteOnly, setFavoriteOnly] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const projectQuery = useQuery({
|
||||
@@ -40,11 +43,12 @@ const ProjectTitles: React.FC = () => {
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (values: { text: string; category: 'default' | 'marketing' | 'tutorial' | 'story' | 'promo' }) =>
|
||||
mutationFn: (values: { text: string; category: 'default' | 'marketing' | 'tutorial' | 'story' | 'promo'; favorite: boolean }) =>
|
||||
createProjectTitle(projectId, {
|
||||
workspace_id: projectQuery.data!.workspace_id,
|
||||
text: values.text,
|
||||
category: values.category,
|
||||
favorite: values.favorite,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
message.success('标题已加入标题库');
|
||||
@@ -55,8 +59,8 @@ const ProjectTitles: React.FC = () => {
|
||||
});
|
||||
|
||||
const toggleMutation = useMutation({
|
||||
mutationFn: ({ titleId, isActive }: { titleId: string; isActive: boolean }) =>
|
||||
updateProjectTitle(titleId, { is_active: isActive }),
|
||||
mutationFn: ({ titleId, isActive, favorite }: { titleId: string; isActive?: boolean; favorite?: boolean }) =>
|
||||
updateProjectTitle(titleId, { is_active: isActive, favorite }),
|
||||
onSuccess: () => {
|
||||
message.success('标题状态已更新');
|
||||
queryClient.invalidateQueries({ queryKey: ['project-titles', projectId] });
|
||||
@@ -64,13 +68,23 @@ const ProjectTitles: React.FC = () => {
|
||||
onError: () => message.error('标题状态更新失败'),
|
||||
});
|
||||
|
||||
const filteredTitles = useMemo(() => {
|
||||
const keyword = searchText.trim().toLowerCase();
|
||||
return (titlesQuery.data || []).filter((item) => {
|
||||
if (categoryFilter !== 'all' && item.category !== categoryFilter) return false;
|
||||
if (favoriteOnly && !item.favorite) return false;
|
||||
if (keyword && !item.text.toLowerCase().includes(keyword)) return false;
|
||||
return true;
|
||||
});
|
||||
}, [categoryFilter, favoriteOnly, searchText, titlesQuery.data]);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<Card title="标题库" extra={<Button onClick={() => titlesQuery.refetch()}>刷新</Button>}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="inline"
|
||||
initialValues={{ category: 'default' }}
|
||||
initialValues={{ category: 'default', favorite: false }}
|
||||
onFinish={(values) => createMutation.mutate(values)}
|
||||
>
|
||||
<Form.Item name="text" rules={[{ required: true, message: '请输入标题' }, { max: 200, message: '标题最多 200 字' }]} style={{ flex: 1 }}>
|
||||
@@ -79,6 +93,9 @@ const ProjectTitles: React.FC = () => {
|
||||
<Form.Item name="category">
|
||||
<Select options={categoryOptions} style={{ width: 120 }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="favorite" valuePropName="checked">
|
||||
<Switch checkedChildren="常用" unCheckedChildren="普通" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={createMutation.isPending} disabled={!projectQuery.data}>
|
||||
新增标题
|
||||
@@ -88,13 +105,25 @@ const ProjectTitles: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
<Card title="项目标题">
|
||||
<Space wrap style={{ marginBottom: 16 }}>
|
||||
<Input.Search placeholder="搜索标题" allowClear onSearch={setSearchText} onChange={(event) => setSearchText(event.target.value)} style={{ width: 220 }} />
|
||||
<Select value={categoryFilter} onChange={setCategoryFilter} style={{ width: 140 }} options={[{ label: '全部分类', value: 'all' }, ...categoryOptions]} />
|
||||
<Switch checked={favoriteOnly} onChange={setFavoriteOnly} checkedChildren="仅常用" unCheckedChildren="全部" />
|
||||
</Space>
|
||||
<List
|
||||
loading={titlesQuery.isLoading}
|
||||
dataSource={titlesQuery.data || []}
|
||||
dataSource={filteredTitles}
|
||||
locale={{ emptyText: '暂无标题,先新增一个标题用于生成脚本和成片命名' }}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Switch
|
||||
key="favorite"
|
||||
checked={item.favorite}
|
||||
checkedChildren="常用"
|
||||
unCheckedChildren="普通"
|
||||
onChange={(checked) => toggleMutation.mutate({ titleId: item.id, favorite: checked })}
|
||||
/>,
|
||||
<Switch
|
||||
key="active"
|
||||
checked={item.is_active}
|
||||
@@ -108,6 +137,7 @@ const ProjectTitles: React.FC = () => {
|
||||
title={
|
||||
<Space wrap>
|
||||
<span>{item.text}</span>
|
||||
{item.favorite && <Tag color="gold">常用</Tag>}
|
||||
<Tag color={item.is_active ? 'green' : 'default'}>{item.is_active ? '启用' : '停用'}</Tag>
|
||||
<Tag>{categoryLabels[item.category] || item.category}</Tag>
|
||||
</Space>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from packages.adapters.sqlalchemy_impl.models import ProjectTitleModel
|
||||
|
||||
|
||||
def mark_title_used_for_generation(db, task) -> None:
|
||||
if not task.strategy_id:
|
||||
return
|
||||
title = db.query(ProjectTitleModel).filter(ProjectTitleModel.id == task.strategy_id).first()
|
||||
if title is None or title.workspace_id != task.workspace_id or title.project_id != task.project_id:
|
||||
return
|
||||
title.usage_count = int(title.usage_count or 0) + 1
|
||||
title.updated_at = datetime.now(timezone.utc)
|
||||
db.add(title)
|
||||
db.commit()
|
||||
@@ -9,6 +9,7 @@ from urllib.parse import urlparse
|
||||
|
||||
import oss2
|
||||
from worker_app.core.asset_usage import mark_asset_used_for_generation
|
||||
from worker_app.core.title_usage import mark_title_used_for_generation
|
||||
from worker_app.celery_app import celery_app
|
||||
from worker_app.db import SessionLocal
|
||||
|
||||
@@ -249,6 +250,7 @@ def generate_video(task_id: str) -> dict:
|
||||
task.error_message = ""
|
||||
task.completed_at = datetime.now(timezone.utc)
|
||||
task_repo.update(task)
|
||||
mark_title_used_for_generation(db, task)
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
|
||||
@@ -123,6 +123,7 @@ class ProjectTitleModel(Base):
|
||||
project_id = Column(String(36), nullable=False, index=True)
|
||||
text = Column(String(200), nullable=False)
|
||||
category = Column(String(50), nullable=False, default="default", index=True)
|
||||
favorite = Column(Boolean, nullable=False, default=False, index=True)
|
||||
usage_count = Column(Integer, nullable=False, default=0)
|
||||
is_active = Column(Boolean, nullable=False, default=True)
|
||||
created_by_user_id = Column(String(36), nullable=False)
|
||||
|
||||
@@ -25,6 +25,7 @@ class SQLAlchemyProjectTitleRepository:
|
||||
text: str,
|
||||
category: str,
|
||||
created_by_user_id: str,
|
||||
favorite: bool = False,
|
||||
) -> ProjectTitleModel:
|
||||
now = datetime.now(timezone.utc)
|
||||
item = ProjectTitleModel(
|
||||
@@ -33,6 +34,7 @@ class SQLAlchemyProjectTitleRepository:
|
||||
project_id=project_id,
|
||||
text=text.strip(),
|
||||
category=category,
|
||||
favorite=favorite,
|
||||
usage_count=0,
|
||||
is_active=True,
|
||||
created_by_user_id=created_by_user_id,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "worker"))
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.api.routes.generation_tasks import _select_title_id
|
||||
from packages.adapters.sqlalchemy_impl.models import Base, ProjectTitleModel
|
||||
from packages.adapters.sqlalchemy_impl.project_title_repository import SQLAlchemyProjectTitleRepository
|
||||
from worker_app.core.title_usage import mark_title_used_for_generation
|
||||
|
||||
|
||||
def _session_and_repository():
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(engine)
|
||||
session = sessionmaker(bind=engine)()
|
||||
return session, SQLAlchemyProjectTitleRepository(session)
|
||||
|
||||
|
||||
def test_select_title_prefers_favorite_then_lowest_usage():
|
||||
_, repository = _session_and_repository()
|
||||
normal = repository.create(
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
text="普通标题",
|
||||
category="default",
|
||||
created_by_user_id="user-1",
|
||||
)
|
||||
favorite = repository.create(
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
text="常用标题",
|
||||
category="default",
|
||||
favorite=True,
|
||||
created_by_user_id="user-1",
|
||||
)
|
||||
normal.usage_count = 0
|
||||
favorite.usage_count = 10
|
||||
repository.update(normal)
|
||||
repository.update(favorite)
|
||||
|
||||
assert _select_title_id(repository, "project-1") == favorite.id
|
||||
|
||||
|
||||
def test_mark_title_used_after_generation_completion():
|
||||
session, _ = _session_and_repository()
|
||||
now = datetime.now(timezone.utc)
|
||||
title = ProjectTitleModel(
|
||||
id="title-1",
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
text="生成标题",
|
||||
category="default",
|
||||
favorite=False,
|
||||
usage_count=2,
|
||||
is_active=True,
|
||||
created_by_user_id="user-1",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
session.add(title)
|
||||
session.commit()
|
||||
|
||||
mark_title_used_for_generation(
|
||||
session,
|
||||
SimpleNamespace(strategy_id="title-1", workspace_id="workspace-1", project_id="project-1"),
|
||||
)
|
||||
|
||||
updated = session.query(ProjectTitleModel).filter(ProjectTitleModel.id == "title-1").first()
|
||||
assert updated.usage_count == 3
|
||||
@@ -25,11 +25,13 @@ def test_project_title_repository_creates_and_lists_titles():
|
||||
project_id="project-1",
|
||||
text=" 3 分钟看懂产品亮点 ",
|
||||
category="marketing",
|
||||
favorite=True,
|
||||
created_by_user_id="user-1",
|
||||
)
|
||||
|
||||
assert title.text == "3 分钟看懂产品亮点"
|
||||
assert title.category == "marketing"
|
||||
assert title.favorite is True
|
||||
assert title.usage_count == 0
|
||||
assert title.is_active is True
|
||||
assert repository.list_by_project("project-1") == [title]
|
||||
|
||||
Reference in New Issue
Block a user