feat(status): report per-instance backend label so probes can identify the origin

`/api/status` now echoes a `backend` field alongside `version`. The baked
`BUILD_VERSION` is identical across every instance built from the same image bath,
so it answers "did this deploy land?" but not "which origin answered?" when a
probe goes through the Cloudflare Worker load balancer (`ARCH-LB-WORKER`) — and the
Worker's `x-lb-backend` header is unreliable (it is silently dropped when the
response headers are immutable, e.g. cross-origin without CORS). Having the backend
self-report in the body removes that dependency entirely.

The value is a per-instance observability label read once at startup from
`BACKEND_ID` (falling back to `HOSTNAME`, which container runtimes set their
selves). It is not a connection secret and is never persisted (`C-REDIS-ONLY-STATE`).
`null` means neither env var was set — two such instances are indistinguishable by
this field by design.

Wiring keeps handlers off the environment (matching the `debug_surface_requested`
rationale: env is process-global and reading it inside a handler races tests):
`main` reads it once and threads `backend_id` through the router builders into
`AppState::backend_id`.

Also replaces the drift-prone `src/main.rs:<line>` anchors in design.md /
design.zh-CN.md with symbol references (`std::env::var` key names,
`LlmClient::with_runtime`), per the symbol-not-line rule; the insert had already
shifted them.

Tests: `setup_status_reports_backend_label` asserts the label round-trips into the
body. GATE-P0 105 passed / 0 failed / 4 ignored; GATE-DOCS 6/6 clean.
This commit is contained in:
anonsaber 2026-10-14 02:28:06 +08:00
parent 6ae8978836
commit 88f0923e6c
8 changed files with 108 additions and 32 deletions

View File

@ -10,5 +10,10 @@ CONFIG_ENCRYPTION_KEY=REPLACE_WITH_64_HEX_CHAR_RANDOM_KEY
# PORT is an optional platform override (default: 8080). There is no RUN_MODE: the
# legacy env parser was removed, and webhook plus reconcile traffic share one router.
# BACKEND_ID is an optional per-instance observability label echoed as `backend` in
# /api/status, so a probe through the load balancer can tell which origin answered
# (the baked build `version` is identical across instances of one image). Falls back
# to HOSTNAME. Not a secret, not a connection setting, never persisted.
BACKEND_ID=
# Business secrets (Telegram/JMAP/LLM/auth) must be supplied once via HTTPS /api/bootstrap;
# they are then persisted in Redis and are never echoed by the API.

View File

