diff --git a/local-docs/【实施方案】Issue225-AGC主站请求标记埋点统计-2026-09-02.md b/local-docs/【实施方案】Issue225-AGC主站请求标记埋点统计-2026-09-02.md index 5339f6f62..45311511f 100644 --- a/local-docs/【实施方案】Issue225-AGC主站请求标记埋点统计-2026-09-02.md +++ b/local-docs/【实施方案】Issue225-AGC主站请求标记埋点统计-2026-09-02.md @@ -279,6 +279,7 @@ TrackingEventDraft.metadata - 同一批新增 route:缺失、非法或未知 marker 时不记录,也不拒绝业务请求。 - 既有 route spec 和已有详细资产事件保持原有全客户端记录语义。 - 4xx/5xx 即使带有效 marker 也不新增成功 route event。 +- `build_router` middleware 集成测试验证 marker、账号认证主体、成功 route tracking 和隔离 outbox 可以串联,且 metadata 保留 `client`、实际 route 和 user/owner。 ### 9.4 Route 覆盖 @@ -301,6 +302,7 @@ TrackingEventDraft.metadata - 2xx 成功响应写入 route tracking。 - 4xx/5xx 不因为 marker 自动新增成功 route event。 - 认证失败不伪造 ExternalApiPrincipal 或用户归属。 +- 轻量 middleware 成功链路测试通过;不依赖真实 SpacetimeDB,不纳入完整环境型 E2E。 ## 10. 实施顺序 diff --git a/local-docs/【实施计划】Issue225-AGC主站请求标记埋点分阶段验收-2026-09-02.md b/local-docs/【实施计划】Issue225-AGC主站请求标记埋点分阶段验收-2026-09-02.md index 431a4fe4c..2acdebf32 100644 --- a/local-docs/【实施计划】Issue225-AGC主站请求标记埋点分阶段验收-2026-09-02.md +++ b/local-docs/【实施计划】Issue225-AGC主站请求标记埋点分阶段验收-2026-09-02.md @@ -273,6 +273,7 @@ cargo test --locked --manifest-path server-rs/Cargo.toml -p spacetime-client tra - External v1 route resolver 覆盖测试。 - discovery/MCP 排除测试。 - 2xx 记录、4xx/5xx 不新增成功 route 事件测试。 +- `build_router` 轻量 middleware 成功链路测试:真实 HTTP request 经认证、tracking middleware 后写入隔离 outbox,并断言 `client`、route 和主体归属。 - outbox/SpacetimeDB input 和后台 tracking readback 测试。 CI 已覆盖且与本改动无直接关系的全量测试可交给 CI;本阶段仍必须运行能直接证明本 Issue 契约的 targeted tests。 @@ -295,6 +296,7 @@ CI 已覆盖且与本改动无直接关系的全量测试可交给 CI;本阶 - `tracking.rs` 抽出 `should_record_route_tracking` 判定,明确新增 AGC-only route 只有在带有效 marker 的 2xx 响应下才进入统一成功 route tracking;既有全客户端 route 保持原语义,4xx/5xx 和上传确认等复用事件均不新增重复成功事件。 - 新增 2xx、4xx/5xx 状态矩阵测试,覆盖 `OK / CREATED / ACCEPTED / NO_CONTENT` 与认证、权限、客户端、服务端失败状态。 - 新增 AGC-only route 正反向门禁测试:有效 marker 记录,缺失或无效 marker 不记录;既有 route 和手工资产事件策略保持不变。 +- 新增 `app::tests::agc_marker_flows_through_auth_tracking_middleware_to_outbox`,不启动真实 SpacetimeDB,仅验证 middleware 到 outbox 的最小闭环。 - 新增 `app.rs` 路由回归:带有效 `X-Genarrative-Client: agc` 的未认证业务请求仍返回 `401`,marker 不绕过鉴权,也不改变失败状态。 - 新增 route metadata 安全边界测试,确认统一 route metadata 只包含既有 route/method/status/operation 和可选 `client`/资产字段,不出现 authorization、token、API Key、Cookie、签名 URL 或 request body 字段。 - 阶段 4 的账号态、External API Key、outbox、后台 readback 和 mapper 测试全部复跑;SpacetimeDB 既有 event-id 幂等回归也通过。未修改鉴权、请求体、异步轮询、event id、daily stat 或失败回退实现。 diff --git a/local-docs/【阶段验收】Issue225阶段6最终门禁与交接收口-2026-09-02.md b/local-docs/【阶段验收】Issue225阶段6最终门禁与交接收口-2026-09-02.md index 5521e9da7..a36a13acd 100644 --- a/local-docs/【阶段验收】Issue225阶段6最终门禁与交接收口-2026-09-02.md +++ b/local-docs/【阶段验收】Issue225阶段6最终门禁与交接收口-2026-09-02.md @@ -75,6 +75,8 @@ X-Genarrative-Client: agc 本期新增的 AGC route spec 只有在请求带有效 marker 时才记录;既有 route spec 与手工资产事件沿用原有全客户端记录策略。 +阶段 6 后续补充了一条轻量 middleware 成功链路回归:真实 `build_router` 请求经过账号认证和 tracking middleware 后写入隔离 outbox,并验证 `client`、实际 route、user/owner 归属;该测试不启动真实 SpacetimeDB,不改变完整环境型 E2E 仍作为发布前 smoke 的边界。 + ### 3.2 主体归属 - 登录账号态:使用已验证 access token 的真实用户,保留既有 `user_id`、`owner_user_id` 和 scope 语义。 diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index e78583295..f8ca4f160 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -478,6 +478,64 @@ mod tests { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } + #[tokio::test] + async fn agc_marker_flows_through_auth_tracking_middleware_to_outbox() { + let outbox_dir = std::env::temp_dir().join(format!( + "genarrative-api-tracking-middleware-{}", + OffsetDateTime::now_utc().unix_timestamp_nanos() + )); + let _ = std::fs::remove_dir_all(&outbox_dir); + let config = AppConfig { + tracking_outbox_dir: outbox_dir.clone(), + tracking_outbox_batch_size: 100, + ..AppConfig::default() + }; + let state = AppState::new(config).expect("state should build"); + let user = seed_phone_user_with_password(&state, "13800138001", TEST_PASSWORD).await; + let token = sign_test_user_token(&state, &user, "sess_agc_tracking_middleware"); + let app = build_router(state); + + let response = app + .oneshot( + Request::builder() + .method("GET") + .uri("/api/auth/me") + .header("authorization", format!("Bearer {token}")) + .header("x-genarrative-client", "agc") + .body(Body::empty()) + .expect("request should build"), + ) + .await + .expect("request should succeed"); + + assert_eq!(response.status(), StatusCode::OK); + let outbox = tokio::fs::read_to_string(outbox_dir.join("active.ndjson")) + .await + .expect("tracking middleware should enqueue an outbox event"); + let records = outbox + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| serde_json::from_str::(line).expect("outbox line should be JSON")) + .collect::>(); + assert_eq!(records.len(), 1); + + let event = &records[0]["event"]; + assert_eq!(event["event_key"], "auth_me_view"); + assert_eq!(event["user_id"], user.id); + assert_eq!(event["owner_user_id"], user.id); + let metadata = serde_json::from_str::( + event["metadata_json"] + .as_str() + .expect("tracking metadata should be serialized as a string"), + ) + .expect("tracking metadata should be valid JSON"); + assert_eq!(metadata["client"], "agc"); + assert_eq!(metadata["route"], "/api/auth/me"); + assert_eq!(metadata["status"], 200); + + let _ = std::fs::remove_dir_all(outbox_dir); + } + #[cfg(any())] fn build_internal_creative_agent_app() -> Router { let mut config = AppConfig::default();