181 lines
5.6 KiB
Go
181 lines
5.6 KiB
Go
package async
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"testing"
|
|
)
|
|
|
|
// TestLifecycle_AllEndpoints_ReachesTerminalState submits a job for every supported
|
|
// endpoint and polls until it reaches completed or failed, then validates the
|
|
// terminal response shape. Passes for either outcome — the test asserts the async
|
|
// mechanism itself, not model availability.
|
|
// Runs in both global and VK modes.
|
|
func TestLifecycle_AllEndpoints_ReachesTerminalState(t *testing.T) {
|
|
for _, mode := range testModes() {
|
|
t.Run(mode.name, func(t *testing.T) {
|
|
for _, ec := range endpointCases() {
|
|
t.Run(ec.name, func(t *testing.T) {
|
|
_, submitted, body := submitCase(t, ec, mode.headers)
|
|
if submitted.ID == "" {
|
|
t.Fatalf("submit returned no job id: %s", body)
|
|
}
|
|
|
|
pollPath := jobPollPath(ec.pollBase, submitted.ID)
|
|
code, job := pollUntilTerminal(t, pollPath, mode.headers)
|
|
|
|
if code != http.StatusOK {
|
|
t.Errorf("expected 200 for terminal job, got %d", code)
|
|
}
|
|
if job.ID != submitted.ID {
|
|
t.Errorf("polled id %q does not match submitted id %q", job.ID, submitted.ID)
|
|
}
|
|
if job.CompletedAt == nil {
|
|
t.Error("completed_at must be set on a terminal job")
|
|
}
|
|
if job.ExpiresAt == nil {
|
|
t.Error("expires_at must be set on a terminal job")
|
|
}
|
|
if job.CompletedAt != nil && job.ExpiresAt != nil && !job.ExpiresAt.After(*job.CompletedAt) {
|
|
t.Error("expires_at must be after completed_at")
|
|
}
|
|
|
|
switch job.Status {
|
|
case "completed":
|
|
if len(job.Result) == 0 || string(job.Result) == "null" {
|
|
t.Error("completed job must have a non-null result")
|
|
}
|
|
case "failed":
|
|
if len(job.Error) == 0 || string(job.Error) == "null" {
|
|
t.Error("failed job must have a non-null error")
|
|
}
|
|
if job.StatusCode == 0 {
|
|
t.Error("failed job must carry a non-zero status_code")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLifecycle_Poll_NonExistentJob_Returns404 confirms that polling a random job ID
|
|
// returns 404 regardless of VK mode (job lookup fails before VK check).
|
|
// Uses chat_completions as a representative endpoint — all endpoints share the same
|
|
// RetrieveJob() path, so repeating across all 11 adds no coverage.
|
|
func TestLifecycle_Poll_NonExistentJob_Returns404(t *testing.T) {
|
|
const fakeID = "00000000-0000-0000-0000-000000000000"
|
|
ec := chatCompletionCase()
|
|
for _, mode := range testModes() {
|
|
t.Run(mode.name, func(t *testing.T) {
|
|
pollPath := jobPollPath(ec.pollBase, fakeID)
|
|
code, _, _ := pollOnce(t, pollPath, mode.headers)
|
|
if code != http.StatusNotFound {
|
|
t.Errorf("expected 404 for non-existent job, got %d", code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLifecycle_CompletedJobResultShape checks that completed jobs carry the expected
|
|
// top-level fields in their result JSON. If a job fails (e.g., no live API key), the
|
|
// shape check is skipped for that case — the test asserts structure, not model availability.
|
|
func TestLifecycle_CompletedJobResultShape(t *testing.T) {
|
|
type shapeCheck struct {
|
|
name string
|
|
check func(t *testing.T, result json.RawMessage)
|
|
}
|
|
|
|
shapeChecks := map[string]shapeCheck{
|
|
"chat_completions": {
|
|
"choices[]",
|
|
func(t *testing.T, result json.RawMessage) {
|
|
var r struct {
|
|
Choices []json.RawMessage `json:"choices"`
|
|
}
|
|
if err := json.Unmarshal(result, &r); err != nil {
|
|
t.Fatalf("unmarshal choices: %v", err)
|
|
}
|
|
if len(r.Choices) == 0 {
|
|
t.Error("completed chat job must have at least one choice")
|
|
}
|
|
},
|
|
},
|
|
"embeddings": {
|
|
"data[]",
|
|
func(t *testing.T, result json.RawMessage) {
|
|
var r struct {
|
|
Data []json.RawMessage `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(result, &r); err != nil {
|
|
t.Fatalf("unmarshal data: %v", err)
|
|
}
|
|
if len(r.Data) == 0 {
|
|
t.Error("completed embeddings job must have at least one data entry")
|
|
}
|
|
},
|
|
},
|
|
"rerank": {
|
|
"results[]",
|
|
func(t *testing.T, result json.RawMessage) {
|
|
var r struct {
|
|
Results []json.RawMessage `json:"results"`
|
|
}
|
|
if err := json.Unmarshal(result, &r); err != nil {
|
|
t.Fatalf("unmarshal results: %v", err)
|
|
}
|
|
if len(r.Results) == 0 {
|
|
t.Error("completed rerank job must have at least one result")
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, ec := range endpointCases() {
|
|
sc, ok := shapeChecks[ec.name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
t.Run(ec.name+"/"+sc.name, func(t *testing.T) {
|
|
_, submitted, body := submitCase(t, ec, nil)
|
|
if submitted.ID == "" {
|
|
t.Fatalf("submit returned no job id: %s", body)
|
|
}
|
|
pollPath := jobPollPath(ec.pollBase, submitted.ID)
|
|
_, job := pollUntilTerminal(t, pollPath, nil)
|
|
if job.Status != "completed" {
|
|
t.Skipf("job status=%q (not completed) — shape check skipped", job.Status)
|
|
}
|
|
sc.check(t, job.Result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLifecycle_Poll_WrongEndpointType_Returns404 submits a job on one endpoint and
|
|
// polls it via a different endpoint's path, expecting 404 (type mismatch).
|
|
func TestLifecycle_Poll_WrongEndpointType_Returns404(t *testing.T) {
|
|
cases := endpointCases()
|
|
if len(cases) < 2 {
|
|
t.Skip("need at least two endpoint cases")
|
|
}
|
|
|
|
for _, mode := range testModes() {
|
|
t.Run(mode.name, func(t *testing.T) {
|
|
// Submit on cases[0], poll via cases[1]'s poll base.
|
|
submitter := cases[0]
|
|
wrongBase := cases[1].pollBase
|
|
|
|
_, submitted, body := submitCase(t, submitter, mode.headers)
|
|
if submitted.ID == "" {
|
|
t.Fatalf("submit returned no job id: %s", body)
|
|
}
|
|
|
|
pollPath := jobPollPath(wrongBase, submitted.ID)
|
|
code, _, _ := pollOnce(t, pollPath, mode.headers)
|
|
if code != http.StatusNotFound {
|
|
t.Errorf("expected 404 when polling with wrong endpoint type, got %d", code)
|
|
}
|
|
})
|
|
}
|
|
}
|