diff --git a/api/alembic/versions/0020_add_tts_audio_urls_column.py b/api/alembic/versions/0020_add_tts_audio_urls_column.py index e9b603a..0abff0b 100644 --- a/api/alembic/versions/0020_add_tts_audio_urls_column.py +++ b/api/alembic/versions/0020_add_tts_audio_urls_column.py @@ -1,51 +1,66 @@ -"""补建缺失列:segments.tts_audio_urls, conversation_messages.tts_audio_urls +"""补建所有 ORM 模型中存在但数据库缺失的列 0001 用 create_all 建表,对已有表不会 ALTER 追加列。 -本迁移补齐 ORM 模型中存在但生产库缺失的 tts_audio_urls 列。 +本迁移自动内省 ORM 元数据与实际数据库 schema 的差异, +为每张已有表补齐缺失列(不含 FK 约束,仅补列定义)。 -Revision ID: 0020_add_tts_audio_urls_column +Revision ID: 0020_backfill_all_missing_columns Revises: 0019_backfill_missing_columns """ +from __future__ import annotations + from typing import Sequence, Union import sqlalchemy as sa from alembic import op -revision: str = "0020_add_tts_audio_urls_column" +revision: str = "0020_backfill_all_missing_columns" down_revision: Union[str, None] = "0019_backfill_missing_columns" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None -def _column_names(table_name: str) -> set[str]: - bind = op.get_bind() - inspector = sa.inspect(bind) - return {column["name"] for column in inspector.get_columns(table_name)} +def _import_all_models() -> None: + from app.features.asset import models as _asset # noqa: F401 + from app.features.auth import models as _auth # noqa: F401 + from app.features.conversation import models as _conv # noqa: F401 + from app.features.evaluation import models as _eval # noqa: F401 + from app.features.memory import models as _memory # noqa: F401 + from app.features.memoir import models as _memoir # noqa: F401 + from app.features.payment import models as _payment # noqa: F401 + from app.features.story import models as _story # noqa: F401 + from app.features.user import models as _user # noqa: F401 def upgrade() -> None: - seg_cols = _column_names("segments") - if "tts_audio_urls" not in seg_cols: - op.add_column( - "segments", - sa.Column("tts_audio_urls", sa.JSON(), nullable=True), - ) + from app.core.db import Base - msg_cols = _column_names("conversation_messages") - if "tts_audio_urls" not in msg_cols: - op.add_column( - "conversation_messages", - sa.Column("tts_audio_urls", sa.JSON(), nullable=True), - ) + _import_all_models() + + bind = op.get_bind() + inspector = sa.inspect(bind) + existing_tables = set(inspector.get_table_names()) + + for table in Base.metadata.sorted_tables: + if table.name not in existing_tables: + continue + + db_cols = {c["name"] for c in inspector.get_columns(table.name)} + + for col in table.columns: + if col.name in db_cols: + continue + + kwargs: dict = {"nullable": True} + + if col.server_default is not None: + kwargs["server_default"] = col.server_default.arg + + new_col = sa.Column(col.name, col.type, **kwargs) + op.add_column(table.name, new_col) def downgrade() -> None: - msg_cols = _column_names("conversation_messages") - if "tts_audio_urls" in msg_cols: - op.drop_column("conversation_messages", "tts_audio_urls") - - seg_cols = _column_names("segments") - if "tts_audio_urls" in seg_cols: - op.drop_column("segments", "tts_audio_urls") + pass diff --git a/app-expo/docs/testing.md b/app-expo/docs/testing.md index d580da8..ac726ce 100644 --- a/app-expo/docs/testing.md +++ b/app-expo/docs/testing.md @@ -137,3 +137,55 @@ E2E 不是默认门禁。 - 团队能承担维护成本 引入后也只测关键主路径,不做像素巡检,不做大面积 UI 回归脚本。 + +### 前置准备 + +安装 Maestro CLI: + +```bash +curl -Ls "https://get.maestro.mobile.dev" | bash +``` + +构建并安装到目标 iOS Simulator / Android Emulator / 真机。不同 flow 对构建方式的要求不同: + +| 场景 | 构建命令 | 读取配置 | +|------|---------|---------| +| 普通开发 | `pnpm ios` / `pnpm android` | `.env.development` | +| 需要登录态的 E2E | `pnpm ios:e2e` / `pnpm android:e2e` | `.env.e2e`(staging backend + E2E 开关) | + +正常 staging 构建继续使用 `.env.staging`,不注入 `EXPO_PUBLIC_E2E`。 + +### 现有 flow + +| Flow 文件 | 用途 | 需要后端 | 需要 E2E 构建 | +|-----------|------|---------|--------------| +| `login-smoke.yaml` | 登录页协议拦截 smoke,验证 App 启动到未登录态 | 否 | 否 | +| `login-authenticated.yaml` | 快捷登录进入已登录态 | 是 | 是 | +| `post-login-tabs-smoke.yaml` | 登录后 Tab 导航 smoke(对话 → 回忆录 → 我的 → 对话) | 是 | 是 | +| `post-login-long-chat.yaml` | 登录后发送大量对话消息,为回忆录生成提供种子数据 | 是 | 是 | + +### 运行命令 + +无后端依赖(普通构建即可): + +```bash +pnpm e2e:ios +pnpm e2e:android +``` + +需要 E2E 构建 + 后端 mock 登录: + +```bash +pnpm e2e:auth:ios +pnpm e2e:auth:android +pnpm e2e:post-login:ios +pnpm e2e:long-chat:ios +``` + +调试时可通过 `pnpm start:e2e` 启动 dev server 并自动加载 `.env.e2e`。 + +### 登录后 E2E 环境要求 + +- 后端:`APP_ENV` 不能是 `production`,并开启 `MOCK_SMS_LOGIN_ENABLED=1` +- App:通过 `pnpm ios:e2e` / `pnpm android:e2e` 构建安装,读取 `.env.e2e` +- 所有需要登录态的 flow 通过点击 `login.e2e.quickLogin.button` 进入已登录态