feat(visionA-backend): Phase 0.8b v0.6 T4 — config FAA 欄位砍 + .env 清 + i18n/godoc polish

對齊 ADR-016 / conversion.md v0.6.1 §3.1:visionA 端不再需要 FAA 設定(v0.5 T1 加的 FAAAPIKey/FAABaseURL 撤回)。

config 砍除:
- internal/config/config.go: ConversionConfig.FAABaseURL + FAAAPIKey 兩欄位
- internal/config/load.go: VISIONA_FAA_BASE_URL + VISIONA_FAA_API_KEY 兩 env 讀取
- Enabled() 簡化為「ConverterBaseURL + ConverterAPIKey 兩個非空」
- internal/config/load_test.go: TestLoad_ConversionEnabled 從 6 case 簡化為 4 case (all_set / missing_converter_url / missing_converter_key / all_empty)

.env*.example 對齊(3 個檔):
- visionA-backend/.env.example: 砍 2 個 FAA env row + 註解;header 改「2 欄位啟用」
- .env.stage.example: 同上;VISIONA_CONVERTER_API_KEY 保留 CHANGE_ME_OPENSSL_RAND_HEX_32 placeholder
- .env.dev.example: 註解區塊統一對齊

T3 review polish:
- m-2 internal/api/conversion.go: i18n message map 砍 4 個 dead case (download_token_failed / mc_token_unavailable / idp_misconfigured / idp_unavailable) — 對應 v0.5 mc_token_client 撤回時砍的 sentinel;落入 default「內部錯誤」、行為不變
- m-3 internal/conversion/util.go: hashObjectKey godoc 補「設計約束(重要)」段 + 3 條「不應做的事」(不出現在 response body/header / 不組 URL / 不寫進 user-facing 錯誤訊息)— 明示用途限定於 slog 欄位內、避免 misuse vector
- cmd/api-server/main.go: godoc 對齊 T4 完成狀態

驗證:
- B 層 verification 主動跑(T3 reviewer 接受暫緩、backend 主動跑避免 reviewer 二次要求):
  * 跨檔 grep: production code 0 functional 命中(殘留全是註解 audit trail / test fixture name)
  * 17 packages race -count=3 全綠
  * 3 個 .env 環境一致性驗證
- go build ./... exit 0
- go test -race -count=3 ./... 17 packages 全綠
- Reviewer 5 軸(v0.6-t4-review) 通過(0 Critical / 0 Major / 2 Minor / 4 Suggestion)

v0.6 對齊改造事實上完工:
- T1 ConverterClient.GetResult method
- T2 flow.go DownloadStream/PromoteToModels 改用 GetResult + e2e endpoint
- T3 faa_client 整檔砍 + ErrFAA* sentinel 清 + s-3/s-4/s-5 必補 + mockFAA regression-only
- T4 config FAA 欄位砍 + .env 清 + i18n/godoc polish

main.go startup log 已是「converter_api_key_set only」、無 FAA 殘留 / 無 tenant_id(T2-T3 已處理)。e2e regression 防護由 mockFAA negative assertion 守住(T3)。

下一步:
- visionA backend 端 ADR-016 對齊完工,等使用者跨 repo 加 converter GET /api/v1/jobs/{id}/result endpoint
- stage redeploy + e2e 完整測試

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-05-16 19:16:28 +08:00
parent 6024c294d3
commit 9e29ebf767
9 changed files with 91 additions and 110 deletions

View File