@ -365,8 +365,8 @@ Testing: **No integrated test directory**, and no stand-alone util tool module.
> Use new configuration, keep old instance on failure. This migration replaces the following stage 0 environment variable list. The stage 0 list is only for historical purposes.
> Compatibility instructions.
> **Actual implementation** (`ARCH-CONFIG-ENV`): process startup read-only **2** process-level credentials plus listening port (`src/main.rs:45` / `:54` / `:63`,
> Add optional `DEBUG_TOKEN` in `:107`), directly `std::env::var`, **do not use figment, do not use TOML/configuration file**.
> **Actual implementation** (`ARCH-CONFIG-ENV`): process startup read-only **2** process-level credentials plus listening port (all via `std::env::var` in `main`, keyed by `REDIS_URL` / `CONFIG_ENCRYPTION_KEY` / `PORT`,
> plus optional `DEBUG_TOKEN` and the `/api/status` label `BACKEND_ID`), **do not use figment, do not use TOML/configuration file**.
> **Business configuration is no longer read from environment variables**: The fields in the following table are all loaded by the `config` layer using serde deserialization through the Redis business configuration.
> And do fail-closed verification through `validate_business_wire` / `validate_nonblank`.
> Historically existing `Config::from_env()` full env parsing path **removed** (see `docs/retired.md` for registration),
@ -420,7 +420,7 @@ Testing: **No integrated test directory**, and no stand-alone util tool module.
### 7.3.1 Configuration Management API
- `GET /` provides a SPA embedded in the Rust binary; `/assets/config.js` and `/assets/styles.css` provide page resources. The service does not read or write local files while running (`C-NO-LOCAL-WRITE`).
- `GET /api/status` publicly returns `{ "ready": boolean, "mode": "configured" | "configuration-setup", "missing": string[], "version": string }`, which only lists the missing environment variable names. When `REDIS_URL` or `CONFIG_ENCRYPTION_KEY` is missing, SPA only displays the configuration boot status and missing variables; the management session authorization area is displayed only after the service status is confirmed to be ready=true. `version` is the `BUILD_VERSION` (`<git-sha or nogit>+<UTC build time>`) baked by `build.rs` during compilation. The SPA no longer renders it, so the API is the only way to confirm a deployment landed.
- `GET /api/status` publicly returns `{ "ready": boolean, "mode": "configured" | "configuration-setup", "missing": string[], "version": string, "backend": string|null }`, which only lists the missing environment variable names. When `REDIS_URL` or `CONFIG_ENCRYPTION_KEY` is missing, SPA only displays the configuration boot status and missing variables; the management session authorization area is displayed only after the service status is confirmed to be ready=true. `version` is the `BUILD_VERSION` (`<git-sha or nogit>+<UTC build time>`) baked by `build.rs` during compilation. The SPA no longer renders it, so the API is the only way to confirm a deployment landed. `backend` is the per-instance label read once at startup from `BACKEND_ID` (falling back to `HOSTNAME`); `version` is identical across instances built from the same image, so `backend` is what lets a probe through the load balancer tell which origin answered. `null` means no label was configured (both env vars unset); two such instances are indistinguishable by this field by design.
- `POST /api/admin/session` accepts `Authorization: Bearer <CONFIG_ENCRYPTION_KEY>` and returns `{ "session": "<opaque>", "expires_in": 1800 }`; the admin session only saves the digest in Redis and expires after 1,800 seconds. `POST /api/admin/session/revoke` revokes the current session and returns `204` successfully.
- The admin page only saves the session in JavaScript memory. Request settings `credentials: omit`, `cache: no-store`, without using cookies, localStorage or sessionStorage. The admin API accepts a valid admin session; the compatibility path also accepts `WORKER_TOKEN`. Worker transparently transmits the authentication header (`SAF-LB-PASSTHRU`) as it is.
- `GET /api/config` and `PUT /api/config` only read and write non-sensitive running parameters:
@ -746,7 +746,7 @@ The LLM capability only exists in the `src/ai.rs` file; **no** configuration/fal
| Variable | Type | Default | Purpose |
|---|---|---|---|
| `LLM_ENABLED` | bool | `false` | Master switch; off by default, no verification when not enabled `LLM_API_KEY`/`LLM_BASE_URL`/`LLM_MODEL` |
| `LLM_ALLOW_NET` | bool | `false` | Runtime outbound permission; `LlmClient` is constructed only if `LLM_ENABLED && LLM_ALLOW_NET` are both true, otherwise the `llm` field is `None` (`src/main.rs:135-141`) |
| `LLM_ALLOW_NET` | bool | `false` | Runtime outbound permission; `LlmClient` is constructed only if `LLM_ENABLED && LLM_ALLOW_NET` are both true, otherwise the `llm` field is `None` (`LlmClient::with_runtime` in `src/main.rs`) |
| `LLM_API_KEY` | string | Can be omitted | Bearer token; required only when `LLM_ENABLED=true` |
| `LLM_BASE_URL` | URL | Can be omitted | Required only when enabled, and must be `https`, otherwise `AiError::InvalidEndpoint` |
| `LLM_MODEL` | string | Can be omitted | Required only when enabled; transparently passed to `model` in `/chat/completions` |

View File

