补充AGC埋点中间件集成测试

新增build_router到tracking outbox的成功链路回归

断言AGC标记、实际路由和用户主体归属

同步Issue225实施与验收文档
This commit is contained in:
2026-09-03 02:41:27 +00:00
parent d1b2aea4a4
commit d39ae3144a
4 changed files with 64 additions and 0 deletions
@@ -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. 实施顺序
@@ -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 或失败回退实现。
@@ -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 语义。
+58
View File
@@ -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::<Value>(line).expect("outbox line should be JSON"))
.collect::<Vec<_>>();
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::<Value>(
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();