@ -79,16 +79,18 @@ VISIONA_PAIRING_TOKEN=
# ============================================================
# Phase 0.8 / 0.8b — 轉檔功能整合dev 通常不啟用,留空即可)
# ============================================================
# 對齊 .autoflow/04-architecture/conversion.md §3 + ADR-015。
# 對齊 docs/autoflow/04-architecture/conversion.md §3 + ADR-015 + ADR-016
#
# 4 個欄位全部非空才會啟用 conversion 模組dev 全空 = sidebar tab 顯示但 endpoint 不註冊。
# 若要在 dev 連 stage 的 converter / FAA 測整合,依 .env.stage.example 模板填入。
# 2 個欄位ConverterBaseURL / ConverterAPIKey全部非空才會啟用 conversion 模組;
# dev 全空 = sidebar tab 顯示但 endpoint 不註冊。
# 若要在 dev 連 stage 的 converter 測整合,依 .env.stage.example 模板填入。
#
# Phase 0.8b v0.6 T4原 FAA 相關 envVISIONA_FAA_BASE_URL / VISIONA_FAA_API_KEY
# 已撤回ADR-016visionA 端不再直接呼叫 FAAdownload / promote 改走 converter.GetResult
#
# 產生 API keyopenssl rand -hex 32
# VISIONA_CONVERTER_BASE_URL=http://192.168.0.130:9501
# VISIONA_FAA_BASE_URL=http://192.168.0.130:5081
# VISIONA_CONVERTER_API_KEY=
# VISIONA_FAA_API_KEY=
# ============================================================
# 進階port 衝突時可改

View File

@ -115,33 +115,29 @@ VISIONA_SEED_DEMO_DATA=false
# 留註解作為 audit trailstage 部署不需設定 VISIONA_STATIC_USER_ID。
# ============================================================
# Phase 0.8 / 0.8b — 轉檔功能整合converter / FAA pre-shared API key
# Phase 0.8 / 0.8b — 轉檔功能整合converter pre-shared API key
# ============================================================
# 對齊 .autoflow/04-architecture/conversion.md §3 + ADR-015。
# 對齊 docs/autoflow/04-architecture/conversion.md §3 + ADR-015 + ADR-016
#
# Phase 0.8b 變更:服務間認證從 OAuth client_credentials 改為 pre-shared API key。
#
# 啟用判定:4 個欄位ConverterBaseURL / FAABaseURL / ConverterAPIKey / FAAAPIKey
# **全部非空**才視為啟用;任一缺 → 5 個 /api/conversion/* endpoint 不註冊。
# 啟用判定:2 個欄位ConverterBaseURL / ConverterAPIKey**全部非空**才視為啟用;
# 任一缺 → 5 個 /api/conversion/* endpoint 不註冊。
#
# Phase 0.8b 移除(不再讀取,也別再放進 .env.stage
# - VISIONA_OIDC_SERVICE_CLIENT_ID / _SECRET服務間認證取消 MC client_credentials
# - VISIONA_CONVERSION_TENANT_ID / VISIONA_OIDC_TENANT_ID取消 tenant 概念)
# - VISIONA_FAA_DELEGATED_TTL_SECONDS取消 delegated download token 機制;改 server-side stream proxy
#
# Phase 0.8b v0.6 T4 移除(不再讀取,也別再放進 .env.stageADR-016 撤回 v0.5 設計缺口):
# - VISIONA_FAA_BASE_URLvisionA 端不再直接呼叫 FAA
# - VISIONA_FAA_API_KEY同上download / promote 改走 converter.GetResult
# kneron_model_converter task-scheduler base URLstage 公司內網)
VISIONA_CONVERTER_BASE_URL=http://192.168.0.130:9501
# File Access Agent base URL
VISIONA_FAA_BASE_URL=http://192.168.0.130:5081
# Pre-shared API key — visionA → converter 服務間認證Phase 0.8b 新增ADR-015 §3
# **必須換掉**openssl rand -hex 32 產生 64 字元 hex與 converter 端 CONVERTER_API_KEY 對齊
# 雙方獨立持有、不共用、嚴格分環境dev / stage / prod 各自獨立 key
# log 永遠不印此值全文;部署時用 AWS Secrets Manager / Vault 注入
VISIONA_CONVERTER_API_KEY=CHANGE_ME_OPENSSL_RAND_HEX_32
# Pre-shared API key — visionA → FAA 服務間認證Phase 0.8b 新增ADR-015 §3
# **必須換掉**openssl rand -hex 32與 FAA 端 FAA_API_KEY 對齊warrenchen 配置)
# 與 ConverterAPIKey **不共用**(每條 trust boundary 各自獨立,避免一處洩漏連坐)
VISIONA_FAA_API_KEY=CHANGE_ME_OPENSSL_RAND_HEX_32