@ -365,8 +365,8 @@ message-weave/
> 使用新配置,失败保留旧实例。该迁移替代下述阶段0环境变量清单,阶段0列表仅作为历史
> 兼容说明。
> **实际实现**(`ARCH-CONFIG-ENV`):进程启动只读 **2 个**进程级凭据加监听端口(`src/main.rs:45` / `:54` / `:63`,
> 另加可选 `DEBUG_TOKEN` 于 `:107`),直接 `std::env::var`,**不使用 figment、不使用 TOML/配置文件**。
> **实际实现**(`ARCH-CONFIG-ENV`):进程启动只读 **2 个**进程级凭据加监听端口(均经 `main` 里的 `std::env::var`,键名 `REDIS_URL` / `CONFIG_ENCRYPTION_KEY` / `PORT`,
> 另加可选 `DEBUG_TOKEN` 与 `/api/status` 标签 `BACKEND_ID`),**不使用 figment、不使用 TOML/配置文件**。
> **业务配置不再从环境变量读取**:下表字段全部经 Redis 业务配置由 `config` 层以 serde 反序列化装载,
> 并经 `validate_business_wire` / `validate_nonblank` 做 fail-closed 校验。
> 历史上存在的 `Config::from_env()` 完整 env 解析路径**已删除**(登记见 `docs/retired.md`),
@ -420,7 +420,7 @@ message-weave/
### 7.3.1 配置管理 API
- `GET /` 提供嵌入 Rust 二进制的 SPA;`/assets/config.js` 与 `/assets/styles.css` 提供页面资源。服务不在运行时读取或写入本地文件(`C-NO-LOCAL-WRITE`)。
- `GET /api/status` 公开返回 `{ "ready": boolean, "mode": "configured" | "configuration-setup", "missing": string[], "version": string }`,只列缺少的环境变量名称。缺少 `REDIS_URL` 或 `CONFIG_ENCRYPTION_KEY` 时,SPA 只显示配置引导状态与缺失变量;服务状态确认 ready=true 后才显示管理会话授权区。`version` 是 `build.rs` 在编译期烘焙的 `BUILD_VERSION`(`<git-sha 或 nogit>+<UTC 构建时间>`)。SPA 已不再渲染它,确认部署落地只能查这个接口。
- `GET /api/status` 公开返回 `{ "ready": boolean, "mode": "configured" | "configuration-setup", "missing": string[], "version": string, "backend": string|null }`,只列缺少的环境变量名称。缺少 `REDIS_URL` 或 `CONFIG_ENCRYPTION_KEY` 时,SPA 只显示配置引导状态与缺失变量;服务状态确认 ready=true 后才显示管理会话授权区。`version` 是 `build.rs` 在编译期烘焙的 `BUILD_VERSION`(`<git-sha 或 nogit>+<UTC 构建时间>`)。SPA 已不再渲染它,确认部署落地只能查这个接口。`backend` 是启动时一次性读取 `BACKEND_ID`(回退到 `HOSTNAME`)的每实例标签;同一镜像构建的实例 `version` 相同,所以 `backend` 才是让穿过负载均衡器的探针分辨命中的是哪个 origin 的字段。`null` 表示未配置标签(两个 env 都未设置);两个都不设的实例在该字段上不可区分,这是设计如此。
- `POST /api/admin/session` 接受 `Authorization: Bearer <CONFIG_ENCRYPTION_KEY>`,返回 `{ "session": "<opaque>", "expires_in": 1800 }`;Redis 中保存的是会话令牌的摘要,30 分钟后过期。`POST /api/admin/session/revoke` 可立即撤销会话,成功返回 `204`。
- 管理页面仅在 JavaScript 内存中保存 session。请求设置 `credentials: omit`、`cache: no-store`,不使用 Cookie、localStorage 或 sessionStorage。管理 API 接受有效 admin session;兼容路径也接受 `WORKER_TOKEN`。Worker 原样透传鉴权头(`SAF-LB-PASSTHRU`)。
- `GET /api/config` 与 `PUT /api/config` 只读取和写入非敏感运行参数:
@ -746,7 +746,7 @@ LLM 能力只存在于 `src/ai.rs` 一个文件;**没有**配置 / 回退 /
| 变量 | 类型 | 默认 | 用途 |
|---|---|---|---|
| `LLM_ENABLED` | bool | `false` | 总开关;默认关闭,未启用时不校验 `LLM_API_KEY`/`LLM_BASE_URL`/`LLM_MODEL` |
| `LLM_ALLOW_NET` | bool | `false` | 运行时出站许可;`LLM_ENABLED && LLM_ALLOW_NET` 同时为真才构造 `LlmClient`,否则 `llm` 字段为 `None`(`src/main.rs:135-141`) |
| `LLM_ALLOW_NET` | bool | `false` | 运行时出站许可;`LLM_ENABLED && LLM_ALLOW_NET` 同时为真才构造 `LlmClient`,否则 `llm` 字段为 `None`(`LlmClient::with_runtime` 于 `src/main.rs`) |
| `LLM_API_KEY` | string | 可省略 | Bearer token;仅 `LLM_ENABLED=true` 时必填 |
| `LLM_BASE_URL` | URL | 可省略 | 仅启用时必填,且必须 `https`,否则 `AiError::InvalidEndpoint` |
| `LLM_MODEL` | string | 可省略 | 仅启用时必填;透传给 `/chat/completions` 的 `model` |

View File

