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 透傳