package opencode import ( "context" "net/http/httptest" "testing" "time" ) // fakePool создаёт Pool, в котором уже «живёт» сервер для каталога (без spawn): // Server{URL: fake.URL}, поэтому Runner ходит по HTTP на фейк-API. func fakePool(t *testing.T, f *fakeAPIServer, dir string) (*Pool, *Client) { t.Helper() ts := httptestURL(t, f) p := NewPool(dir) p.mu.Lock() p.segs[dir] = &Server{URL: ts, PollInterval: time.Millisecond} p.mu.Unlock() return p, &Client{BaseURL: ts} } // httptestURL запускает фейк-API и возвращает его URL. func httptestURL(t *testing.T, f *fakeAPIServer) string { t.Helper() ts := httptest.NewServer(f.handler()) t.Cleanup(ts.Close) return ts.URL } func TestRun_Success(t *testing.T) { dir := t.TempDir() f := &fakeAPIServer{ verdictParts: []part{{Type: "text", Text: "done"}}, } p, _ := fakePool(t, f, dir) r := &Runner{Pool: p, PollInterval: 5 * time.Millisecond} res, err := r.Run(context.Background(), "task", dir, "dev", "") if err != nil { t.Fatalf("Run err: %v", err) } if res.RC != 0 { t.Errorf("RC = %d, want 0", res.RC) } if res.SessionID != "sess-fake" { t.Errorf("SessionID = %q, want sess-fake", res.SessionID) } if !contains(res.Stdout, "done") { t.Errorf("Stdout = %q, want contain done", res.Stdout) } } func TestRun_IdleTimeout(t *testing.T) { dir := t.TempDir() // prompt блокируется (агент «завис»), прогресс не растёт → idle abort f := &fakeAPIServer{blockPrompt: true} p, _ := fakePool(t, f, dir) r := &Runner{Pool: p, IdleTimeout: 30 * time.Millisecond, PollInterval: 5 * time.Millisecond} res, err := r.Run(context.Background(), "task", dir, "dev", "") if err != nil { t.Fatalf("Run err: %v", err) } if res.RC != -1 { t.Errorf("RC = %d, want -1 (idle timeout)", res.RC) } } func TestRun_ContextCancel(t *testing.T) { dir := t.TempDir() f := &fakeAPIServer{blockPrompt: true} p, _ := fakePool(t, f, dir) ctx, cancel := context.WithCancel(context.Background()) r := &Runner{Pool: p, IdleTimeout: time.Minute, HardTimeout: time.Minute, PollInterval: 5 * time.Millisecond} done := make(chan *Result, 1) errCh := make(chan error, 1) go func() { res, err := r.Run(ctx, "task", dir, "dev", "") done <- res errCh <- err }() time.Sleep(30 * time.Millisecond) cancel() res := <-done if err := <-errCh; err != nil { t.Fatalf("Run err: %v", err) } if res.RC != -1 { t.Errorf("RC = %d, want -1", res.RC) } } // TestResumeDev_Fallback: resume (sessionID) "падает" rc!=0 только когда самого // сервера нет; в фейке такого нет, поэтому проверяем, что при успехе // fallback не срабатывает и таймаут не выставляется. func TestResumeDev_NoFallbackOnSuccess(t *testing.T) { dir := t.TempDir() f := &fakeAPIServer{ verdictParts: []part{{Type: "text", Text: "ok"}}, } p, _ := fakePool(t, f, dir) r := &Runner{Pool: p, PollInterval: 5 * time.Millisecond} res, timedOut := r.ResumeDev(context.Background(), "task", dir, "lost-session") if timedOut { t.Error("timedOut = true, want false (успех не должен считаться таймаутом)") } if res.RC != 0 { t.Errorf("RC = %d, want 0", res.RC) } } func contains(s, sub string) bool { return len(s) >= len(sub) && (s == sub || len(s) > 0 && indexOf(s, sub) >= 0) } func indexOf(s, sub string) int { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return i } } return -1 }