fix: music poll resilience + hostile-URL guard + embed dup-index (gadfly round 1)

- pollResult tolerates up to 5 CONSECUTIVE bad polls (transport blip,
  unparseable payload, task momentarily absent) instead of killing a
  multi-minute exclusive-GPU job on the first hiccup; only status=2, a
  failure run, or ctx deadline aborts
- server-supplied result.File must be server-relative; combined with the
  upstreamPath dot-dot/scheme rejection this stops a hostile upstream
  from steering the follow-up GET at other proxy endpoints (test:
  ../../api/models/unload refused)
- WithSteps(<=0) rejected; embed responses repeating an index rejected;
  musicFormatMIME now wraps speechMIME (one format table, wav32
  normalized); poll interval is a test-shrinkable var (CI no longer
  burns 2s+ per music test); parens + comments per review

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-07-13 00:52:06 -04:00
co-authored by Claude Fable 5
parent cacce61ddc
commit cf2d83f157
3 changed files with 143 additions and 35 deletions
+71
View File
@@ -51,6 +51,10 @@ func aceStepStub(t *testing.T, mp3 []byte) (*httptest.Server, *atomic.Int32) {
}
func TestMusicGenerate(t *testing.T) {
old := musicPollInterval
musicPollInterval = 5 * time.Millisecond
defer func() { musicPollInterval = old }()
mp3 := []byte("ID3fakeaudio")
srv, polls := aceStepStub(t, mp3)
defer srv.Close()
@@ -79,6 +83,10 @@ func TestMusicGenerate(t *testing.T) {
}
func TestMusicGenerateUpstreamFailure(t *testing.T) {
old := musicPollInterval
musicPollInterval = 5 * time.Millisecond
defer func() { musicPollInterval = old }()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/upstream/musicgen-acestep/release_task":
@@ -222,3 +230,66 @@ func TestInstructedQuery(t *testing.T) {
t.Errorf("InstructedQuery = %q", got)
}
}
func TestMusicGenerateSurvivesTransientPollFailures(t *testing.T) {
old := musicPollInterval
musicPollInterval = 5 * time.Millisecond
defer func() { musicPollInterval = old }()
mp3 := []byte("ID3fakeaudio")
var polls atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/upstream/musicgen-acestep/release_task":
_, _ = w.Write([]byte(`{"data": {"task_id": "t-3"}}`))
case "/upstream/musicgen-acestep/query_result":
switch polls.Add(1) {
case 1:
w.WriteHeader(http.StatusBadGateway) // transient transport blip
case 2:
_, _ = w.Write([]byte(`{"data": []}`)) // task momentarily absent
default:
_, _ = w.Write([]byte(`{"data": [{"task_id": "t-3", "status": 1,
"result": "{\"file\": \"/v1/audio?path=out.mp3\"}"}]}`))
}
case "/upstream/musicgen-acestep/v1/audio":
w.Header().Set("Content-Type", "audio/mpeg")
_, _ = w.Write(mp3)
}
}))
defer srv.Close()
p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client()))
mm, _ := p.MusicModel("musicgen-acestep")
res, err := mm.Generate(context.Background(), musicgen.Request{Prompt: "p"})
if err != nil {
t.Fatalf("Generate should survive 2 transient failures: %v", err)
}
if len(res.Audio.Data) == 0 {
t.Fatal("no audio")
}
}
func TestMusicGenerateRejectsHostileFileURL(t *testing.T) {
old := musicPollInterval
musicPollInterval = 5 * time.Millisecond
defer func() { musicPollInterval = old }()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/upstream/musicgen-acestep/release_task":
_, _ = w.Write([]byte(`{"data": {"task_id": "t-4"}}`))
case "/upstream/musicgen-acestep/query_result":
_, _ = w.Write([]byte(`{"data": [{"task_id": "t-4", "status": 1,
"result": "{\"file\": \"/v1/audio?path=../../api/models/unload\"}"}]}`))
}
}))
defer srv.Close()
p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client()))
mm, _ := p.MusicModel("musicgen-acestep")
_, err := mm.Generate(context.Background(), musicgen.Request{Prompt: "p"})
if err == nil {
t.Fatal("dot-dot result file URL accepted")
}
}