From cb021b66d9a4acaa1c63c5de074b23805cd96f49 Mon Sep 17 00:00:00 2001 From: lingying Date: Wed, 16 Sep 2026 03:48:44 +0800 Subject: [PATCH 1/6] fix(#1894): fix unit test failures - mock attrs + category default - Add title_text/title_category/title_config to all MagicMock instances in route tests - Fix assertion: title_category default is '' not 'default' --- tests/unit/test_scripts_routes.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_scripts_routes.py b/tests/unit/test_scripts_routes.py index 4444a19a3..8e2cbc49a 100644 --- a/tests/unit/test_scripts_routes.py +++ b/tests/unit/test_scripts_routes.py @@ -116,7 +116,7 @@ class TestScriptResponse: segments=[], tags=[], created_at=now, updated_at=now, ) assert r.title_text == "" - assert r.title_category == "default" + assert r.title_category == "" assert r.title_config == {} @@ -167,6 +167,9 @@ class TestRouteHandlers: mock_script.content = "内容" mock_script.segments = [{"text": "段1", "duration": None}] mock_script.tags = [] + mock_script.title_text = "" + mock_script.title_category = "" + mock_script.title_config = {} mock_script.created_at = datetime(2026, 9, 8, tzinfo=timezone.utc) mock_script.updated_at = datetime(2026, 9, 8, tzinfo=timezone.utc) svc.create_script.return_value = mock_script @@ -189,6 +192,9 @@ class TestRouteHandlers: mock_script.content = "" mock_script.segments = [] mock_script.tags = [] + mock_script.title_text = "" + mock_script.title_category = "" + mock_script.title_config = {} mock_script.created_at = datetime(2026, 9, 8, tzinfo=timezone.utc) mock_script.updated_at = datetime(2026, 9, 8, tzinfo=timezone.utc) svc.list_scripts.return_value = ([mock_script], 1) @@ -209,6 +215,9 @@ class TestRouteHandlers: mock_script.content = "" mock_script.segments = [] mock_script.tags = [] + mock_script.title_text = "" + mock_script.title_category = "" + mock_script.title_config = {} mock_script.created_at = datetime(2026, 9, 8, tzinfo=timezone.utc) mock_script.updated_at = datetime(2026, 9, 8, tzinfo=timezone.utc) svc.get_script.return_value = mock_script @@ -241,6 +250,9 @@ class TestRouteHandlers: mock_script.content = "原内容" mock_script.segments = [] mock_script.tags = [] + mock_script.title_text = "" + mock_script.title_category = "" + mock_script.title_config = {} mock_script.created_at = datetime(2026, 9, 8, tzinfo=timezone.utc) mock_script.updated_at = datetime(2026, 9, 8, tzinfo=timezone.utc) svc.update_script.return_value = mock_script @@ -286,3 +298,4 @@ class TestRouteHandlers: with pytest.raises(HTTPException) as exc_info: delete_script("bad", authenticated_user=auth, svc=svc) assert exc_info.value.status_code == 404 + -- 2.54.0 From b620b50f23231cd84d9aa8141d4f142f889a761a Mon Sep 17 00:00:00 2001 From: lingying Date: Wed, 16 Sep 2026 03:48:56 +0800 Subject: [PATCH 2/6] fix(#1894): remove conflicting 076_merge_title_libs migration (renamed to 077) --- alembic/versions/076_merge_title_libs.py | 67 ------------------------ 1 file changed, 67 deletions(-) delete mode 100644 alembic/versions/076_merge_title_libs.py diff --git a/alembic/versions/076_merge_title_libs.py b/alembic/versions/076_merge_title_libs.py deleted file mode 100644 index c3dc2d9da..000000000 --- a/alembic/versions/076_merge_title_libs.py +++ /dev/null @@ -1,67 +0,0 @@ -"""#1894: merge title_libraries into scripts — add title_text/title_category/title_config - -Revision ID: 076_merge_title_libs -Revises: 075_add_sentence_timings -Create Date: 2026-09-15 -""" - -import sqlalchemy as sa - -from alembic import context, op - -revision = "076_merge_title_libs" -down_revision = "075_add_sentence_timings" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - with op.batch_alter_table("scripts") as batch: - batch.add_column( - sa.Column("title_text", sa.String(500), nullable=False, server_default=""), - ) - batch.add_column( - sa.Column("title_category", sa.String(50), nullable=False, server_default=""), - ) - batch.add_column( - sa.Column("title_config", sa.JSON, nullable=False, server_default="{}"), - ) - - if context.get_context().dialect.name == "postgresql": - conn = op.get_bind() - result = conn.execute(sa.text("SELECT to_regclass('public.title_libraries')")) - if result.scalar() is not None: - conn.execute(sa.text(""" - INSERT INTO scripts - (id, user_id, title, content, segments, tags, - title_text, title_category, title_config, - created_at, updated_at) - SELECT - gen_random_uuid()::TEXT, - tl.user_id, - COALESCE(tl.name, '迁移标题'), - COALESCE(tl.text, ''), - '[]'::JSONB, - COALESCE(tl.tags, '[]'::JSONB), - COALESCE(tl.text, ''), - COALESCE(tl.category, ''), - COALESCE(tl."metadata", '{}'::JSONB), - tl.created_at, - tl.updated_at - FROM title_libraries tl - WHERE tl.is_active = true - AND NOT EXISTS ( - SELECT 1 FROM scripts s - WHERE s.user_id = tl.user_id - AND s.title_text = COALESCE(tl.text, '') - AND s.title_category = COALESCE(tl.category, '') - AND s.created_at = tl.created_at - ) - """)) - - -def downgrade() -> None: - with op.batch_alter_table("scripts") as batch: - batch.drop_column("title_config") - batch.drop_column("title_category") - batch.drop_column("title_text") -- 2.54.0 From d551ce62ac853c20e6146ada1172488132391177 Mon Sep 17 00:00:00 2001 From: lingying Date: Wed, 16 Sep 2026 03:49:18 +0800 Subject: [PATCH 3/6] fix(#1894): add 077_merge_title_libs migration (down_revision=076_membership_points) Replaces deleted 076_merge_title_libs. develop has 076_membership_points.py from PR #1919/#1936, so our migration must chain after it as 077. --- alembic/versions/077_merge_title_libs.py | 67 ++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 alembic/versions/077_merge_title_libs.py diff --git a/alembic/versions/077_merge_title_libs.py b/alembic/versions/077_merge_title_libs.py new file mode 100644 index 000000000..e526501f6 --- /dev/null +++ b/alembic/versions/077_merge_title_libs.py @@ -0,0 +1,67 @@ +"""#1894: merge title_libraries into scripts — add title_text/title_category/title_config + +Revision ID: 077_merge_title_libs +Revises: 076_membership_points +Create Date: 2026-09-15 +""" + +import sqlalchemy as sa + +from alembic import context, op + +revision = "077_merge_title_libs" +down_revision = "076_membership_points" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + with op.batch_alter_table("scripts") as batch: + batch.add_column( + sa.Column("title_text", sa.String(500), nullable=False, server_default=""), + ) + batch.add_column( + sa.Column("title_category", sa.String(50), nullable=False, server_default=""), + ) + batch.add_column( + sa.Column("title_config", sa.JSON, nullable=False, server_default="{}"), + ) + + if context.get_context().dialect.name == "postgresql": + conn = op.get_bind() + result = conn.execute(sa.text("SELECT to_regclass('public.title_libraries')")) + if result.scalar() is not None: + conn.execute(sa.text(""" + INSERT INTO scripts + (id, user_id, title, content, segments, tags, + title_text, title_category, title_config, + created_at, updated_at) + SELECT + gen_random_uuid()::TEXT, + tl.user_id, + COALESCE(tl.name, '迁移标题'), + COALESCE(tl.text, ''), + '[]'::JSONB, + COALESCE(tl.tags, '[]'::JSONB), + COALESCE(tl.text, ''), + COALESCE(tl.category, ''), + COALESCE(tl."metadata", '{}'::JSONB), + tl.created_at, + tl.updated_at + FROM title_libraries tl + WHERE tl.is_active = true + AND NOT EXISTS ( + SELECT 1 FROM scripts s + WHERE s.user_id = tl.user_id + AND s.title_text = COALESCE(tl.text, '') + AND s.title_category = COALESCE(tl.category, '') + AND s.created_at = tl.created_at + ) + """)) + + +def downgrade() -> None: + with op.batch_alter_table("scripts") as batch: + batch.drop_column("title_config") + batch.drop_column("title_category") + batch.drop_column("title_text") -- 2.54.0 From 29e80db7220f5676b5e0b6cdf6f381fbe6830e8e Mon Sep 17 00:00:00 2001 From: lingying Date: Wed, 16 Sep 2026 03:57:33 +0800 Subject: [PATCH 4/6] fix(#1894): add 076_membership_points.py from develop to fix alembic chain --- alembic/versions/076_membership_points.py | 133 ++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 alembic/versions/076_membership_points.py diff --git a/alembic/versions/076_membership_points.py b/alembic/versions/076_membership_points.py new file mode 100644 index 000000000..de3e5b95f --- /dev/null +++ b/alembic/versions/076_membership_points.py @@ -0,0 +1,133 @@ +"""add membership & points system + +Revision ID: 076_membership_points +Revises: 075_add_sentence_timings +Create Date: 2026-09-15 +""" + +import sqlalchemy as sa +from sqlalchemy import text + +from alembic import op + +revision = "076_membership_points" +down_revision = "075_add_sentence_timings" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # 1. users 表新增字段 + with op.batch_alter_table("users") as batch: + batch.add_column( + sa.Column("is_member", sa.Boolean(), nullable=False, server_default=sa.text("false")), + ) + batch.add_column( + sa.Column("member_type", sa.String(20), nullable=True), + ) + batch.add_column( + sa.Column("member_expires_at", sa.DateTime(), nullable=True), + ) + batch.add_column( + sa.Column("points_balance", sa.Integer(), nullable=False, server_default=sa.text("0")), + ) + + # 2. points_accounts 积分账户表 + op.create_table( + "points_accounts", + sa.Column("id", sa.String(36), primary_key=True), + sa.Column("user_id", sa.String(36), nullable=False, unique=True, index=True), + sa.Column("balance", sa.Integer(), nullable=False, server_default=sa.text("0")), + sa.Column("total_earned", sa.Integer(), nullable=False, server_default=sa.text("0")), + sa.Column("total_spent", sa.Integer(), nullable=False, server_default=sa.text("0")), + sa.Column( + "created_at", + sa.DateTime(), + nullable=False, + server_default=sa.text("NOW()"), + ), + sa.Column( + "updated_at", + sa.DateTime(), + nullable=False, + server_default=sa.text("NOW()"), + ), + ) + + # 3. points_transactions 积分流水表 + op.create_table( + "points_transactions", + sa.Column("id", sa.String(36), primary_key=True), + sa.Column("user_id", sa.String(36), nullable=False, index=True), + sa.Column("account_id", sa.String(36), nullable=False, index=True), + sa.Column("type", sa.String(20), nullable=False, index=True), + sa.Column("source", sa.String(50), nullable=False, index=True), + sa.Column("amount", sa.Integer(), nullable=False), + sa.Column("balance_after", sa.Integer(), nullable=False), + sa.Column("description", sa.String(255), nullable=False, server_default=""), + sa.Column("ref_id", sa.String(100), nullable=False, server_default=""), + sa.Column( + "created_at", + sa.DateTime(), + nullable=False, + server_default=sa.text("NOW()"), + ), + ) + + # 4. points_orders 积分/会员订单表 + op.create_table( + "points_orders", + sa.Column("id", sa.String(36), primary_key=True), + sa.Column("user_id", sa.String(36), nullable=False, index=True), + sa.Column("order_type", sa.String(20), nullable=False), + sa.Column("product_code", sa.String(50), nullable=False), + sa.Column("amount_cents", sa.Integer(), nullable=False), + sa.Column("original_amount_cents", sa.Integer(), nullable=False, server_default=sa.text("0")), + sa.Column("discount", sa.Float(), nullable=False, server_default=sa.text("1.0")), + sa.Column("points_amount", sa.Integer(), nullable=False, server_default=sa.text("0")), + sa.Column("status", sa.String(20), nullable=False, server_default="pending", index=True), + sa.Column("payment_method", sa.String(50), nullable=True), + sa.Column("payment_id", sa.String(100), nullable=True), + sa.Column("paid_at", sa.DateTime(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(), + nullable=False, + server_default=sa.text("NOW()"), + ), + ) + + # 5. daily_usage_records 每日使用记录表 + op.create_table( + "daily_usage_records", + sa.Column("id", sa.String(36), primary_key=True), + sa.Column("user_id", sa.String(36), nullable=False, index=True), + sa.Column("usage_date", sa.DateTime(), nullable=False), + sa.Column("usage_type", sa.String(50), nullable=False, server_default="free_clip"), + sa.Column("count", sa.Integer(), nullable=False, server_default=sa.text("0")), + sa.Column( + "updated_at", + sa.DateTime(), + nullable=False, + server_default=sa.text("NOW()"), + ), + sa.UniqueConstraint( + "user_id", + "usage_date", + "usage_type", + name="uq_daily_usage_user_date_type", + ), + ) + + +def downgrade() -> None: + op.drop_table("daily_usage_records") + op.drop_table("points_orders") + op.drop_table("points_transactions") + op.drop_table("points_accounts") + + with op.batch_alter_table("users") as batch: + batch.drop_column("points_balance") + batch.drop_column("member_expires_at") + batch.drop_column("member_type") + batch.drop_column("is_member") -- 2.54.0 From ab7595a4322602523f3da4fa9eae1b991f3a4f00 Mon Sep 17 00:00:00 2001 From: lingying Date: Wed, 16 Sep 2026 04:31:24 +0800 Subject: [PATCH 5/6] chore(ci): retrigger CI after fixes --- .ci-trigger | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci-trigger b/.ci-trigger index bf3d58bf7..9f0e1fcb3 100644 --- a/.ci-trigger +++ b/.ci-trigger @@ -1,2 +1,2 @@ CI trigger file - safe to delete -updated! \ No newline at end of file +retrigger at 2026-09-15 20:31:24 UTC -- 2.54.0 From fe9e0806eab04754464de25d8d76f2a35c393ad6 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Tue, 15 Sep 2026 21:24:33 +0000 Subject: [PATCH 6/6] style: auto-format with black + isort + ruff + prettier [skip ci-format-check] --- tests/unit/test_points_gate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_points_gate.py b/tests/unit/test_points_gate.py index 01c21a30a..6fc8e913e 100644 --- a/tests/unit/test_points_gate.py +++ b/tests/unit/test_points_gate.py @@ -206,9 +206,10 @@ class TestPointsGateGlobalsBinding: """Toggling _pg_module._points_gate_enabled must flip what the wrapper sees (proxy pattern), not just a stale local in the decorator closure. """ - import packages.middleware.points_gate as _pg from app.api.routes import generation_tasks + import packages.middleware.points_gate as _pg + monkeypatch.setattr(_pg, "_points_gate_enabled", lambda: True) # if the wrapper bound a stale local, this would still be False assert _pg._points_gate_enabled() is True -- 2.54.0