package conversion import ( "context" "encoding/json" "io" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // noopService 是一個 compile-time 驗證 — 用來確認 Service interface 的方法集合穩定。 // 真實實作(Flow)會在 T6 補。這裡只測 interface 簽名沒有打錯(避免 T6 才發現要改 interface)。 type noopService struct{} func (noopService) InitJob(ctx context.Context, in InitJobInput) (*Job, error) { return nil, nil } func (noopService) GetJob(ctx context.Context, userID, jobID string) (*Job, error) { return nil, nil } func (noopService) PromoteToModels(ctx context.Context, userID, jobID, name string) (*PromoteResult, error) { return nil, nil } // Phase 0.8b T4:DownloadRedirectURL 改 DownloadStream(API key 模式下沒有 delegated token, // 改 server-side stream proxy;對應 ADR-015 §7 / conversion.md §4.1) func (noopService) DownloadStream(ctx context.Context, userID, jobID string) (io.ReadCloser, *DownloadMetadata, error) { return nil, nil, nil } func (noopService) ActiveJob(ctx context.Context, userID string) (*Job, error) { return nil, nil } // File-scope compile-time check — 若 Service interface 改變, // noopService 就不再實作此 interface,編譯失敗。 // 移到 file scope(T1 review M1):t.Run 內的 var declaration 只在執行該 test 時驗, // 而我們希望「package 編譯成功」就保證 interface 穩定。 var _ Service = noopService{} // TestService_InterfaceSatisfied 在 test 中再 assert 一次,作為文件性說明。 func TestService_InterfaceSatisfied(t *testing.T) { t.Parallel() var _ Service = noopService{} } // TestJob_JSONShape 驗證 Job struct 的 JSON tag 與 api-conversion.md §1-2 response 對齊。 // // 這是契約測試:frontend 依 api-conversion.md 寫 type;backend 改 json tag 一定要回頭看這個 test。 func TestJob_JSONShape(t *testing.T) { t.Parallel() createdAt, _ := time.Parse(time.RFC3339, "2026-04-30T12:00:00Z") expiresAt := createdAt.Add(7 * 24 * time.Hour) job := Job{ JobID: "550e8400-e29b-41d4-a716-446655440000", Status: "running", Stage: "bie", Progress: 45, StageProgress: 60, CreatedAt: createdAt, UpdatedAt: createdAt.Add(5 * time.Minute), ExpiresAt: expiresAt, SourceFilename: "yolov5s.onnx", TargetChip: "720", } raw, err := json.Marshal(job) require.NoError(t, err) // 必要欄位都在 assert.Contains(t, string(raw), `"job_id":"550e8400-e29b-41d4-a716-446655440000"`) assert.Contains(t, string(raw), `"status":"running"`) assert.Contains(t, string(raw), `"stage":"bie"`) assert.Contains(t, string(raw), `"progress":45`) assert.Contains(t, string(raw), `"stage_progress":60`) assert.Contains(t, string(raw), `"created_at":"2026-04-30T12:00:00Z"`) assert.Contains(t, string(raw), `"expires_at":"2026-05-07T12:00:00Z"`) assert.Contains(t, string(raw), `"source_filename":"yolov5s.onnx"`) assert.Contains(t, string(raw), `"target_chip":"720"`) // error 欄位 zero value 時應被 omitempty 隱藏 assert.NotContains(t, string(raw), `"error_code"`) assert.NotContains(t, string(raw), `"error_message"`) } // TestJob_FailedShape 驗證 failed job 的 error 欄位序列化。 func TestJob_FailedShape(t *testing.T) { t.Parallel() job := Job{ JobID: "job-failed", Status: "failed", ErrorCode: "QUANTIZATION_FAILED", ErrorMessage: "model has unsupported operator", } raw, err := json.Marshal(job) require.NoError(t, err) assert.Contains(t, string(raw), `"error_code":"QUANTIZATION_FAILED"`) assert.Contains(t, string(raw), `"error_message":"model has unsupported operator"`) } // TestPromoteResult_JSONShape 對齊 api-conversion.md §3 response。 func TestPromoteResult_JSONShape(t *testing.T) { t.Parallel() createdAt, _ := time.Parse(time.RFC3339, "2026-04-30T12:30:00Z") pr := PromoteResult{ ModelID: "abc-123", Source: "converted", SourceJobID: "550e8400-...", Name: "YOLOv5 Face KL520", TargetChip: "kl520", FileSize: 12345678, Status: "ready", CreatedAt: createdAt, } raw, err := json.Marshal(pr) require.NoError(t, err) assert.Contains(t, string(raw), `"model_id":"abc-123"`) assert.Contains(t, string(raw), `"source":"converted"`) assert.Contains(t, string(raw), `"source_job_id":"550e8400-..."`) assert.Contains(t, string(raw), `"file_size":12345678`) assert.Contains(t, string(raw), `"status":"ready"`) assert.Contains(t, string(raw), `"target_chip":"kl520"`) } // TestInitJobInput_AcceptsReader 驗證 InitJobInput.Body 接受 io.Reader(即 streaming 不收 buffer)。 // // 關鍵:若有人不小心把欄位改成 []byte,這個測試編譯會壞。 func TestInitJobInput_AcceptsReader(t *testing.T) { t.Parallel() in := InitJobInput{ UserID: "user-abc", ContentType: "multipart/form-data; boundary=xyz", Body: strings.NewReader("--xyz--"), ContentLength: 7, } // 確認 Body 是 io.Reader(compile time 透過 type assertion) var _ io.Reader = in.Body assert.Equal(t, "user-abc", in.UserID) }