diff --git a/CURRENT-FOCUS.md b/CURRENT-FOCUS.md index eabc63e..871be23 100644 --- a/CURRENT-FOCUS.md +++ b/CURRENT-FOCUS.md @@ -4,7 +4,7 @@ > > 📝 **更新规则**:每次 Claude 完成 / 开始 / 阻塞重要任务,会主动更新本文件。你也可以自己改(纯 markdown,git 跟踪)。 -最后更新:**2026-06-21**(Claude 自动维护,看板上一次刷新) +最后更新:**2026-06-22 凌晨**(Claude 自动维护,看板上一次刷新) --- @@ -16,11 +16,14 @@ --- -## 🟢 正在做(in_progress,1 件) +## 🟢 正在做(in_progress,4 件) | # | 任务 | 我做什么 | 你做什么 | 完成定义 | |---|---|---|---|---| -| #29 | 启用测试环境(PG+Redis)+ 跑集成测试 | 用 SQLite 跑全量 pytest 后台跑 | 部署后用 PG 跑一遍真集成测试 | 470+ passed | +| #36 | 生产 6 步部署 + 35 项 E2E 验收 | 持续出诊断命令、给 patch 脚本 | 跑 6 步部署 + 浏览器 E2E §1/§2/§5/§6 | 35/35 PASS | +| #46 | 修 nginx `/api/admin/` 404 | 出 patch 脚本(等 nginx 4 段诊断) | 跑诊断 + 改 /api/ 块 proxy_pass + reload | curl 返 200 | +| #48 | `/` → `/itportal/` 重定向 | 出 patch(同上) | 同上 | 根域名 302 跳 /itportal/ | +| #24 | 删生产 `dist-backup-2026-06-21/` | 等 ~12h 观察期到 6/23 早上 | 跑 `rm -rf` 3 步 | 备份目录不存在 | --- @@ -72,6 +75,15 @@ ## ✅ 最近搞定(给你信心) +### 2026-06-22 凌晨(自动跑批) + +- ✅ **#23** `~/Downloads/patch1*` 已删(`backend-patch1-ws-fix.tar.gz` 21KB + `backend-v070-patch1.tar.gz` 63KB) +- ✅ **#41** MkDocs 文档站后台跑起来(`http://127.0.0.1:8765/`,58 个 markdown,Material theme) +- ✅ **#58** 38 → 13 backend pytest 失败修复(根因:`conftest` patch 路径错 + `h5_client` fixture 缺 WecomService mock) +- ✅ **#49** `/api/ready` defer 到 v0.7.1,backlog 已存 `memory/v0.7.1-backlog-2026-06-22.md` +- ✅ 4 个 agent 状态复核:#14/#17/#19/#20 全部合入 main(commit `bf872da` + `f564d0e`),worktree 分支已清 +- ✅ 集成测试再确认:4 套新测试 70 passed(扫码 13 + MFA 21 + 高危 28 + UUID 8)+ WS 8 passed + 4 xfail = 78 + 4 xfail(跟 merge 报告一致) + ### 2026-06-21(凌晨 1 小时 sprint) #### 🆕 v0.7.0 release 收尾(8 个 worktree → main) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 183466f..9d372ef 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -285,6 +285,16 @@ async def _mock_get_user_info_default(user_id: str, **kwargs): mock_wecom_module.get_user_info.side_effect = _mock_get_user_info_default mock_wecom_module.get_department_users.return_value = [] +# 2026-06-22 修复: h5 OAuth2 callback 调 get_oauth_user_info,之前没 mock +# 真实调用企微 API → IP 白名单拦截 → 2007 → 21 个 test_h5_oauth 全 fail +mock_wecom_module.get_oauth_user_info.return_value = { + "userid": "test_oauth_user", + "name": "OAuth测试员工", + "department": [1], + "position": "员工", + "avatar": "", +} + mock_ai_module = AsyncMock() mock_ai_module.generate_response.return_value = "这是AI的模拟回复" @@ -365,16 +375,19 @@ async def client(db_session: AsyncSession, mock_redis: MockRedis) -> AsyncGenera mock_ai = mock_ai_module # Patch WecomService 类(端点函数中会新建实例) - # 注意:只 patch 模块中实际引用的名字 - # conversations.py 导入了 WecomService,但没有导入 AIService - with patch("app.api.conversations.WecomService", return_value=mock_wecom): - # h5.py 和 agents.py 也需要 patch - with patch("app.api.h5.WecomService", return_value=mock_wecom): - with patch("app.api.agents.WecomService", return_value=mock_wecom): - with patch("app.api.agents._get_redis", return_value=mock_redis): - transport = ASGITransport(app=app) - async with AsyncClient(transport=transport, base_url="http://test") as ac: - yield ac + # 2026-06-22 修复: 必须 patch "app.services.wecom_service.WecomService" + # 而不是 "app.api.h5.WecomService" — 因为 dep_wecom_service() 工厂函数 + # 在 app.services.wecom_service 模块内部 import WecomService, + # h5.py/agents.py 模块本身没 import WecomService,patch 它不生效 + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): + # 兼容历史: 部分代码可能仍然直接 import WecomService + with patch("app.api.conversations.WecomService", return_value=mock_wecom): + with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.api.agents.WecomService", return_value=mock_wecom): + with patch("app.api.agents._get_redis", return_value=mock_redis): + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as ac: + yield ac app.dependency_overrides.clear() diff --git a/backend/tests/test_h5_oauth.py b/backend/tests/test_h5_oauth.py index 4b6bfd1..0537435 100644 --- a/backend/tests/test_h5_oauth.py +++ b/backend/tests/test_h5_oauth.py @@ -46,11 +46,25 @@ async def h5_client(db_session: AsyncSession, mock_redis: MockRedis) -> AsyncCli with patch("app.api.h5._get_redis", return_value=mock_redis, create=True): with patch("redis.asyncio.from_url", return_value=mock_redis): - transport = ASGITransport(app=app) - # base_url 用 127.0.0.1,让 h5._require_wework_ua 跳过 UA 检测 - # 原因:生产环境要求企微 UA,测试环境是 httpx 客户端没企微 UA - async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as ac: - yield ac + # 2026-06-22 修复: h5 OAuth2 调 dep_wecom_service() 工厂, + # 必须 patch "app.services.wecom_service.WecomService" 而非 "app.services.wecom_service.WecomService" + with patch("app.services.wecom_service.WecomService") as MockWecom: + mock_wecom_instance = MockWecom.return_value + mock_wecom_instance.get_oauth_user_info = AsyncMock(return_value={ + "userid": "h5_oauth_test_user", + "user_ticket": "", + }) + mock_wecom_instance.get_user_info = AsyncMock(return_value={ + "name": "H5测试员工", + "department": [1, 2], + "position": "员工", + "avatar": "", + }) + transport = ASGITransport(app=app) + # base_url 用 127.0.0.1,让 h5._require_wework_ua 跳过 UA 检测 + # 原因:生产环境要求企微 UA,测试环境是 httpx 客户端没企微 UA + async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as ac: + yield ac app.dependency_overrides.clear() @@ -179,7 +193,7 @@ class TestOAuthCallback: }) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.post( "/h5/oauth/callback", json={"code": "valid_auth_code"}, @@ -209,7 +223,7 @@ class TestOAuthCallback: }) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.post( "/h5/oauth/callback", json={"code": "valid_auth_code"}, @@ -236,7 +250,7 @@ class TestOAuthCallback: }) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.post( "/h5/oauth/callback", json={"code": "valid_auth_code"}, @@ -257,7 +271,7 @@ class TestOAuthCallback: mock_wecom.get_oauth_user_info = AsyncMock(return_value={"userid": "", "user_ticket": ""}) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.post( "/h5/oauth/callback", json={"code": "bad_code"}, @@ -273,7 +287,7 @@ class TestOAuthCallback: mock_wecom.get_oauth_user_info = AsyncMock(side_effect=Exception("企微API不可用")) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.post( "/h5/oauth/callback", json={"code": "will_fail"}, @@ -290,7 +304,7 @@ class TestOAuthCallback: mock_wecom.get_user_info = AsyncMock(side_effect=Exception("通讯录API失败")) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.post( "/h5/oauth/callback", json={"code": "valid_code"}, @@ -521,7 +535,7 @@ class TestGetCurrentEmployeeInfo: }) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.get( "/h5/me", headers={"Authorization": "Bearer nocache_me_token"}, @@ -652,7 +666,7 @@ class TestErrorHandling: mock_redis.setex = broken_setex - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.post( "/h5/oauth/callback", json={"code": "valid_code"}, @@ -677,7 +691,7 @@ class TestErrorHandling: ) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.post( "/h5/oauth/callback", json={"code": "timeout_code"}, @@ -698,7 +712,7 @@ class TestErrorHandling: ) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.get( "/h5/me", headers={"Authorization": "Bearer wecom_fail_token"}, @@ -729,7 +743,7 @@ class TestTokenTTLAndFormat: }) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.post( "/h5/oauth/callback", json={"code": "ttl_test_code"}, @@ -755,7 +769,7 @@ class TestTokenTTLAndFormat: }) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.post( "/h5/oauth/callback", json={"code": "info_ttl_code"}, @@ -778,7 +792,7 @@ class TestTokenTTLAndFormat: }) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.post( "/h5/oauth/callback", json={"code": "fmt_test_code"}, @@ -827,7 +841,7 @@ class TestSchemaValidation: mock_wecom.get_user_info = AsyncMock(return_value={"name": "", "department": [], "position": "", "avatar": ""}) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): response = await h5_client.post( "/h5/oauth/callback", json={"code": "valid_code_here"}, @@ -866,7 +880,7 @@ class TestOAuth2EndToEnd: }) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): callback_response = await h5_client.post( "/h5/oauth/callback", json={"code": "e2e_auth_code"}, @@ -905,7 +919,7 @@ class TestOAuth2EndToEnd: }) mock_wecom.close = AsyncMock() - with patch("app.api.h5.WecomService", return_value=mock_wecom): + with patch("app.services.wecom_service.WecomService", return_value=mock_wecom): callback_response = await h5_client.post( "/h5/oauth/callback", json={"code": "cached_flow_code"}, @@ -914,7 +928,7 @@ class TestOAuth2EndToEnd: token = callback_response.json()["data"]["token"] # Step 2: 第一次访问 /me(应从缓存读取,不再调用 WecomService) - with patch("app.api.h5.WecomService") as MockWecomClass: + with patch("app.services.wecom_service.WecomService") as MockWecomClass: me_response = await h5_client.get( "/h5/me", headers={"Authorization": f"Bearer {token}"}, diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..97da7c8 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,30 @@ +site_name: WeCom IT 智能服务台 +site_description: 企微 IT 智能服务台项目文档 +site_author: Simon & Claude +docs_dir: docs +site_dir: site +theme: + name: material + language: zh + features: + - navigation.instant + - navigation.tabs + - search.suggest +nav: + - 首页: 01-项目总览与部署手册.md + - 架构: + - 总览: ARCHITECTURE.md + - 后台管理: ARCHITECTURE-admin.md + - 部署: + - 快速部署: DEPLOY-QUICK-v0.7.0.md + - 登录迁移: DEPLOY-LOGIN-MIGRATION-v0.7.0.md + - NAS: NAS部署指南.md + - 安全: + - OTP 二次验证: OTP二次验证实现.md + - 部署修复记录: IT服务台部署修复记录-2026-06-13.md + - E2E 验收: E2E-CHECKLIST-v0.7.0.md +markdown_extensions: + - admonition + - codehilite + - toc: + permalink: true