View File

@ -80,7 +80,8 @@ VISIONA_FRONTEND_URL=http://localhost:3000
# 服務間認證從 OAuth client_credentials 改為 pre-shared API key見 ADR-015、conversion.md §3
# 兩個 service client env 不再讀取OIDCConfig.ServiceClientID/Secret struct 欄位
# 為了 backward compat 暫保留、但 conversion 模組不再依賴)。
# 取代設定見下方 Phase 0.8 / 0.8b 區塊的 VISIONA_CONVERTER_API_KEY / VISIONA_FAA_API_KEY。
# 取代設定見下方 Phase 0.8 / 0.8b 區塊的 VISIONA_CONVERTER_API_KEY
# Phase 0.8b v0.6 T4 起 visionA 端不再直接呼叫 FAA、原 VISIONA_FAA_API_KEY 已撤回)。
# Cookie HMAC 簽章 secret 至少 32 byte 隨機字串prod 用 openssl rand -hex 32
VISIONA_SESSION_SECRET=CHANGE_ME_TO_RANDOM_64_BYTES_in_production
@ -157,40 +158,34 @@ VISIONA_PAIRING_TOKEN=
# ============================================================
# Phase 0.8 / 0.8b — 轉檔功能整合converter / FAA pre-shared API key
# Phase 0.8 / 0.8b — 轉檔功能整合converter pre-shared API key
# ============================================================
# 對齊 .autoflow/04-architecture/conversion.md §3 + ADR-015。
# 對齊 docs/autoflow/04-architecture/conversion.md §3 + ADR-015 + ADR-016
#
# Phase 0.8b 變更:服務間認證從 OAuth client_credentials 改為 pre-shared API key。
#
# 啟用判定:4 個欄位ConverterBaseURL / FAABaseURL / ConverterAPIKey / FAAAPIKey
# **全部非空**才視為啟用;任一缺 → 5 個 /api/conversion/* endpoint 不註冊 / 回 501。
# 啟用判定:2 個欄位ConverterBaseURL / ConverterAPIKey**全部非空**才視為啟用;
# 任一缺 → 5 個 /api/conversion/* endpoint 不註冊 / 回 501。
#
# Phase 0.8b 移除(不再讀取):
# - VISIONA_OIDC_SERVICE_CLIENT_ID / _SECRETOAuth client_credentials 機制取消)
# - VISIONA_OIDC_TENANT_ID取消 tenant 概念converter 端的 user_id 仍由 visionA 灌入)
# - VISIONA_FAA_DELEGATED_TTL_SECONDSdelegated download token 機制取消,改 server-side stream proxy
#
# Phase 0.8b v0.6 T4 移除不再讀取ADR-016 撤回 v0.5 設計缺口):
# - VISIONA_FAA_BASE_URLvisionA 端不再直接呼叫 FAA
# - VISIONA_FAA_API_KEY同上download / promote 改走 converter.GetResult
# kneron_model_converter task-scheduler base URL
# dev/stagehttp://192.168.0.130:9501
# prodhttps://converter.visiona.cloud
VISIONA_CONVERTER_BASE_URL=
# File Access Agent base URL
# dev/stagehttp://192.168.0.130:5081
# prodhttps://faa.innovedus.com
VISIONA_FAA_BASE_URL=
# Pre-shared API key — visionA → converter 服務間認證Phase 0.8b 新增ADR-015 §3
# 產生openssl rand -hex 3264 字元 hex
# 與 converter 端 CONVERTER_API_KEY env 對齊(雙方獨立持有,嚴格分環境 dev / stage / prod
# ⚠️ 不可 commitprod 用 Secrets Manager / Vaultlog 永遠不印此值全文
VISIONA_CONVERTER_API_KEY=
# Pre-shared API key — visionA → FAA 服務間認證Phase 0.8b 新增ADR-015 §3
# 產生方式同上;與 FAA 端 FAA_API_KEY env 對齊warrenchen 配置)
# 與 ConverterAPIKey **不共用**(每條 trust boundary 各自獨立,避免一處洩漏連坐)
VISIONA_FAA_API_KEY=
# 上傳模型檔大小上限MB— 與 converter 端 limit 對齊
VISIONA_CONVERTER_MAX_MODEL_SIZE_MB=500

