diff --git a/docs/plans/2026-06-24-001-test-calendar-e2e-plan.md b/docs/plans/2026-06-24-001-test-calendar-e2e-plan.md new file mode 100644 index 0000000..d2d943d --- /dev/null +++ b/docs/plans/2026-06-24-001-test-calendar-e2e-plan.md @@ -0,0 +1,109 @@ +--- +title: Calendar Feature Test Plan +status: active +date: 2026-06-24 +type: test +branch: test/calendar-e2e +--- + +## Summary + +针对已合并到 main 的日历与日程功能(U1-U12 + 代码走查修复),制定分层测试计划:单元测试基线验证 → 集成测试补全 → Playwright E2E 测试。重点验证端到端用户流程。 + +## Current State + +- **单元测试**: 104 个已存在(`tests/unit/calendar/` 11 文件 + `tests/unit/tools/test_calendar_tool.py`),覆盖 db/service/routes/extraction/recurrence/scheduler/reminders/sync/tool。 +- **集成测试**: 无日历相关集成测试。 +- **E2E**: Playwright 已配置(`src/agentkit/server/frontend/playwright.config.ts`),自动启动后端(8000)+前端(5173),有 login/chat/terminal 三个 spec。无日历 spec。 +- **关键缺陷**: 日历路由未接入 `src/agentkit/server/app.py` — 全量服务器启动时 `/api/v1/calendar/*` 返回 404。必须先修复才能跑 E2E。 + +## Test Layers + +### Layer 1: 单元测试基线 (Verify Existing) + +**目标**: 确认 104 个已有测试在新分支上全部通过。 + +```bash +python3 -m pytest tests/unit/calendar/ tests/unit/tools/test_calendar_tool.py -x -q +``` + +**验收**: 104 passed, 0 failed。 + +### Layer 2: 路由接入修复 (Critical Fix) + +**目标**: 将日历路由注册到 `create_app()`,使全量服务器可访问 `/api/v1/calendar/*`。 + +**改动**: +- `src/agentkit/server/app.py`: 导入 `calendar` 路由模块,`app.include_router(calendar.router, prefix="/api/v1")` +- 在 lifespan 中初始化 `CalendarService` 并挂到 `app.state.calendar_service` +- 在 lifespan 中启动/停止 `ReminderScheduler` + +**验收**: `agentkit serve` 启动后 `GET /api/v1/calendar/events` 返回 401(未认证)而非 404。 + +### Layer 3: 集成测试 (API via TestClient) + +**目标**: 通过 FastAPI TestClient 测试完整 API 流程(含认证、DB、业务逻辑),不依赖 Docker。 + +**新增文件**: `tests/unit/calendar/test_integration_flows.py` + +**测试用例**: +1. 完整事件生命周期: 创建 → 查询 → 更新 → 删除 +2. 循环事件: 创建 RRULE 事件 → 列表返回展开后的多次出现 +3. 标签管理: 创建标签 → 关联事件 → 按标签过滤 +4. 事件类型: 创建类型 → 创建带类型的事件 → 类型默认提醒规则克隆 +5. 邀请流程: 创建事件 → 发送邀请 → 接受/拒绝邀请 → 验证邀请列表 +6. 提醒规则: 创建带提醒规则的事件 → 验证规则持久化 +7. 授权隔离: 用户 A 的事件用户 B 不可见/不可改 +8. ICS 导入导出: 导入 .ics → 导出 → 验证往返一致性 + +### Layer 4: E2E 测试 (Playwright) + +**目标**: 通过浏览器模拟真实用户操作,验证前端 UI + 后端 API 完整链路。 + +**新增文件**: `src/agentkit/server/frontend/e2e/calendar.spec.ts` + +**前置条件**: Layer 2 修复完成(路由已接入)。 + +**测试用例**: + +| # | 场景 | 步骤 | +|---|------|------| +| E1 | 日历面板加载 | 登录 → 打开 Agent 页面 → 点击日历 tab → 验证摘要视图显示 | +| E2 | 创建事件 | 日历 tab → 打开抽屉 → 点击新建 → 填写标题/时间 → 保存 → 验证事件出现在列表和网格 | +| E3 | 三视图切换 | 抽屉内 → 切换 月/卡片/列表 视图 → 验证各视图正确渲染 | +| E4 | 编辑事件 | 点击已有事件 → 修改标题 → 保存 → 验证更新生效 | +| E5 | 删除事件 | 选中事件 → 删除 → 验证从列表消失 | +| E6 | 标签管理 | 创建标签 → 给事件打标签 → 按标签过滤 | +| E7 | 循环事件展示 | 创建每周循环事件 → 切换到月视图 → 验证多次出现 | +| E8 | 邀请管理 | 创建事件 → 添加邀请 → 验证邀请列表显示 | + +**实现策略**: +- 复用 `e2e/helpers.ts` 的 `loginAndHydrate(page)` 登录 +- 使用 Playwright `data-testid` 选择器(需在前端组件添加 testid) +- 每个测试独立创建事件,避免相互依赖 +- 使用 `waitForResponse` 等待 API 响应 + +### Layer 5: 前端组件测试 (Vitest, 可选) + +**目标**: 验证 Pinia store 和 API client 逻辑。 + +**新增文件**: `src/agentkit/server/frontend/tests/unit/stores/calendar.test.ts` + +**注**: `@vue/test-utils` 未安装,组件挂载测试需新增依赖。按 ponytail 原则,优先 E2E 覆盖,组件测试列为可选。 + +## Execution Order + +1. Layer 1 — 运行现有单元测试(基线) +2. Layer 2 — 修复路由接入 +3. Layer 3 — 编写集成测试 +4. Layer 4 — 编写并运行 E2E 测试 +5. Layer 5 — (可选) 前端组件测试 + +## Risks + +| Risk | Mitigation | +|------|------------| +| E2E 需要真实后端+前端启动,耗时较长 | Playwright config 已配置 webServers 自动启停 | +| FullCalendar 组件渲染复杂,选择器难定位 | 使用 `data-testid` + `getByTestId` | +| 循环事件展开在 E2E 中时区敏感 | 使用 UTC 固定时间,避免相对时间 | +| 路由接入可能影响现有服务器启动 | 先跑全量单元测试确认无回归 | diff --git a/src/agentkit/server/app.py b/src/agentkit/server/app.py index b64e1bf..fbe3a63 100644 --- a/src/agentkit/server/app.py +++ b/src/agentkit/server/app.py @@ -50,6 +50,7 @@ from agentkit.server.routes import ( auth as auth_routes, documents, admin as admin_routes_module, + calendar as calendar_routes, ) from agentkit.server.auth.jwt_utils import get_jwt_secret from agentkit.server.auth.middleware import AuthMiddleware @@ -402,6 +403,30 @@ async def lifespan(app: FastAPI): app.state.expert_template_registry = ExpertTemplateRegistry() + # Calendar subsystem (U1-U12): init DB, service, reminder scheduler, agent tool. + calendar_scheduler = None + try: + from agentkit.calendar.db import init_calendar_db + from agentkit.calendar.scheduler import ReminderScheduler + from agentkit.calendar.service import CalendarService + from agentkit.tools.calendar_tool import CalendarTool + + await init_calendar_db() + cal_service = CalendarService() + app.state.calendar_service = cal_service + calendar_scheduler = ReminderScheduler() + await calendar_scheduler.start() + app.state.calendar_scheduler = calendar_scheduler + # Register CalendarTool so ReAct agents can create/query events. + try: + app.state.tool_registry.register(CalendarTool(service=cal_service)) + logger.info("CalendarTool registered for ReAct integration") + except Exception: + pass # Already registered + logger.info("Calendar subsystem initialized (service + reminder scheduler)") + except Exception: + logger.exception("Failed to initialize calendar subsystem — calendar API unavailable") + yield # Shutdown @@ -429,6 +454,11 @@ async def lifespan(app: FastAPI): await task_store.stop_cleanup() + # Stop calendar reminder scheduler + cal_scheduler = getattr(app.state, "calendar_scheduler", None) + if cal_scheduler is not None: + await cal_scheduler.stop() + def _on_config_change(app: FastAPI, config: ServerConfig) -> None: """Handle config change by reloading affected components. @@ -952,6 +982,7 @@ def create_app( app.include_router(auth_routes.admin_router, prefix="/api/v1") app.include_router(admin_routes_module.admin_router, prefix="/api/v1") app.include_router(documents.router, prefix="/api/v1") + app.include_router(calendar_routes.router, prefix="/api/v1") # Serve GUI when in GUI mode gui_mode = os.environ.get("AGENTKIT_GUI_MODE") diff --git a/src/agentkit/server/routes/calendar.py b/src/agentkit/server/routes/calendar.py index 32b0724..2a0d7b1 100644 --- a/src/agentkit/server/routes/calendar.py +++ b/src/agentkit/server/routes/calendar.py @@ -29,7 +29,6 @@ from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, Upl from fastapi.responses import Response from pydantic import BaseModel, Field -from agentkit.calendar.sync.ics_provider import ICSProvider from agentkit.server.auth.dependencies import require_authenticated logger = logging.getLogger(__name__) @@ -424,6 +423,8 @@ async def import_ics( """Import events from an uploaded .ics file.""" service = _get_calendar_service(request) content = await file.read() + from agentkit.calendar.sync.ics_provider import ICSProvider # lazy: avoid circular import + provider = ICSProvider(service) try: result = await provider.import_ics(content, user["user_id"]) @@ -442,6 +443,8 @@ async def export_ics( """Export the current user's events to a downloadable .ics file.""" service = _get_calendar_service(request) events = await service.list_events(user_id=user["user_id"], start=start, end=end) + from agentkit.calendar.sync.ics_provider import ICSProvider # lazy: avoid circular import + provider = ICSProvider(service) ics_bytes = provider.export_ics(events) return Response(