完善客户端发布渠道并接入模板库灰度
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m36s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m37s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m42s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m51s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m55s
Project CI / AI game creator shell Rust crates (push) Successful in 4m15s
Project CI / Repository checks (push) Successful in 4m45s
Project CI / Frontend tests (push) Successful in 7m47s
Project CI / Native shell tests (push) Successful in 9m33s
Project CI / Backend tests (push) Successful in 10m9s
Project CI / AI game creator shell web tests (push) Successful in 4m38s

区分发布渠道与系统,支持 dev、release 和自定义渠道
允许网站通过服务端配置选择客户端下载检测渠道
接入模板库灰度权限并阻断退出和切号后的异步操作
补齐发布、下载、灰度与会话竞态测试及当前规范
This commit is contained in:
kdletters
2026-09-20 12:12:49 +08:00
parent fc0ce4ee5f
commit 623e007fae
30 changed files with 1554 additions and 323 deletions
@@ -1,5 +1,6 @@
use axum::{
Json, Router,
extract::State,
http::{HeaderValue, StatusCode, header::CACHE_CONTROL},
response::{IntoResponse, Response},
routing::get,
@@ -18,8 +19,8 @@ pub fn router(_state: AppState) -> Router<AppState> {
Router::new().route("/api/client-downloads", get(client_downloads))
}
async fn client_downloads() -> Response {
download_response(fetch_latest_client_downloads().await)
async fn client_downloads(State(state): State<AppState>) -> Response {
download_response(fetch_latest_client_downloads(&state.config.client_download_channel).await)
}
fn download_response(result: Result<ClientDownloads, ClientDownloadError>) -> Response {
@@ -69,6 +70,29 @@ mod tests {
use platform_oss::client_downloads::ClientDownload as SourceDownload;
use serde_json::{Value, json};
#[tokio::test]
async fn client_downloads_uses_server_config_and_does_not_accept_query_overrides() {
use axum::{body::Body, http::Request};
use tower::ServiceExt;
let state = AppState::new(crate::config::AppConfig {
client_download_channel: "dev-win".to_string(),
..Default::default()
})
.unwrap();
let app = router(state.clone()).with_state(state);
let response = app
.oneshot(
Request::builder()
.uri("/api/client-downloads?channel=dev&url=https://example.com")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
assert_eq!(response.headers()[CACHE_CONTROL], "no-store");
}
async fn read_success(result: ClientDownloads) -> Value {
let response = download_response(Ok(result));
assert_eq!(response.status(), StatusCode::OK);
@@ -151,6 +175,7 @@ mod tests {
#[tokio::test]
async fn upstream_failures_share_a_safe_retryable_uncached_error() {
for error in [
ClientDownloadError::InvalidChannel,
ClientDownloadError::Transport,
ClientDownloadError::UpstreamStatus,
ClientDownloadError::ManifestTooLarge,