View File

@ -141,16 +141,17 @@ func main() {
// 對齊 docs/autoflow/04-architecture/conversion.md、ADR-015、ADR-016。
//
// 啟用條件cfg.Conversion.Enabled() —
// 由 ConverterBaseURL + ConverterAPIKey 決定(FAABaseURL / FAAAPIKey 由 T4 砍除 env 校驗)。
// 由 ConverterBaseURL + ConverterAPIKey 決定(v0.6 T4 起 visionA 端不再需要 FAA env)。
// 不啟用時 deps.Conversion 為 nil5 個 endpoint 自動回 501registerConversionRoutes 處理)。
//
// **Phase 0.8b T5**:完全切換至 pre-shared API key 認證 — 不再 wire MCTokenClient、
// 不再讀 OIDCConfig.ServiceClientID/Secret、不再有 tenant_id / delegated_ttl_sec
// 概念。參見 ADR-015 §6 變更影響清單。
//
// **Phase 0.8b v0.6 T3**:撤回 visionA → FAA 直接呼叫ADR-016 撤回 v0.5 設計缺口)。
// faa_client.go / FAAClient interface / FlowOpts.FAA 全部砍除download / promote 流程
// 改走 converter.GetResult。FAABaseURL / FAAAPIKey env 仍保留在 config 直到 T4 一併砍。
// **Phase 0.8b v0.6 T3 + T4**:撤回 visionA → FAA 直接呼叫ADR-016 撤回 v0.5 設計缺口)。
// T3 砍 faa_client.go / FAAClient interface / FlowOpts.FAAT4 砍 ConversionConfig
// FAABaseURL / FAAAPIKey 兩欄位與對應 envVISIONA_FAA_BASE_URL / VISIONA_FAA_API_KEY
// `Enabled()` 簡化為只判 converter 兩欄位。download / promote 流程改走 converter.GetResult。
var conversionService conversion.Service
if cfg.Conversion.Enabled() {
// 不再檢查 ServiceClientID/Secret —— Phase 0.8b 起 conversion 不依賴 OIDC service client。

View File

@ -508,17 +508,15 @@ func errorMessageFor(code string) string {
case "converter_unavailable":
return "轉檔服務暫時無法使用"
case "faa_unavailable":
// converter promote 內部 PUT FAA 失敗時透傳,由 SRE 區分 converter 不可達 vs
// converter→FAA push 失敗ADR-016visionA 端不再直接呼叫 FAA
return "檔案存取服務暫時無法使用"
case "download_token_failed":
return "無法取得下載授權"
case "mc_token_unavailable":
return "無法取得下載授權,請重試"
case "idp_misconfigured":
return "系統設定錯誤,請聯絡支援"
case "idp_unavailable":
return "認證服務暫時無法使用"
case "service_busy":
return "系統繁忙,請稍後再試"
// Phase 0.8b v0.6 T3 / T4以下 i18n key 對應的 sentinel 已砍除,不再會被產生:
// - download_token_failed / mc_token_unavailablev0.5 mc_token_client 撤回)
// - idp_misconfigured / idp_unavailable同上
// 故 case 直接省略errorMessageFor 落入 default。
default:
return "內部錯誤"
}

View File

@ -182,30 +182,31 @@ type LoggerConfig struct {
// ConversionConfig 控制 Phase 0.8 / 0.8b 轉檔功能整合。
//
// 對齊 .autoflow/04-architecture/conversion.md §3、`adr/adr-015-server-to-server-api-key.md`。
// 對齊 docs/autoflow/04-architecture/conversion.md §3、
// `adr/adr-015-server-to-server-api-key.md`、`adr/adr-016-download-via-converter.md`。
//
// 啟用判定(由 Enabled() 給 main.go 用Phase 0.8b 4 個欄位ConverterBaseURL /
// FAABaseURL / ConverterAPIKey / FAAAPIKey**全部非空**才視為啟用;任一缺即視為未啟用,
// 啟用判定(由 Enabled() 給 main.go 用Phase 0.8b v0.6 起2 個必要欄位
// ConverterBaseURL / ConverterAPIKey**全部非空**才視為啟用;任一缺即視為未啟用,
// 5 個 /api/conversion/* endpoint 不會 wiremain.go 在 wire 階段跳過、log warn
//
// **Phase 0.8b 變更**:服務間認證從 OAuth client_credentials 改為 pre-shared API keyADR-015
// `Enabled()` 加入兩個 API key 非空檢查。
// **Phase 0.8b 變更**:服務間認證從 OAuth client_credentials 改為 pre-shared API keyADR-015
//
// **Phase 0.8b T5 完成**(見 conversion.md §3.2 / ADR-015 §5 §7原暫留欄位
// TenantID / DelegatedTTLSeconds 已移除 — MC 認證鏈與 delegated download token 機制
// 都不存在了,兩個欄位連同對應 envVISIONA_OIDC_TENANT_ID /
// VISIONA_FAA_DELEGATED_TTL_SECONDS一併清除。
//
// **Phase 0.8b v0.6 T4 完成**(見 ADR-016 §2 / conversion.md v0.6.1 §3.1):原 FAA 相關
// 欄位 FAABaseURL / FAAAPIKey 已移除 — visionA 端不再直接呼叫 FAAdownload / promote
// 流程改走 converter.GetResultADR-016 撤回 v0.5 設計缺口),兩個欄位連同對應 env
// VISIONA_FAA_BASE_URL / VISIONA_FAA_API_KEY一併清除`Enabled()` 也簡化為只判
// converter 兩欄位。
type ConversionConfig struct {
// ConverterBaseURL 是 kneron_model_converter task-scheduler 服務的 base URL。
// 例http://192.168.0.130:9501dev / stage / https://converter.visiona.cloudprod
// 對齊 VISIONA_CONVERTER_BASE_URL留空 = 不啟用 Phase 0.8 轉檔功能。
ConverterBaseURL string
// FAABaseURL 是 File Access Agent 的 base URL。
// 例http://192.168.0.130:5081dev / stage / https://faa.innovedus.comprod
// 對齊 VISIONA_FAA_BASE_URL留空 = 不啟用 Phase 0.8 轉檔功能。
FAABaseURL string
// ConverterAPIKey 是 visionA → converter 服務間認證的 pre-shared API keyPhase 0.8b 新增)。
// 對齊 VISIONA_CONVERTER_API_KEY以 `Authorization: Bearer <key>` 形式帶上。
// 雙方獨立產生(`openssl rand -hex 32`visionA 端的值必須與 converter 端的
@ -216,12 +217,6 @@ type ConversionConfig struct {
// 部署用 AWS Secrets Manager / Vault嚴格分環境dev / stage / prod 各自獨立 key
ConverterAPIKey string
// FAAAPIKey 是 visionA → FAA 服務間認證的 pre-shared API keyPhase 0.8b 新增)。
// 對齊 VISIONA_FAA_API_KEY以 `Authorization: Bearer <key>` 形式帶上。
// 與 ConverterAPIKey **不共用**(每條 trust boundary 各自獨立,避免一處洩漏連坐 — ADR-015 §3
// 對應 FAA 端的 `FAA_API_KEY` env由 warrenchen 配置(跨 repo 同步)。
FAAAPIKey string
// MaxModelSizeMB 是 visionA-backend 端對上傳模型檔的大小上限MB
// 與 converter 端 limit 對齊converter 預設 500 MB
// 對齊 VISIONA_CONVERTER_MAX_MODEL_SIZE_MB預設 500。
@ -230,14 +225,12 @@ type ConversionConfig struct {
// Enabled 回傳 Phase 0.8 / 0.8b conversion 是否啟用。
//
// **Phase 0.8b 變更**ADR-015 §6除既有的 ConverterBaseURL / FAABaseURL 外,
// 加入 ConverterAPIKey / FAAAPIKey 非空檢查4 個欄位皆非空才算啟用。
// 任一缺 → 視為未啟用main.go 不會 wire conversion.Service5 個 endpoint 回 501 / 不註冊)。
// **Phase 0.8b v0.6 T4 簡化**ADR-016 §2 / conversion.md v0.6.1 §3.1visionA 端撤回
// 對 FAA 的直接呼叫download / promote 改走 converter.GetResult只剩 ConverterBaseURL
// 與 ConverterAPIKey 兩個欄位需非空。任一缺 → 視為未啟用main.go 不會 wire
// conversion.Service5 個 endpoint 回 501 / 不註冊)。
func (c ConversionConfig) Enabled() bool {
return c.ConverterBaseURL != "" &&
c.FAABaseURL != "" &&
c.ConverterAPIKey != "" &&
c.FAAAPIKey != ""
return c.ConverterBaseURL != "" && c.ConverterAPIKey != ""
}
// CORSConfig 控制 api-server 對瀏覽器的 CORS 白名單。

View File

@ -68,15 +68,17 @@ func Load() *Config {
CORS: CORSConfig{
AllowedOrigins: getEnvStringSlice("VISIONA_CORS_ALLOWED_ORIGINS", nil),
},
// Phase 0.8 / 0.8b conversion (見 .autoflow/04-architecture/conversion.md §3、ADR-015)
// Phase 0.8 / 0.8b conversion (見 docs/autoflow/04-architecture/conversion.md §3、
// ADR-015、ADR-016)
// Phase 0.8b T5原暫留欄位 TenantID / DelegatedTTLSeconds 與對應 env
// VISIONA_OIDC_TENANT_ID / VISIONA_FAA_DELEGATED_TTL_SECONDS已移除 —
// MC 認證鏈與 delegated download token 機制不存在了。
// Phase 0.8b v0.6 T4原 FAA 相關欄位 FAABaseURL / FAAAPIKey 與對應 env
// VISIONA_FAA_BASE_URL / VISIONA_FAA_API_KEY已移除 — ADR-016 撤回 v0.5
// 設計缺口visionA 端不再直接呼叫 FAA、download/promote 改走 converter.GetResult。
Conversion: ConversionConfig{
ConverterBaseURL: getEnvString("VISIONA_CONVERTER_BASE_URL", ""),
FAABaseURL: getEnvString("VISIONA_FAA_BASE_URL", ""),
ConverterAPIKey: getEnvString("VISIONA_CONVERTER_API_KEY", ""),
FAAAPIKey: getEnvString("VISIONA_FAA_API_KEY", ""),
MaxModelSizeMB: getEnvInt("VISIONA_CONVERTER_MAX_MODEL_SIZE_MB", 500),
},
}

View File

@ -268,15 +268,19 @@ func TestLoad_CORSAllowedOrigins(t *testing.T) {
// TestLoad_ConversionDefaults 驗證 Phase 0.8 / 0.8b conversion 欄位的預設行為。
//
// 對齊 .autoflow/04-architecture/conversion.md §3 + ADR-015留空時 Enabled() 為 false
// 5 個 endpoint 不會 wiremain.go 在 wire 階段會跳過)。
// 對齊 docs/autoflow/04-architecture/conversion.md §3 + ADR-015 + ADR-016:留空時
// Enabled() 為 false5 個 endpoint 不會 wiremain.go 在 wire 階段會跳過)。
//
// Phase 0.8b T5原暫留欄位 TenantID / DelegatedTTLSeconds 已從 ConversionConfig 移除
// MC 認證鏈與 delegated download token 機制不存在了);本 test 不再驗這兩欄位。
//
// Phase 0.8b v0.6 T4原 FAA 相關欄位 FAABaseURL / FAAAPIKey 已移除ADR-016 撤回
// v0.5 設計缺口visionA 端不再直接呼叫 FAA本 test 不再驗這兩欄位,對應的
// VISIONA_FAA_BASE_URL / VISIONA_FAA_API_KEY env 也不再 clear。
func TestLoad_ConversionDefaults(t *testing.T) {
for _, k := range []string{
"VISIONA_CONVERTER_BASE_URL", "VISIONA_FAA_BASE_URL",
"VISIONA_CONVERTER_API_KEY", "VISIONA_FAA_API_KEY",
"VISIONA_CONVERTER_BASE_URL",
"VISIONA_CONVERTER_API_KEY",
"VISIONA_CONVERTER_MAX_MODEL_SIZE_MB",
} {
t.Setenv(k, "")
@ -284,49 +288,36 @@ func TestLoad_ConversionDefaults(t *testing.T) {
cfg := Load()
assert.Empty(t, cfg.Conversion.ConverterBaseURL)
assert.Empty(t, cfg.Conversion.FAABaseURL)
assert.Empty(t, cfg.Conversion.ConverterAPIKey, "Phase 0.8bAPI key 預設留空")
assert.Empty(t, cfg.Conversion.FAAAPIKey, "Phase 0.8bAPI key 預設留空")
assert.Equal(t, 500, cfg.Conversion.MaxModelSizeMB, "預設 500 MB與 converter 對齊)")
assert.False(t, cfg.Conversion.Enabled(), "全空 → 不啟用")
}
// TestLoad_ConversionEnabled 驗證 Conversion.Enabled() 的判定邏輯Phase 0.8b 修訂)
// TestLoad_ConversionEnabled 驗證 Conversion.Enabled() 的判定邏輯
//
// Phase 0.8b 變更4 個欄位Converter URL / FAA URL / Converter API key / FAA API key
// 全部非空才視為啟用;任一缺即 disable。
// Phase 0.8b v0.6 T4 簡化ADR-016visionA 端撤回對 FAA 的直接呼叫後,
// 只剩 ConverterBaseURL + ConverterAPIKey 兩個欄位需非空;任一缺即 disable。
func TestLoad_ConversionEnabled(t *testing.T) {
cases := []struct {
name string
converterURL string
faaURL string
converterKey string
faaKey string
wantEnabled bool
}{
{"all_set_enables",
"http://converter:9501", "http://faa:5081",
"http://converter:9501",
"converter-key-32-bytes-hex-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"faa-key-32-bytes-hex-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
true},
{"missing_converter_url_disabled",
"", "http://faa:5081",
"converter-key", "faa-key",
false},
{"missing_faa_url_disabled",
"http://converter:9501", "",
"converter-key", "faa-key",
"",
"converter-key",
false},
{"missing_converter_key_disabled",
"http://converter:9501", "http://faa:5081",
"", "faa-key",
false},
{"missing_faa_key_disabled",
"http://converter:9501", "http://faa:5081",
"converter-key", "",
"http://converter:9501",
"",
false},
{"all_empty_disabled",
"", "", "", "",
"", "",
false},
}
@ -334,9 +325,7 @@ func TestLoad_ConversionEnabled(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Setenv("VISIONA_CONVERTER_BASE_URL", tc.converterURL)
t.Setenv("VISIONA_FAA_BASE_URL", tc.faaURL)
t.Setenv("VISIONA_CONVERTER_API_KEY", tc.converterKey)
t.Setenv("VISIONA_FAA_API_KEY", tc.faaKey)
cfg := Load()
assert.Equal(t, tc.wantEnabled, cfg.Conversion.Enabled())
})
@ -347,39 +336,35 @@ func TestLoad_ConversionEnabled(t *testing.T) {
//
// Phase 0.8b T5原暫留欄位 TenantID / DelegatedTTLSeconds 已移除,本 test
// 不再驗這兩欄位(對應 env 也不再讀取)。
//
// Phase 0.8b v0.6 T4原 FAA 相關欄位 FAABaseURL / FAAAPIKey 已移除ADR-016
// 本 test 不再驗這兩欄位、對應 env 也不再讀取。
func TestLoad_ConversionAllSet(t *testing.T) {
const fakeConverterKey = "fake-converter-api-key-for-test-do-not-use-in-prod"
const fakeFAAKey = "fake-faa-api-key-for-test-do-not-use-in-prod"
t.Setenv("VISIONA_CONVERTER_BASE_URL", "http://192.168.0.130:9501")
t.Setenv("VISIONA_FAA_BASE_URL", "http://192.168.0.130:5081")
t.Setenv("VISIONA_CONVERTER_API_KEY", fakeConverterKey)
t.Setenv("VISIONA_FAA_API_KEY", fakeFAAKey)
t.Setenv("VISIONA_CONVERTER_MAX_MODEL_SIZE_MB", "300")
cfg := Load()
assert.Equal(t, "http://192.168.0.130:9501", cfg.Conversion.ConverterBaseURL)
assert.Equal(t, "http://192.168.0.130:5081", cfg.Conversion.FAABaseURL)
assert.Equal(t, fakeConverterKey, cfg.Conversion.ConverterAPIKey)
assert.Equal(t, fakeFAAKey, cfg.Conversion.FAAAPIKey)
assert.Equal(t, 300, cfg.Conversion.MaxModelSizeMB)
assert.True(t, cfg.Conversion.Enabled())
}
// TestLoad_ConversionAPIKeysOnlyPhase 0.8b T5 — 4 個必要欄位齊全即 Enabled。
// TestLoad_ConversionAPIKeysOnlyPhase 0.8b v0.6 T4 — 2 個必要欄位齊全即 Enabled。
//
// 此 test 在 T1-T4 期間驗證「廢棄 env 不設也能 Enabled」T5 完成後該邏輯
// 由本 test 與 TestLoad_ConversionAllSet 共同覆蓋(因為廢棄 env 已徹底移除)。
// 本 test 與 TestLoad_ConversionAllSet 共同覆蓋 Enabled() 的最小啟用條件:
// 只要 ConverterBaseURL + ConverterAPIKey 兩個欄位都非空、即視為啟用,
// MaxModelSizeMB 不影響啟用判定。
func TestLoad_ConversionAPIKeysOnly(t *testing.T) {
const fakeConverterKey = "fake-converter-api-key-only-test"
const fakeFAAKey = "fake-faa-api-key-only-test"
t.Setenv("VISIONA_CONVERTER_BASE_URL", "http://192.168.0.130:9501")
t.Setenv("VISIONA_FAA_BASE_URL", "http://192.168.0.130:5081")
t.Setenv("VISIONA_CONVERTER_API_KEY", fakeConverterKey)
t.Setenv("VISIONA_FAA_API_KEY", fakeFAAKey)
cfg := Load()
assert.True(t, cfg.Conversion.Enabled(),
"Phase 0.8b T54 個必要欄位齊全即 Enabled")
"Phase 0.8b v0.6 T42 個必要欄位齊全即 Enabled")
}

View File

@ -30,6 +30,15 @@ func truncate(s string, max int) string {
// - 目前 visionA 的 object_key 不直接含 user 敏感資訊,但保險起見統一 hash
// - 16 chars hex64-bit對 visionA 內部 job 數量來說碰撞機率極低,足以追蹤單一 request
//
// **設計約束(重要):此 hash 僅供內部 log / observability 用、不應對外暴露**。
// 具體不應:
// - 出現在 HTTP response body / headerfrontend 不需要、洩漏內部 storage 結構)
// - 拿來組 presigned URL / download URL 的 pathhash 不可逆、不能用來定位物件)
// - 寫進使用者可見的錯誤訊息(用 request_id 追蹤即可)
//
// 用途限定於 slog 欄位(如 `slog.String("object_key_hash", hashObjectKey(...))`
// 配合 request_id 在 log aggregator 內串接同一 NEF 物件在多個 service 之間的流向。
//
// Phase 0.8b v0.6T3原本定義在 faa_client.go砍 faa_client.go 時搬到 util.go
// flow.go DownloadStream / PromoteToModels 仍用 hashObjectKey 為 log 加 target_object_key
// 標記,方便跨 service 追蹤同一個 NEF 物件)。