SurfSense 后端 E2E 测试基座全解:sys.modules 劫持、确定性 Fake 与 Playwright 无网环境实战
【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense
本文是 SurfSense 开源仓库中 surfsense_backend/tests/e2e/README.md 的深度展开。SurfSense 是一个连接 Google Drive、Gmail、Notion、Confluence、Linear、Jira、Slack、ClickUp、Dropbox、OneDrive 等众多外部服务的开源研究平台,其后端 E2E(端到端)测试面临一个棘手问题:测试必须跑通真实的生产路由代码,却又不能真的去调用这些第三方 SDK 与 LLM 服务。本文围绕这套 "Backend E2E Harness" 展开,讲解它如何通过
sys.modules劫持与严格的确定性 Fake,让 Playwright 在完全离线、无外部依赖的环境下完成全链路验证,并给出从零搭建到 CI 复现的完整实操流程。读完你不仅能直接跑起 SurfSense 的 E2E 套件,还能把同样的"测试专用入口 + 模块级注入 + 严格失败"模式复用到自己的多 SDK 项目中。
一、这套 E2E 基座解决什么问题
在介绍细节之前,先理解 SurfSense 后端 E2E 的两条硬约束:
- 要测真实生产代码:E2E 的价值在于从 Playwright 浏览器一直打到 FastAPI 路由、Celery 异步任务、索引管线(解析、分块、向量化、入库)的完整链路,因此不能像单元测试那样 mock 掉路由层;
- 绝不能碰真实外部服务:Composio、Notion、Confluence、Google 系工具、各种 LLM 提供商既不允许真实出网(成本、稳定性、认证都不可控),也不应该在 CI 里被意外打到。
SurfSense 的答案就是surfsense_backend/tests/e2e/这个"测试专用后端基座"——它包含测试专属的启动入口和一组替换外部 SDK 的 Fake,仅由 Playwright 使用,不属于生产镜像:
本目录包含 Playwright 使用的仅测试后端入口和 Fake。它们不是生产镜像的一部分:
.dockerignore排除了tests/,E2E Docker 阶段通过单独的构建上下文复制该目录。
目录文件清单
| 路径 | 用途 |
|---|---|
run_backend.py | 先把测试 Fake 装进sys.modules,再启动 FastAPI(生产main.py完全不变) |
run_celery.py | 以同样的 Fake 配置启动 Celery worker |
middleware/scenario.py | 读取X-E2E-Scenario请求头,写入请求作用域的 context var |
fakes/composio_module.py | 供连接器流程使用的composio包 Fake |
fakes/llm.py | 假聊天模型工厂 |
fakes/embeddings.py | 确定性 embedding 辅助函数 |
fakes/fixtures/drive_files.json | Drive 夹具数据与 canary 文件内容 |
实际仓库中,fakes/目录远不止文档列出的这几个文件,还包括chat_llm.py(模拟带工具调用行为的聊天模型)、notion_module.py、confluence_indexer.py、docling_service.py、dropbox_api.py、jira_module.py、linear_module.py、mcp_runtime.py、mcp_oauth_runtime.py、native_google.py、onedrive_graph.py、slack_module.py、clickup_module.py等,以及按连接器组织的 JSON 夹具(gmail_messages.json、calendar_events.json、confluence_pages.json、jira_issues.json、linear_issues.json、notion_pages.json、onedrive_files.json、slack_messages.json、dropbox_files.json、clickup_tasks.json)和二进制 PDF 夹具(fakes/fixtures/binary/)。详见 surfsense_backend/tests/e2e/fakes/。
二、为什么需要 import hook:模块加载时机问题
核心难点在文档中有明确的解释:
一些生产模块在模块加载时就会导入 SDK 客户端,例如
from composio import Composio。到app.app被导入时,这些绑定已经固定下来。
这是 Python 里很常见的"时机陷阱":如果等生产模块已经执行完from composio import Composio再去 patch,绑定已经固化在模块命名空间里,unittest.mock.patch只能从外部替换属性,无法挽回sys.modules缓存造成的早期绑定。所以 E2E 入口必须在任何app.*模块被导入之前,先把 Fake 模块装进sys.modules:
# tests/e2e/run_backend.py def _hijack_external_sdks() -> None: import tests.e2e.fakes.composio_module as _fake_composio import tests.e2e.fakes.notion_module as _fake_notion sys.modules["composio"] = _fake_composio sys.modules["notion_client"] = _fake_notion sys.modules["notion_client.errors"] = _fake_notion.errors此后,生产代码里任何from composio import Composio、import notion_client都会解析到本地 Fake,而生产代码本身一行不改——文档强调"生产代码在有无此文件时字节级一致"。
引导(bootstrap)顺序是严格有讲究的
run_backend.py的_bootstrap()明确标注了每一步的顺序是"load-bearing"(承重墙级别):
- 在
sys.modules中劫持composio与notion_client; - 加载
.env并设置环境变量默认值(app.config在导入时会读环境变量); - 配置日志;
- 物化一份合成的
global_llm_config.yaml,保证 Auto 模式的模型固定解析能找到候选; - 导入生产应用
app.app(它传递性地导入已"假化"的外部 SDK,并读取环境默认值与 YAML); - 在所有消费点 patch LLM / embedding 绑定;
- 把测试专用中间件与
/__e2e__路由挂到应用上。
从源码结构看,这种"先劫持模块 → 再设环境 → 后导入生产代码 → 最后补 patch"的顺序,保证了生产代码在导入那一刻看到的就是 Fake 世界,这是整个方案成立的前提。
环境变量默认值:sentinel 哨兵机制
_load_dotenv_and_set_env_defaults()是一个值得借鉴的细节:它用setdefault给每个生产配置需要的环境变量设置默认值,与 docker/docker-compose.deps-only.yml 中db/redis的默认端口、账号完全对齐(postgresql+asyncpg://postgres:postgres@localhost:5432/surfsense、redis://localhost:6379/0),因此本地跑 E2E不需要.env文件。
更有意思的是 sentinel 哨兵键:
os.environ.setdefault("COMPOSIO_API_KEY", "local-deny-real-call-sentinel") os.environ.setdefault("COMPOSIO_ENABLED", "TRUE") os.environ.setdefault("OPENAI_API_KEY", "local-deny-real-call-sentinel") os.environ.setdefault("ANTHROPIC_API_KEY", "local-deny-real-call-sentinel") os.environ.setdefault("LITELLM_API_KEY", "local-deny-real-call-sentinel")Fake 从不读取这些值,它们的意义在于:一旦有任何泄漏的真实调用逃出 Fake 层,就会带着无效 key 打到真实提供商并收到 401,从而"大声失败"。这是文档提到的"纵深防御"第一层,配合 Fake 内部的严格__getattr__(第二层)和 CI 里的网络拒绝(第三层,见第五节)。
加载顺序还有一个优先级设计:load_dotenv()在最后调用,且python-dotenv不会覆盖已存在的环境变量,因此E2E 默认值 > 开发者.env,而显式 export 的 shell 变量 > 两者——本地调试时不会被.env里的AUTH_TYPE=GOOGLE之类配置意外干扰。
合成全局 LLM 配置
_install_synthetic_global_llm_config()解决的是 CI 全新检出场景:真实的app/config/global_llm_config.yaml被 gitignore(由生产运维自带真实 key),新检出目录里没有这个文件,会导致 Auto 模式在每次聊天流式请求时报 "No usable global LLM configs are available for Auto mode"。方案是把tests/e2e/fixtures/global_llm_config.yaml拷到生产期望的位置(仅当目标不存在时),纯属测试期脚手架,不碰生产代码。
LLM 绑定 patch:patch 在导入之后、每个消费点
由于unittest.mock.patch无法挽回导入期的from ... import ...绑定,_patch_llm_bindings()的做法是逐个消费点显式 patch:
targets = [ "app.services.llm_service.get_agent_llm", "app.tasks.connector_indexers.confluence_indexer.get_agent_llm", "app.tasks.connector_indexers.google_drive_indexer.get_agent_llm", "app.tasks.connector_indexers.google_gmail_indexer.get_agent_llm", "app.tasks.connector_indexers.notion_indexer.get_agent_llm", "app.tasks.connector_indexers.onedrive_indexer.get_agent_llm", "app.tasks.connector_indexers.dropbox_indexer.get_agent_llm", "app.tasks.connector_indexers.local_folder_indexer.get_agent_llm", "app.tasks.document_processors._save.get_agent_llm", "app.tasks.document_processors.markdown_processor.get_agent_llm", ]同时还有一组 chat 流式模型工厂(app.agents.chat.runtime.llm_config.create_chat_litellm_from_agent_config、create_chat_litellm_from_config,以及app.tasks.chat.streaming.flows.shared.llm_bundle下的两个对应函数)被替换为tests.e2e.fakes.chat_llm的假工厂。patch 失败(如某个 indexer 未加载)会打印 warning 而非静默放过——源码注释明确警告:"如果生产代码在 E2E 里走到这条路径,就会打到真实提供商,请更新run_backend.py"。
Celery worker 为什么要单独入口
run_celery.py几乎是run_backend.py的镜像(同样的劫持、环境默认值、LLM patch、运行时 Fake 安装),区别在于导入的是app.celery_app而不是app.app。原因是:
Celery worker 运行在独立的 Python 解释器中,所以 patch 必须在这里也应用一遍——它们不会从 FastAPI 进程带过来。
这正是许多人容易踩的坑:以为 mock 是跨进程共享的。_main()里还包含平台相关的 worker 参数:
# macOS forks-after-MPS-init crash prefork workers; threads avoid it. default_pool = "threads" if sys.platform == "darwin" else "prefork"并默认监听surfsense,surfsense.connectors两个队列、并发数 2,可通过CELERY_POOL、CELERY_CONCURRENCY、CELERY_TASK_DEFAULT_QUEUE覆盖。
三、Fake 的设计哲学:严格失败,绝不静默穿透
文档给出了 Fake 的总原则:
Fake 应该大声失败。如果生产代码开始使用 Fake 未实现的新 SDK 方法,请把该方法加进 Fake,而不是让测试去调用真实服务。
3.1 严格 mixin:未知属性一律NotImplementedError
以 fakes/composio_module.py 为例,所有假类继承_StrictFakeMixin:
class _StrictFakeMixin: def __getattr__(self, name: str) -> Any: raise NotImplementedError( f"E2E Composio fake missing surface: {self._component_name}.{name!r}. " f"If production code needs this, add an explicit method to " f"surfsense_backend/tests/e2e/fakes/composio_module.py — " f"the strict fake refuses to silently fall through to the real SDK." )同理,_Tools.execute遇到没有建模的 tool slug 时也抛出NotImplementedError("E2E Composio fake has no handler for tool slug ...")。这样一旦生产代码引入新的 SDK 调用,CI 会立刻给出清晰的"请把该接口补进 Fake"提示,而不是让真实 SDK 悄悄穿透。
3.2 顶层Composio类的形状
生产代码的调用方式是Composio(api_key=..., file_download_dir=...),因此 Fake 提供同名构造,并在实例上挂出connected_accounts、tools、auth_configs三个子客户端:
connected_accounts.initiate(...):模拟 OAuth 发起。真实 SDK 的redirect_url指向 Composio 托管的 OAuth 界面再跳第三方;E2E 里直接短路回我们自己的同源回调,从而彻底避免真实网络需求。它还根据 scenario 决定回调 URL 带connectedAccountId=...(成功)还是?error=access_denied(拒绝场景)。connected_accounts.wait_for_connection / get / delete / refresh:返回固定 ACTIVE 状态的_ConnectedAccount。auth_configs.list():为每个要测的 toolkit 返回一个 auth config(googledrive、gmail、googlecalendar)。tools.execute(slug=..., connected_account_id=..., arguments=...):按 slug 分发给 Drive / Gmail / Calendar 的处理器。
值得注意的细节是initiate的 account id 合成规则:fake-acct-{toolkit_id}-{user_id},同一实体上同一 toolkit 会得到相同 ID——这样"重复连接"场景能稳定触发composio_routes.py里的重连分支。
3.3 用夹具模拟文件列表与下载
tools.execute的 Drive 处理器会解析 Drive 风格的q=查询(如'<folder_id>' in parents and trashed = false),从中抽取 folder id,再到_DRIVE_FIXTURE(来自drive_files.json)里取对应列表,并模拟trashed = false、mimeType !=、mimeType =等过滤条件。下载场景则把夹具内容写进/tmp/surfsense-e2e-composio-downloads/并返回文件路径——与真实 SDK "下载字节到本地文件并返回路径"的行为一致,生产侧composio_service.py读取字节的逻辑无需任何改动。
drive_files.json的结构是"文件夹 id → 文件列表"的映射,外加两个特殊键:
"_file_contents": { "fake-file-canary": "Canary token for E2E tests: SURFSENSE_E2E_CANARY_TOKEN_DRIVE_001\n..." }, "_file_binary_paths": { "fake-file-pdf-native": "binary/drive-canary.pdf", "fake-file-pdf-composio": "binary/composio-drive-canary.pdf" }文本文件直接内联内容,二进制(PDF)则指向fakes/fixtures/binary/下的真实 PDF 文件(fakes/fixtures/binary/generate_canary_pdfs.py用于生成它们)。canary token(如SURFSENSE_E2E_CANARY_TOKEN_DRIVE_001)是贯穿全链路的"信标":文件内容被索引后,测试断言可以在最终结果里找到这些标记,从而确认端到端管线真实跑通。
3.4 确定性 LLM 与 embedding
- fakes/llm.py:生产索引管线用
summary_chain = SUMMARY_PROMPT_TEMPLATE | llm总结文档,llm由app.services.llm_service.get_agent_llm按文档提供。Fake 把该函数替换成返回 langchain 原生的FakeListChatModel,固定返回"E2E_FAKE_SUMMARY: Indexed by Playwright E2E run with deterministic LLM stub."——不碰任何真实 LLM 提供商包,其余链条(prompt 模板、chain 调用)原样运行。 - fakes/embeddings.py:
fake_embed_text / fake_embed_texts返回全 0.1 的np.float32向量,维度从config.embedding_model_instance.dimension动态解析,"与 documents.embedding 的 pgvector 列向量兼容"。它沿用了集成测试surfsense_backend/tests/integration/conftest.py中patched_embed_textsfixture 的模式,patch 目标覆盖源绑定、消费方绑定与索引缓存门面(app.indexing_pipeline.cache.cached_indexing.embed_texts),且 patch 失败会直接raise RuntimeError——注释解释得很直白:"静默穿透到真实 embedding 模型会既昂贵又不确定"。 - fakes/chat_llm.py:这是最"聪明"的 Fake——一个完整的
BaseChatModel子类FakeChatLLM,能模拟多轮工具调用。它维护一张 canary 表(Gmail、Calendar、Drive、OneDrive、Dropbox、Notion、Confluence、Linear、Jira、Slack、ClickUp、手动上传等),根据最新人类消息的关键词决定:是直接返回包含 canary token 的文本,还是先发一个tool_calls(如search_gmail→read_gmail_email)让生产 agent 执行真实(Fake 化的)工具、拿到工具结果后再作答。它甚至能模拟"主 agent 把连接器任务委派给mcp_discovery子 agent"的流程。_astream还把工具调用流式输出为AIMessageChunk,保证流式 SSE 路径也能被覆盖。
四、请求级场景编排:X-E2E-Scenario 中间件
单个 Fake 只能给出"一切顺利"的答案,但 E2E 还要测失败路径。middleware/scenario.py提供了一个极简而优雅的机制:把X-E2E-Scenario请求头读入一个ContextVar,Fake 按当前请求上下文切换行为。
_scenario: ContextVar[str] = ContextVar("e2e_scenario", default="happy") async def dispatch(self, request: Request, call_next) -> Response: value = request.headers.get("X-E2E-Scenario", "happy") token = _scenario.set(value) try: request.state.e2e_scenario = value return await call_next(request) finally: _scenario.reset(token)注意finally里的reset:ContextVar 是请求作用域的,请求结束即还原,不会串扰。该中间件只在 E2E 入口挂载(_install_test_only_app_extensions),生产环境既不会挂中间件、也不会读这个请求头。
支持的内置场景:
| 场景 | 行为 |
|---|---|
happy(默认) | 一切成功,返回确定性夹具 |
denied | Composio.connected_accounts.initiate返回指向我们回调、带?error=access_denied的跳转 URL,覆盖 OAuth 拒绝分支 |
auth_expired | GOOGLEDRIVE_LIST_FILES抛_AuthExpiredError(消息含 "expired or revoked"/"401"),路由层据此把连接器置为config.auth_expired |
duplicate | Fake 无特殊行为;重复路径靠"同一 toolkit 跑两次 OAuth 流程"自然触发 |
Fake 侧通过tests.e2e.middleware.scenario.current_scenario()惰性读取(延迟导入是为了规避 sys.modules 劫持期scenario尚不可导入的时序)。
测试专用 token 铸造端点
同属"测试专用扩展"的还有 auth_mint.py:POST /__e2e__/auth/token。它存在的理由是生产登录接口/auth/jwt/login有 5 次/分钟/IP 的限流,Playwright 的并行 worker 会把它打爆。mint 端点用X-E2E-Mint-Secret共享密钥做认证(默认local-e2e-mint-secret-not-for-production,CI 里由docker-compose.e2e.yml的E2E_MINT_SECRET与 GitHub Actions 工作流共同持有),然后直接为已 seed 的e2e-test@surfsense.net用户签发 JWT。auth_mint.install(app)与ScenarioMiddleware一起由_install_test_only_app_extensions挂载,同样永远不会进入生产镜像。
五、共享给后端集成测试
文档还展示了一个关键复用:后端集成测试在"需要真实路由代码但不想碰真实 SDK"时,可以直接复用同一套 Fake:
from tests.e2e.fakes import composio_module as _fake_composio sys.modules["composio"] = _fake_composio from app.app import app当前实践见 surfsense_backend/tests/integration/composio/conftest.py——它的 docstring 写明"composio 的 sys.modules 劫持位于父级 integration conftest",并把config.COMPOSIO_API_KEY设为"e2e-integration-composio-sentinel"。这意味着同一条"模块级注入"技术同时服务了 E2E 与集成测试两层,维护成本共享。
六、本地运行 E2E:完整实操流程
文档给出的推荐本地流程是:只把 Postgres 和 Redis 跑在 Docker 里,后端与 Celery worker 跑在宿主机上。两个入口用setdefault提供了所有必需变量的默认值,因此不需要.env。
一次性准备
从surfsense_web/执行:
pnpm install pnpm exec playwright install --with-deps chromium每次运行:五个步骤
1. 拉起 Postgres + Redis(在仓库根目录;deps-only 里的 Zero、pgAdmin 等其余服务 E2E 用不到):
docker compose -f docker/docker-compose.deps-only.yml up -d db redis2. 启动后端(surfsense_backend/,终端 A):
uv sync uv run alembic upgrade head uv run python tests/e2e/run_backend.pyuv sync安装依赖,alembic upgrade head把 170+ 个迁移版本跑到最新,最后启动带 Fake 的 FastAPI(默认0.0.0.0:8000,可用UVICORN_HOST、UVICORN_PORT、UVICORN_LOG_LEVEL覆盖)。
3. 启动 Celery worker(surfsense_backend/,终端 B):
uv run python tests/e2e/run_celery.py4. 注册 Playwright 用户:
curl -X POST http://localhost:8000/auth/register \ -H "Content-Type: application/json" \ -d '{"email":"e2e-test@surfsense.net","password":"E2eTestPassword123!"}'5. 运行 Playwright(surfsense_web/,终端 C):
pnpm test:e2e # dev server(快速迭代) pnpm test:e2e:headed # 显示浏览器 pnpm test:e2e:ui # Playwright UI 模式 pnpm test:e2e:prod # build + start(与 CI 完全一致)这四条命令对应 surfsense_web/package.json 里的脚本(另有test:e2e:debug、test:e2e:report、test:e2e:install)。playwright.config.ts与运行脚本共享默认值,因此全新检出即可工作;只有当你把测试指向另一套堆栈时,才需要设置PLAYWRIGHT_TEST_EMAIL、PLAYWRIGHT_TEST_PASSWORD、NEXT_PUBLIC_FASTAPI_BACKEND_URL或后端环境变量(如DATABASE_URL)。
清理
docker compose -f docker/docker-compose.deps-only.yml down加-v会一并清掉 Postgres 数据卷。
七、与 CI 完全一致的 Hermetic 方案
如果要把 CI 环境精确复现——后端和 Celery 都在容器里、网络出口在 L3 层被禁止——可以把上面第 1–3 步替换为:
docker compose -f docker/docker-compose.e2e.yml up -d --build --wait然后照常执行第 4 步(curl 注册)和第 5 步(pnpm test:e2e:prod)。拆除:
docker compose -f docker/docker-compose.e2e.yml down -v --remove-orphans文档提醒:这会构建约 9 GB 的surfsense-e2e-backend:local镜像,所以日常开发用上面的 deps-only 流程更快。
Hermetic 堆栈的源码级细节
docker/docker-compose.e2e.yml 揭示了这个方案的多层防线:
- 网络隔离:
internal网络声明为internal: true(纯内部网桥,无到宿主机/互联网的路由),db、redis、celery_worker都只挂在这张网上;backend额外挂一张普通ingress网桥,仅为让宿主机能访问:8000。celery worker 的对外攻击面为零。 - 代理哨兵:即使 backend 有出网路径,容器环境里的
HTTPS_PROXY=http://127.0.0.1:1/HTTP_PROXY会把任何泄漏的 Python 出站 HTTP 调用变成"连接被拒"(NO_PROXY只放行 localhost、db、redis 等内部目标)。这就是文档说的"L3 拒绝"之上的应用层兜底。 - 离线 HuggingFace:
HF_HUB_OFFLINE=1、TRANSFORMERS_OFFLINE=1,配合构建参数EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2,确保 embedding 模型也离线可用。 - 环境一致性:
backend-env锚点里的大部分变量与run_backend.py的setdefault默认值一一对应(DATABASE_URL指向db:5432/surfsense_e2e,AUTH_TYPE=LOCAL,REGISTRATION_ENABLED=TRUE,ETL_SERVICE=DOCLING,sentinel API keys……),另外设置了E2E_MINT_SECRET供 token mint 端点使用。 - 镜像构建:
backend服务用build.target: e2e构建(见 surfsense_backend/Dockerfile 中base → deps → models → {e2e, production}的阶段图),并通过additional_contexts把被.dockerignore排除的tests/以tests-source上下文注入(COPY --from=tests-source . ./tests/)——这样测试 Fake 物理上不会进入生产镜像,与文档开头"不属于生产镜像"的承诺闭环。celery_worker不重复构建,pull_policy: never直接复用backend构建出的同一镜像。 - 健康检查:backend 用 Python 标准库探测
/openapi.json(避免依赖镜像里不一定装的 curl/wget),celery 用celery inspect ping检查 worker 存活。
八、如何新增一个 Fake(可操作的清单)
文档给出了新增 Fake 的三步清单,结合源码可以补充更多细节:
- 新增
fakes/<sdk>_module.py:参考composio_module.py的写法——顶层类用_StrictFakeMixin,未建模属性抛NotImplementedError;建模生产代码实际会用到的子客户端与方法;需要失败路径时读tests.e2e.middleware.scenario.current_scenario()分支。 - 在
run_backend.py和run_celery.py中都注册:必须放在import app.app/import app.celery_app之前。两类入口各维护一套_hijack_external_sdks()、_patch_llm_bindings()、_install_runtime_fakes(),因为 FastAPI 与 Celery 是两个独立进程,patch 不会跨进程传递。 - 如果需要按测试变化的行为:从
tests.e2e.middleware.scenario.current_scenario()读取当前场景。同时注意:若 Fake 需要连接器级数据,把夹具加进fakes/fixtures/下对应 JSON(文本走_file_contents,二进制走_file_binary_paths指向fakes/fixtures/binary/)。
文档还给出了一条硬性纪律:生产代码引入新的 SDK 方法时,把方法补进 Fake,而不是让测试调用真实服务——这是整套方案"测试可信度"的根基。
九、设计启示:这套模式的通用价值
把 SurfSense 的 Backend E2E Harness 抽象出来,可以看到一个可迁移到任何"多 SDK 接入型"项目的模式:
- 测试专用入口而非改造生产入口:
run_backend.py/run_celery.py与生产main.py/celery_worker.py并存,生产代码零侵入、字节级一致,且通过 Docker 多阶段构建 +.dockerignore从物理上保证测试代码不进生产镜像; - sys.modules 劫持早于任何业务导入:解决了"模块加载期绑定固定"这一 mock 工具的先天盲区;
- 严格失败的 Fake 契约:
NotImplementedError让"Fake 与生产 SDK 面漂移"在 CI 立刻暴露,而不是静默打到真实服务; - 确定性数据 + canary token:固定向量、固定摘要、内容里的标记 token,让 E2E 断言既稳定又可读;
- 请求级场景编排:一个
X-E2E-Scenario头 + ContextVar,就同时覆盖了 happy path 与 OAuth 拒绝、凭证过期、重复连接等异常分支; - 多层纵深防御:模块劫持(第一层)→ 严格
__getattr__(第二层)→ 哨兵 API key + 代理拒绝 + L3 内网隔离(第三层),任何一层失守都会被大声暴露。
如果你在维护一个重度依赖第三方 SDK 的后端,SurfSense 这套 surfsense_backend/tests/e2e/ 的实现是值得通读的参考范本;配套的 docker-compose.e2e.yml 与 surfsense_backend/tests/integration/composio/conftest.py 则分别展示了容器化隔离与"一套 Fake 两处复用"的落地姿势。
【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考