fix(visionA-backend): DownloadStream 移除 dead ensurePromoted call (Bug #11)

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) <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-05-18 16:33:23 +08:00
parent 78c1343e9a
commit 2d629f3ba2
2 changed files with 31 additions and 18 deletions

View File

@ -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 內有 NEFconverter 端冪等)
// 回傳的 targetObjectKey 在 v0.6 只用於 logvisionA 端不再用它打 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 401converter ↔ FAA
// OAuth 鏈獨立 bug、跟 download 不該綁visionA download 卡 502。
// download 直接 GetResult 即可——converter MinIO 在 worker 跑完 nef stage 就有 NEF
// worker `_upload_output` line 121、不需要 promote。
//
// PromoteToModels 流程仍會呼叫 ensurePromotedline 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.GetFilevisionA 端完全不打 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),
)

View File

@ -1220,24 +1220,31 @@ func TestDownloadStream_JobNotCompleted(t *testing.T) {
assert.Nil(t, meta)
}
// TestDownloadStream_PromoteError_Propagationpromote 5xx 透傳。
func TestDownloadStream_PromoteError_Propagation(t *testing.T) {
// TestDownloadStream_DoesNotCallPromote2026-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_Propagationconverter.GetResult 5xx 透傳