@ -275,12 +275,18 @@ Response conventions:
The JMAP probe deliberately authenticates against the *normalized origin*: probing the raw
session URL unauthenticated would report not-ready forever and make ingress stop routing.
- `GET /api/status` (**always 200**) returns
`{"ready": <bool>, "mode": "configured"|"configuration-setup", "missing": [...], "version": "<build-fingerprint>"}`.
`{"ready": <bool>, "mode": "configured"|"configuration-setup", "missing": [...], "version": "<build-fingerprint>", "backend": "<id>"|null}`.
`ready` is `false` and `missing` lists the absent required keys (`REDIS_URL`,
`CONFIG_ENCRYPTION_KEY`) when a variable is absent — the route itself never errors, so it is
safe to poll. `version` is the `BUILD_VERSION` string baked in by `build.rs`
(`<git-sha-or-nogit>+<UTC build time>`). The SPA no longer renders it — the API is now the
only way to confirm a deploy landed.
(`<git-sha-or-nogit>+<UTC build time>`) — identical across every instance built from the same
image, so it answers "did this deploy land?" but not "which instance answered?". `backend`
answers that: a per-instance label read once at startup from `BACKEND_ID` (falling back to
`HOSTNAME`, which container runtimes set), threaded into `AppState::backend_id` and echoed
here. It is an observability label, not a connection secret and not persisted. `null` means
neither env var was set; two such instances are indistinguishable by this field, so a probe
that needs to tell origins apart must set `BACKEND_ID` per instance at deploy time. The SPA
no longer renders `version` — the API is now the only way to confirm a deploy landed.
- `/debug/*` returns **503** in exactly one place: `POST /debug/notify` when the business
config is not loaded or there is no outbound client — `service_unavailable` with
`Retry-After: 30`. The three probe endpoints instead report failure

View File

