From 2d629f3ba20e6b38bf86155017292a44a911622f Mon Sep 17 00:00:00 2001 From: jim800121chen Date: Mon, 18 May 2026 16:33:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(visionA-backend):=20DownloadStream=20?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=20dead=20ensurePromoted=20call=20(Bug=20#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-016 v0.6 後 visionA download 直接從 converter MinIO 拿 NEF、不需先 promote 推上 FAA; 原 Phase 0.8 / v0.4-v0.5 設計的 ensurePromoted 是 dead call。 stage e2e 證實: - visionA DownloadStream → f.ensurePromoted → converter.Promote - converter Promote → faa.putFile (OAuthClientError 401 — converter↔FAA OAuth 鏈獨立 bug) - converter Promote 回 500、visionA DownloadStream 因 ensurePromoted 失敗回 502 - user 看到下載失敗 修法:flow.go DownloadStream 移除 ensurePromoted call、直接 GetResult(converter MinIO 已在 worker `_upload_output` 寫進 NEF、不需 promote 把 NEF 推 FAA)。 PromoteToModels(line 531)流程仍會呼叫 Promote、這是「加到模型庫」的合理步驟、不動。 驗證: - 17 packages race -count=3 全綠 - 新 test TestDownloadStream_DoesNotCallPromote 取代舊 TestDownloadStream_PromoteError_Propagation (故意把 promoteFunc 設成 t.Fatalf、確保 download path 完全不打 promote) 關聯 follow-up(未在本 commit 修): - converter↔FAA OAuth 401 仍是 promote-to-models 流程的 bug、需 converter / MC scope debug - 但 download 解耦後 user 至少能下載 Co-Authored-By: Claude Opus 4.7 (1M context) --- visionA-backend/internal/conversion/flow.go | 24 +++++++++++------- .../internal/conversion/flow_test.go | 25 ++++++++++++------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/visionA-backend/internal/conversion/flow.go b/visionA-backend/internal/conversion/flow.go index 899c79c..bb243da 100644 --- a/visionA-backend/internal/conversion/flow.go +++ b/visionA-backend/internal/conversion/flow.go @@ -756,15 +756,20 @@ func (f *flow) DownloadStream(ctx context.Context, userID, jobID string) (io.Rea return nil, nil, fmt.Errorf("%w: status=%s", ErrJobNotCompleted, cj.Status) } - // 3. ensurePromoted — 自動觸發 promote 確保 converter MinIO 內有 NEF(converter 端冪等) - // 回傳的 targetObjectKey 在 v0.6 只用於 log(visionA 端不再用它打 FAA) - targetObjectKey, err := f.ensurePromoted(ctx, userID, jobID, cj) - if err != nil { - return nil, nil, err - } + // 2026-05-18 Bug #11 移除 ensurePromoted call: + // 原 Phase 0.8 / v0.4-v0.5 設計需要先 promote(拿 target_object_key for FAA path) + // 再 stream from FAA。v0.6 + ADR-016 後 visionA 直接從 converter MinIO 拿、不再 + // 走 FAA、target_object_key 無用、ensurePromoted 是 dead call。 + // 實際 stage e2e 也證實:converter promote → FAA put 撞 OAuth 401(converter ↔ FAA + // OAuth 鏈獨立 bug、跟 download 不該綁),visionA download 卡 502。 + // download 直接 GetResult 即可——converter MinIO 在 worker 跑完 nef stage 就有 NEF + // (worker `_upload_output` line 121)、不需要 promote。 + // + // PromoteToModels 流程仍會呼叫 ensurePromoted(line 603 直接呼叫 f.converter.Promote)、 + // 這是「加到模型庫」流程的合理步驟、本次不動。 - // 4. converter.GetResult — 從 converter MinIO streaming pull NEF - // (v0.6:取代原 faa.GetFile(targetObjectKey);visionA 端不再直接打 FAA) + // 3. converter.GetResult — 從 converter MinIO streaming pull NEF + // (v0.6:取代原 faa.GetFile;visionA 端完全不打 FAA) stream, resultMeta, err := f.converter.GetResult(ctx, jobID) if err != nil { return nil, nil, err @@ -786,10 +791,11 @@ func (f *flow) DownloadStream(ctx context.Context, userID, jobID string) (io.Rea ContentLength: resultMeta.ContentLength, } + // 2026-05-18 Bug #11:原 log 含 hashObjectKey(targetObjectKey) — Bug #11 移除 + // ensurePromoted 後 targetObjectKey 不再可用;改記 job_id(已可 cross-ref)+ meta info。 f.logger.InfoContext(ctx, "conversion.flow.download_stream_opened", slog.String("user_hash", hashUserID(userID)), slog.String("job_id", jobID), - slog.String("object_key_hash", hashObjectKey(targetObjectKey)), slog.Int64("content_length", resultMeta.ContentLength), slog.String("filename", meta.Filename), ) diff --git a/visionA-backend/internal/conversion/flow_test.go b/visionA-backend/internal/conversion/flow_test.go index dbb115a..4c12fac 100644 --- a/visionA-backend/internal/conversion/flow_test.go +++ b/visionA-backend/internal/conversion/flow_test.go @@ -1220,24 +1220,31 @@ func TestDownloadStream_JobNotCompleted(t *testing.T) { assert.Nil(t, meta) } -// TestDownloadStream_PromoteError_Propagation:promote 5xx 透傳。 -func TestDownloadStream_PromoteError_Propagation(t *testing.T) { +// TestDownloadStream_DoesNotCallPromote:2026-05-18 Bug #11 後 download 不再呼叫 converter.Promote。 +// 設計理由:ADR-016 v0.6 後 visionA download 直接從 converter MinIO 拿 NEF、不需先 promote 推上 FAA; +// ensurePromoted 是 dead call。 +// 此 test 故意把 promoteFunc 設成 fail——download 不該打到、test 仍應成功(GetResult 走完)。 +func TestDownloadStream_DoesNotCallPromote(t *testing.T) { t.Parallel() fix := newFlowFixture(t) fix.converter.setJob(&ConverterJob{JobID: "j1", Status: "completed", CreatedAt: time.Now()}) fix.ownership.Set("j1", "user-alice") + // 故意把 promote 設成 fail——download 不該打到、test 應該仍成功 fix.converter.promoteFunc = func(ctx context.Context, jobID string, req PromoteReq) (*ConverterPromoteResult, error) { - return nil, fmt.Errorf("%w: promote 502", ErrConverterUnavailable) + t.Fatalf("download 不該呼叫 promote (Bug #11 已移除 ensurePromoted)") + return nil, errors.New("unreachable") } - _, _, err := fix.svc.DownloadStream(context.Background(), "user-alice", "j1") - require.Error(t, err) - assert.True(t, errors.Is(err, ErrConverterUnavailable)) + stream, meta, err := fix.svc.DownloadStream(context.Background(), "user-alice", "j1") + require.NoError(t, err) + require.NotNil(t, stream) + require.NotNil(t, meta) + _ = stream.Close() - // converter.GetResult 不該被打到(promote 失敗在 GetResult 之前) - assert.Equal(t, int32(0), fix.converter.getResultCalls.Load(), - "promote 失敗應在 converter.GetResult 之前短路") + // converter.GetResult 應該被打到 1 次(promote 沒被打、直接 GetResult) + assert.Equal(t, int32(1), fix.converter.getResultCalls.Load(), + "download 應直接 GetResult、不繞 promote") } // TestDownloadStream_ConverterGetResultError_Propagation:converter.GetResult 5xx 透傳