@ -272,11 +272,10 @@ env var — 并且“DEBUG_TOKEN”非空**(“SAF-DEBUG-GATE”;门被读
JMAP 探针有意针对*规范化来源*进行身份验证:探测原始数据
未经身份验证的会话 URL 将永远报告未就绪并使入口停止路由。
- `GET /api/status`(**始终 200**)返回
`{“ready”:<bool>,“mode”:“configured”|“configuration-setup”,“missing”:[...],“version”:“<build-fingerprint>”}`。
“ready”为“false”,“missing”列出了缺少的必需密钥(“REDIS_URL”、
`CONFIG_ENCRYPTION_KEY`)当变量不存在时 - 路由本身永远不会出错,所以它是
可以安全进行民意调查。 `version` 是由 `build.rs` 烘焙的 `BUILD_VERSION` 字符串
(`<git-sha-or-nogit>+<UTC 构建时间>`)。SPA 已不再渲染它,确认部署落地只能查这个接口。
`{"ready": <bool>, "mode": "configured"|"configuration-setup", "missing": [...], "version": "<build-fingerprint>", "backend": "<id>"|null}`。
`ready` 为 `false`、`missing` 列出缺少的必需变量名(`REDIS_URL`、
`CONFIG_ENCRYPTION_KEY`)——路由本身永不报错,可安全轮询。`version` 是 `build.rs` 烘焙的 `BUILD_VERSION` 字符串
(`<git-sha 或 nogit>+<UTC 构建时间>`)——同一镜像构建的每个实例都相同,因此它回答「本次部署是否落地」,但不回答「命中的是哪个实例」。`backend` 回答后者:每实例标签,启动时一次性读取 `BACKEND_ID`(回退容器运行时设置的 `HOSTNAME`),穿入 `AppState::backend_id` 并在此回显。它是观测标签,不是连接密钥,也不持久化。`null` 表示两个 env 都未设置;两个都不设的实例在该字段上不可区分,所以需要分辨 origin 的探针必须在部署时为每个实例设置 `BACKEND_ID`。SPA 已不再渲染 `version`,确认部署落地只能查这个接口。
- `/debug/*` 在一个地方返回 **503**:业务执行时`POST /debug/notify`
配置未加载或没有出站客户端 — `service_unavailable`
`重试时间:30`。三个探测端点反而报告失败

View File

@ -313,6 +313,7 @@ mod tests {
state,
HashSet::new(),
Arc::new(NoopWorker),
None,
runtime_provider(OutboundConfig::default()),
SecretString::new(String::new()),
debug_token,

View File

@ -26,6 +26,23 @@ fn debug_surface_requested(debug_arg: bool, debug_enabled_var: Option<&str>) ->
)
}
/// Per-instance observability label for `/api/status` (see `AppState::backend_id`). Read once at
/// startup so request handlers never touch the environment. `BACKEND_ID` wins; otherwise
/// `HOSTNAME` (set by container runtimes) gives out-of-the-box distinction between instances.
/// `None` only when both are unset, so a probe honestly reports "not labeled" instead of a
/// misleading default — two instances that both skip it are indistinguishable by design until an
/// operator sets `BACKEND_ID`, which is a deploy-time concern, not a connection secret.
fn backend_id_from_env() -> Option<String> {
std::env::var("BACKEND_ID")
.ok()
.filter(|value| !value.is_empty())
.or_else(|| {
std::env::var("HOSTNAME")
.ok()
.filter(|value| !value.is_empty())
})
}
use crate::channel::telegram::TelegramClient;
use crate::domain::jmap::{client::JmapClientBackend, JmapService};
use config::{encryption_key_from_env, Config};
@ -50,6 +67,7 @@ async fn main() -> Result<(), error::BotError> {
.parse()
.map_err(|e| error::BotError::Config(format!("invalid PORT: {e}")))?;
let listener = tokio::net::TcpListener::bind(addr).await?;
let backend_id = backend_id_from_env();
let mut missing = Vec::new();
let redis_value = match std::env::var("REDIS_URL") {
Ok(value) if !value.trim().is_empty() => value,
@ -73,7 +91,7 @@ async fn main() -> Result<(), error::BotError> {
?missing,
"required bootstrap environment is incomplete; serving setup mode"
);
let app = router_configuration_setup(missing).merge(web::router());
let app = router_configuration_setup(missing, backend_id).merge(web::router());
axum::serve(listener, app).await?;
return Ok(());
}
@ -198,6 +216,7 @@ async fn main() -> Result<(), error::BotError> {
redis,
allowlist,
Arc::new(WorkerHandle::new(worker)),
backend_id,
runtime,
admin_token,
debug_token,

View File

@ -104,6 +104,11 @@ pub(crate) struct AppState {
/// `--debug` AND `DEBUG_TOKEN` was set. `debug_router` is mounted iff this is `Some`, so the
/// `/debug/*` surface is absent by default. `SAF-DEBUG-AUTH`: compared in constant time.
pub(crate) debug_token: Option<SecretString>,
/// Per-instance observability label echoed in `/api/status`, so a probe through the load
/// balancer can tell which origin answered — `version` alone is identical across instances
/// built from the same image. Read once at startup from `BACKEND_ID` (falls back to
/// `HOSTNAME`); not a connection secret, not persisted. `None` only when both are unset.
pub(crate) backend_id: Option<String>,
}
impl From<AuthSecrets> for AuthState {
@ -220,6 +225,7 @@ async fn setup_status(State(app): State<AppState>) -> Response {
"mode": if ready { "configured" } else { "configuration-setup" },
"missing": app.setup_missing.as_ref(),
"version": BUILD_VERSION,
"backend": &app.backend_id,
}))
.into_response()
}
@ -1924,6 +1930,7 @@ pub fn router_with_state<S: ReliableState + 'static>(
state,
allowlist,
Arc::new(NoopWorker),
None,
)
}
@ -1939,7 +1946,7 @@ pub fn router_with_state<S: ReliableState + 'static>(
/// 404 removes that ambiguity for clients, probes and the SPA alike. The SPA bootstrap card is
/// driven by `/api/status` alone (`web/config.js` reveals the admin-session card only when
/// `ready === true`), so nothing the status page renders is lost.
pub fn router_configuration_setup(missing: Vec<String>) -> Router {
pub fn router_configuration_setup(missing: Vec<String>, backend_id: Option<String>) -> Router {
let worker_handle = Arc::new(WorkerHandle::new(Arc::new(NoopWorker)));
let app_state = AppState {
auth: AuthState {
@ -1958,6 +1965,7 @@ pub fn router_configuration_setup(missing: Vec<String>) -> Router {
business_revision: Arc::new(RwLock::new(0)),
setup_missing: Arc::new(missing),
reload: Arc::new(ReloadCoordinator::new(worker_handle, None)),
backend_id,
};
Router::new()
.route("/api/status", get(setup_status))
@ -1972,6 +1980,7 @@ pub fn router_with_worker_state<S: ReliableState + 'static>(
state: S,
allowlist: HashSet<i64>,
worker_handler: Arc<dyn WorkerHandler>,
backend_id: Option<String>,
) -> Router {
router_with_worker_state_and_runtime(
secrets,
@ -1979,6 +1988,7 @@ pub fn router_with_worker_state<S: ReliableState + 'static>(
state,
allowlist,
worker_handler,
backend_id,
runtime_provider(OutboundConfig::default()),
)
}
@ -1989,6 +1999,7 @@ pub fn router_with_worker_state_and_runtime<S: ReliableState + 'static>(
state: S,
allowlist: HashSet<i64>,
worker_handler: Arc<dyn WorkerHandler>,
backend_id: Option<String>,
runtime: RuntimeConfigProvider,
) -> Router {
router_with_worker_state_runtime_bootstrap(
@ -1997,6 +2008,7 @@ pub fn router_with_worker_state_and_runtime<S: ReliableState + 'static>(
state,
allowlist,
worker_handler,
backend_id,
runtime,
SecretString::new(String::new()),
None,
@ -2013,6 +2025,7 @@ pub fn router_with_worker_state_runtime_bootstrap<S: ReliableState + 'static>(
state: S,
allowlist: HashSet<i64>,
worker_handler: Arc<dyn WorkerHandler>,
backend_id: Option<String>,
runtime: RuntimeConfigProvider,
admin_token: SecretString,
debug_token: Option<SecretString>,
@ -2023,6 +2036,7 @@ pub fn router_with_worker_state_runtime_bootstrap<S: ReliableState + 'static>(
state,
allowlist,
worker_handler,
backend_id,
runtime,
admin_token,
debug_token,
@ -2040,6 +2054,7 @@ fn router_with_worker_state_runtime_bootstrap_config<S: ReliableState + 'static>
state: S,
allowlist: HashSet<i64>,
worker_handler: Arc<dyn WorkerHandler>,
backend_id: Option<String>,
runtime: RuntimeConfigProvider,
admin_token: SecretString,
debug_token: Option<SecretString>,
@ -2061,6 +2076,7 @@ fn router_with_worker_state_runtime_bootstrap_config<S: ReliableState + 'static>
business_revision: Arc::new(RwLock::new(0)),
setup_missing: Arc::new(setup_missing),
reload,
backend_id,
};
let mut router = Router::new()
.route("/webhook/tg", post(telegram_webhook))
@ -2211,7 +2227,7 @@ mod tests {
#[tokio::test]
async fn configuration_setup_status_lists_only_missing_names() {
let response = router_configuration_setup(vec!["REDIS_URL".into()])
let response = router_configuration_setup(vec!["REDIS_URL".into()], None)
.oneshot(
Request::builder()
.method("GET")
@ -2231,10 +2247,36 @@ mod tests {
assert!(!body.windows(4).any(|part| part == b"pass"));
}
#[tokio::test]
async fn setup_status_reports_backend_label() {
// `backend` is threaded from the router builder into `/api/status` so a probe through
// the load balancer can tell which origin answered — `version` alone is identical
// across instances built from the same image. `main` reads `BACKEND_ID`/`HOSTNAME` at
// startup and passes a real value; here the value is echoed verbatim end-to-end.
let response = router_configuration_setup(vec![], Some("edge-1".into()))
.oneshot(
Request::builder()
.method("GET")
.uri("/api/status")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let value: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(value["backend"], "edge-1");
}
#[tokio::test]
async fn configuration_setup_status_lists_both_bootstrap_requirements() {
let response =
router_configuration_setup(vec!["REDIS_URL".into(), "CONFIG_ENCRYPTION_KEY".into()])
let response = router_configuration_setup(
vec!["REDIS_URL".into(), "CONFIG_ENCRYPTION_KEY".into()],
None,
)
.oneshot(
Request::builder()
.method("GET")
@ -2255,8 +2297,10 @@ mod tests {
#[tokio::test]
async fn configuration_setup_mounts_only_status_and_probes() {
let router =
router_configuration_setup(vec!["REDIS_URL".into(), "CONFIG_ENCRYPTION_KEY".into()]);
let router = router_configuration_setup(
vec!["REDIS_URL".into(), "CONFIG_ENCRYPTION_KEY".into()],
None,
);
let token = "Bearer CONFIG_ENCRYPTION_KEY-candidate";
// The status surface and both probes stay reachable: the SPA bootstrap card is driven
@ -2312,6 +2356,7 @@ mod tests {
MemoryState::default(),
HashSet::new(),
Arc::new(NoopWorker),
None,
runtime_provider(OutboundConfig::default()),
SecretString::new("acl-root".into()),
None,
@ -3297,6 +3342,7 @@ mod tests {
state.clone(),
HashSet::new(),
handler,
None,
);
(router, state)
}