package qoderruntime import ( "context" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync/atomic" "testing" "time" "github.com/multica-ai/multica/server/internal/daemon" "github.com/multica-ai/multica/server/internal/testutil" ) const workspaceID = "11111111-1111-4111-8111-111111111111" func TestAPIRecoversBlackholedHTTP2Connection(t *testing.T) { started := make(chan struct{}, 1) var posts atomic.Int32 server, connections := testutil.NewBlackholeHTTP2Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.ProtoMajor != 2 { t.Errorf("expected HTTP/2, got %s", r.Proto) } if r.Method == http.MethodPost { posts.Add(1) started <- struct{}{} <-r.Context().Done() return } _, _ = io.WriteString(w, `{}`) })) a, err := newAPI(server.URL, "test-token", "") if err != nil { t.Fatal(err) } testutil.TrustHTTP2Server(t, a.client, server) a.client.Timeout = 2 * time.Second ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second) defer cancel() if err := a.call(ctx, http.MethodGet, "/warm", nil, nil); err != nil { t.Fatal(err) } conn := <-connections defer conn.Close() done := make(chan error, 1) go func() { done <- a.call(ctx, http.MethodPost, "/sessions", nil, nil) }() select { case <-started: case <-ctx.Done(): t.Fatal("session request did not reach server") } conn.Drop.Store(true) if err := <-done; err == nil { t.Fatal("interrupted request unexpectedly succeeded") } if err := a.call(ctx, http.MethodGet, "/recovered", nil, nil); err != nil { t.Fatalf("Qoder client reused the dead connection: %v", err) } select { case <-connections: default: t.Fatal("Qoder recovery did not open a new connection") } if count := posts.Load(); count != 1 { t.Fatalf("session request was replayed %d times", count) } } type harness struct { b *Bridge status string task *daemon.Task events []map[string]any creates, sends, cancels int lostSend bool complete, failure map[string]any messages []map[string]any prompt string sessionAgent string } func setup(t *testing.T) *harness { t.Helper() h := &harness{status: "dispatched"} h.task = &daemon.Task{ID: "task-1", RuntimeID: "runtime-1", WorkspaceID: workspaceID, IssueID: "issue-1", AuthToken: "mat_task", Agent: &daemon.AgentData{Instructions: "Run tests", RuntimeConfig: json.RawMessage(`{"qoder_agent_id":"agent_1"}`), Skills: []daemon.SkillData{{Name: "multica-platform"}}}} m := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.Header.Get("X-Workspace-ID") != workspaceID { t.Error("missing workspace") } auth := "Bearer multica-secret" if strings.HasPrefix(r.URL.Path, "/api/issues/") { auth = "Bearer mat_task" } if r.Header.Get("Authorization") != auth { t.Error("wrong credential scope") } if r.Header.Get("X-Client-Capabilities") != "" { t.Error("unsupported capabilities advertised") } switch { case strings.HasSuffix(r.URL.Path, "/tasks/claim"): json.NewEncoder(w).Encode(map[string]any{"task": h.task}) h.task = nil case strings.HasPrefix(r.URL.Path, "/api/issues/"): io.WriteString(w, `{"title":"Fix bug","description":"Use repository context"}`) case strings.HasSuffix(r.URL.Path, "/status"): json.NewEncoder(w).Encode(map[string]string{"status": h.status}) case strings.HasSuffix(r.URL.Path, "/start"): h.status = "running" io.WriteString(w, `{}`) case strings.HasSuffix(r.URL.Path, "/complete"): json.NewDecoder(r.Body).Decode(&h.complete) h.status = "completed" io.WriteString(w, `{}`) case strings.HasSuffix(r.URL.Path, "/fail"): json.NewDecoder(r.Body).Decode(&h.failure) h.status = "failed" io.WriteString(w, `{}`) case strings.HasSuffix(r.URL.Path, "/messages"): var req struct { Messages []map[string]any `json:"messages"` } json.NewDecoder(r.Body).Decode(&req) h.messages = append(h.messages, req.Messages...) io.WriteString(w, `{}`) case strings.HasSuffix(r.URL.Path, "/session"), strings.HasSuffix(r.URL.Path, "/cancel-ack"): io.WriteString(w, `{}`) default: t.Errorf("unexpected Multica request %s", r.URL) http.NotFound(w, r) } })) t.Cleanup(m.Close) q := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.Header.Get("Authorization") != "Bearer qoder-secret" { t.Error("wrong Qoder credential") } switch { case r.URL.Path == "/sessions" && r.Method == "POST": h.creates++ var input struct { Agent string `json:"agent"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { t.Fatal(err) } h.sessionAgent = input.Agent io.WriteString(w, `{"id":"sess_1","status":"idle"}`) case r.URL.Path == "/sessions/sess_1/events" && r.Method == "POST": h.sends++ var req struct { Events []event `json:"events"` } json.NewDecoder(r.Body).Decode(&req) h.prompt = req.Events[0].text() h.events = append(h.events, map[string]any{"id": "evt_user", "type": "user.message", "content": []map[string]string{{"type": "text", "text": h.prompt}}}) if h.lostSend { http.Error(w, "unknown", 502) return } io.WriteString(w, `{"data":[]}`) case r.URL.Path == "/sessions/sess_1/events" && r.Method == "GET": if r.URL.Query().Get("page") != "" { t.Error("event ID used as page cursor") } events := h.events after := r.URL.Query().Get("after_id") if after != "" { for i, e := range events { if e["id"] == after { events = events[i+1:] break } } } json.NewEncoder(w).Encode(map[string]any{"data": events, "has_more": false}) case strings.HasSuffix(r.URL.Path, "/cancel"): h.cancels++ io.WriteString(w, `{"status":"canceling"}`) case r.URL.Path == "/sessions/sess_1": io.WriteString(w, `{"status":"idle"}`) default: t.Errorf("unexpected Qoder request %s", r.URL) http.NotFound(w, r) } })) t.Cleanup(q.Close) var err error h.b, err = New(Config{MulticaURL: m.URL, MulticaToken: "multica-secret", WorkspaceID: workspaceID, QoderURL: q.URL, QoderToken: "qoder-secret", EnvironmentID: "env_1", StateDir: t.TempDir()}, nil) if err != nil { t.Fatal(err) } h.b.state = &journal{Binding: "test", RuntimeID: "runtime-1"} return h } func (h *harness) finish(reason string) { h.events = append(h.events, map[string]any{"id": "evt_reply", "type": "agent.message", "content": []map[string]string{{"type": "text", "text": "Tests passed. PR: https://example.com/pr/1"}}}, map[string]any{"id": "evt_idle", "type": "session.status_idle", "stop_reason": map[string]string{"type": reason}}) } func TestIssueRun(t *testing.T) { h := setup(t) ctx := context.Background() h.events = []map[string]any{{"id": "evt_initial", "type": "session.status_idle", "stop_reason": map[string]string{"type": "end_turn"}}} if err := h.b.step(ctx); err != nil { t.Fatal(err) } if h.complete != nil { t.Fatal("completed before user turn") } if !strings.Contains(h.prompt, "Fix bug") || strings.Contains(h.prompt, "secret") || strings.Contains(h.prompt, "mat_task") { t.Fatalf("incorrect prompt: %s", h.prompt) } h.finish("end_turn") if err := h.b.step(ctx); err != nil { t.Fatal(err) } if h.complete["session_id"] != "sess_1" || !strings.Contains(fmt.Sprint(h.complete["output"]), "Tests passed") { t.Fatalf("result: %#v", h.complete) } if h.creates != 1 || h.sends != 1 || len(h.messages) != 1 || h.b.state.first() != nil { t.Fatal("incorrect lifecycle") } data, err := os.ReadFile(h.b.path) if err != nil { t.Fatal(err) } if strings.Contains(string(data), "secret") { t.Fatal("credential persisted") } } func TestChatRun(t *testing.T) { h := setup(t) ctx := context.Background() h.task.IssueID = "" h.task.ChatSessionID = "chat-1" h.task.ChatMessage = "Introduce yourself" if err := h.b.step(ctx); err != nil { t.Fatal(err) } if h.failure != nil || !strings.Contains(h.prompt, "Introduce yourself") || !strings.Contains(h.prompt, "Audience: direct room.") || strings.Contains(h.prompt, "pull request") { t.Fatalf("incorrect chat prompt: %s (failure %#v)", h.prompt, h.failure) } h.finish("end_turn") if err := h.b.step(ctx); err != nil { t.Fatal(err) } if h.complete["session_id"] != "sess_1" || !strings.Contains(fmt.Sprint(h.complete["output"]), "Tests passed") { t.Fatalf("result: %#v", h.complete) } } func TestChatAudience(t *testing.T) { for _, tc := range []struct{ channel, chatType, want string }{ {"", "", "Audience: direct room."}, {"slack", "p2p", "Audience: direct room."}, {"slack", "group", "Audience: group room; not private"}, {"feishu", "", "Audience: unknown."}, } { if got := chatAudience(tc.channel, tc.chatType); !strings.Contains(got, tc.want) { t.Errorf("chatAudience(%q, %q) = %q, want %q", tc.channel, tc.chatType, got, tc.want) } } } func TestChatAttachmentsFailBeforeExecution(t *testing.T) { h := setup(t) h.task.IssueID = "" h.task.ChatSessionID = "chat-1" h.task.ChatMessageAttachments = []daemon.ChatAttachmentMeta{{ID: "att-1"}} if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.creates != 0 || !strings.Contains(fmt.Sprint(h.failure["error"]), "attachments") { t.Fatalf("chat attachment ignored: %#v", h.failure) } } func TestAmbiguousSubmitReconcilesAfterRestart(t *testing.T) { h := setup(t) h.lostSend = true if err := h.b.step(context.Background()); err == nil { t.Fatal("expected ambiguous failure") } j, err := readJournal(h.b.path, "test") if err != nil { t.Fatal(err) } h.b.state = j if j.first().Phase != "sending" || j.first().SessionID != "sess_1" { t.Fatal("missing intent") } h.finish("end_turn") if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.sends != 1 || h.creates != 1 || h.complete == nil { t.Fatal("restart duplicated/lost execution") } } func TestUnconfirmedSubmissionDoesNotResend(t *testing.T) { h := setup(t) h.lostSend = true _ = h.b.step(context.Background()) h.events = nil for range 2 { if err := h.b.step(context.Background()); err == nil { t.Fatal("expected reconciliation warning") } } if h.sends != 1 { t.Fatal("duplicated submission") } } func TestCancelStopsRemote(t *testing.T) { h := setup(t) if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } h.status = "cancelled" if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.cancels != 1 || h.b.state.first() != nil || h.complete != nil { t.Fatal("incorrect cancellation") } } func TestRequiresActionIsNotSuccess(t *testing.T) { h := setup(t) if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } h.finish("requires_action") if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.complete != nil || !strings.Contains(fmt.Sprint(h.failure["error"]), "requires_action") { t.Fatalf("unexpected report: %#v", h.failure) } } func TestLocalDirectoryFailsBeforeExecution(t *testing.T) { h := setup(t) h.task.ProjectResources = []daemon.ProjectResourceData{{ResourceType: "local_directory"}} if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.creates != 0 || h.failure == nil { t.Fatal("local directory ignored") } } func TestStateLockAndBinding(t *testing.T) { path := filepath.Join(t.TempDir(), "lock") f, err := lockState(path) if err != nil { t.Fatal(err) } if second, err := lockState(path); err == nil { second.Close() t.Fatal("second process acquired lock") } f.Close() f, err = lockState(path) if err != nil { t.Fatal(err) } f.Close() path = filepath.Join(t.TempDir(), "state.json") if err := save(path, &journal{Binding: "a"}); err != nil { t.Fatal(err) } if _, err := readJournal(path, "b"); err == nil { t.Fatal("wrong binding accepted") } } func TestAPIRejectsRedirectsAndRedactsErrors(t *testing.T) { target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Error("followed redirect") })) defer target.Close() source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", target.URL) w.WriteHeader(302) io.WriteString(w, "sensitive-secret") })) defer source.Close() a, err := newAPI(source.URL, "credential", "") if err != nil { t.Fatal(err) } err = a.call(context.Background(), http.MethodGet, "/", nil, nil) if err == nil || strings.Contains(err.Error(), "sensitive-secret") { t.Fatalf("unsafe error: %v", err) } if _, err := newAPI("http://remote.example", "token", ""); err == nil { t.Fatal("plaintext remote URL accepted") } } func TestOpaquePaginationWithEmptyProjectedPage(t *testing.T) { h := setup(t) if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } requests := 0 q := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requests++ if requests == 1 { if r.URL.Query().Get("after_id") != "evt_user" { t.Error("missing event cursor") } io.WriteString(w, `{"data":[],"has_more":true,"next_page":"opaque-cursor"}`) } else { if r.URL.Query().Get("page") != "opaque-cursor" || r.URL.Query().Get("after_id") != "" { t.Error("incorrect page request") } io.WriteString(w, `{"data":[{"id":"evt_done","type":"session.status_idle","stop_reason":{"type":"end_turn"}}],"has_more":false}`) } })) defer q.Close() h.b.qoder.base = q.URL if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } j, err := readJournal(h.b.path, "test") if err != nil { t.Fatal(err) } h.b.state = j if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.complete == nil || requests != 2 { t.Fatal("pagination lost terminal event") } } func TestTerminalReportSurvivesRestart(t *testing.T) { h := setup(t) h.b.state.Runs = []*run{&run{TaskID: "task-1", SessionID: "sess_1", Phase: "final", Outcome: "completed", Output: "Recovered result"}} if err := h.b.persist(); err != nil { t.Fatal(err) } j, err := readJournal(h.b.path, "test") if err != nil { t.Fatal(err) } h.b.state = j if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.complete["output"] != "Recovered result" || h.creates != 0 || h.sends != 0 { t.Fatal("terminal recovery re-executed task") } } func TestRejectedMessageFailsRun(t *testing.T) { h := setup(t) q := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/sessions" { io.WriteString(w, `{"id":"sess_1"}`) return } http.Error(w, "bad message", 400) })) defer q.Close() h.b.qoder.base = q.URL if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.failure == nil || h.b.state.first() != nil { t.Fatal("definite rejection left run pending") } } func TestRegistrationAndShutdown(t *testing.T) { h := setup(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() m := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/workspaces/" + workspaceID: json.NewEncoder(w).Encode(map[string]string{"id": workspaceID}) case "/api/daemon/register": var req struct { Runtimes []struct { Type string `json:"type"` Mode string `json:"runtime_mode"` } `json:"runtimes"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Error(err) } if len(req.Runtimes) != 1 || req.Runtimes[0].Type != Provider || req.Runtimes[0].Mode != "cloud" { t.Error("wrong registration provider") } io.WriteString(w, `{"runtimes":[{"id":"runtime-1","provider":"qoder_cloud"}]}`) case "/api/daemon/heartbeat": io.WriteString(w, `{}`) cancel() default: t.Errorf("unexpected startup request %s", r.URL) } })) defer m.Close() h.b.multica.base = m.URL q := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/agents/agent_1" && r.URL.Path != "/environments/env_1" { t.Error("unexpected remote validation") } id := strings.TrimPrefix(strings.TrimPrefix(r.URL.Path, "/agents/"), "/environments/") json.NewEncoder(w).Encode(map[string]string{"id": id}) })) defer q.Close() h.b.qoder.base = q.URL err := h.b.Run(ctx) if err != nil && ctx.Err() == nil { t.Fatal(err) } } func TestPerAgentSessionBinding(t *testing.T) { for _, id := range []string{"agent_first", "agent_second"} { t.Run(id, func(t *testing.T) { h := setup(t) h.task.Agent.RuntimeConfig = json.RawMessage(fmt.Sprintf(`{"qoder_agent_id":%q}`, id)) if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.sessionAgent != id { t.Fatalf("session agent = %q, want %q", h.sessionAgent, id) } restored, err := readJournal(h.b.path, "test") if err != nil || restored.first().AgentID != id { t.Fatal("per-run agent binding not persisted") } }) } } func TestMissingAgentBindingFailsBeforeSession(t *testing.T) { for _, raw := range []string{`{}`, `{"qoder_agent_id":"../agents"}`, `{"qoder_agent_id":1}`} { h := setup(t) h.task.Agent.RuntimeConfig = json.RawMessage(raw) if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.creates != 0 || h.status != "failed" { t.Fatal("unbound task created a QCA session") } } } func TestRunDeadlineReleasesQueue(t *testing.T) { h := setup(t) if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } h.b.state.first().Deadline = time.Now().Add(-time.Second) if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.failure == nil || h.b.state.first() != nil || h.cancels != 1 { t.Fatal("expired run did not cancel and finish") } } func TestAmbiguousCreationIsNotRepeated(t *testing.T) { h := setup(t) h.b.state.Runs = []*run{&run{TaskID: "task-1", Phase: "creating", Deadline: time.Now().Add(time.Hour)}} if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.creates != 0 || h.failure == nil { t.Fatal("ambiguous creation was repeated") } } func TestTimeoutKeepsFailedCancellationForRecovery(t *testing.T) { h := setup(t) if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(503) })) defer remote.Close() h.b.qoder.base = remote.URL h.b.state.first().Deadline = time.Now().Add(-time.Second) if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.b.state.first() != nil || len(h.b.state.Cleanup) != 1 || h.failure == nil { t.Fatal("failed cancellation blocked queue or lost session") } j, err := readJournal(h.b.path, "test") if err != nil { t.Fatal(err) } if len(j.Cleanup) != 1 || j.Cleanup[0] != "sess_1" { t.Fatal("cleanup not durable") } } func TestDeletedRemoteSessionFailsRun(t *testing.T) { h := setup(t) if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } remote := httptest.NewServer(http.NotFoundHandler()) defer remote.Close() h.b.qoder.base = remote.URL if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.failure == nil || h.b.state.first() != nil { t.Fatal("deleted session not terminal") } } func TestRateLimitedCreationRetriesAfterDelay(t *testing.T) { h := setup(t) attempts := 0 remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { attempts++; w.WriteHeader(http.StatusTooManyRequests) })) defer remote.Close() h.b.qoder.base = remote.URL if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.b.state.first() == nil || h.b.state.first().Phase != "prepared" || h.failure != nil { t.Fatal("rate limit became permanent failure") } if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if attempts != 1 { t.Fatal("rate limit retry ignored backoff") } } func TestSubmissionDeadlineReleasesUnconfirmedRun(t *testing.T) { h := setup(t) h.lostSend = true if err := h.b.step(context.Background()); err == nil { t.Fatal("expected lost response") } h.events = nil h.b.state.first().SubmissionDeadline = time.Now().Add(-time.Second) if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } if h.failure == nil || h.b.state.first() != nil || h.sends != 1 { t.Fatal("unconfirmed submission blocked queue or resent") } } func TestManagedJournalPersistsWithoutLocalFiles(t *testing.T) { h := setup(t) var saved []byte h.b.cfg.SaveState = func(_ context.Context, raw []byte) error { saved = append([]byte(nil), raw...); return nil } h.b.path = "/not/a/writable/path" if err := h.b.step(context.Background()); err != nil { t.Fatal(err) } restored, err := decodeJournal(saved, "test") if err != nil { t.Fatal(err) } if restored.first() == nil || restored.first().SessionID != "sess_1" || restored.first().Deadline.IsZero() { t.Fatal("managed recovery lost session or deadline") } } func TestToolResultsPreserveCallIdentity(t *testing.T) { for _, prefix := range []string{"agent.", "agent.mcp_"} { t.Run(prefix, func(t *testing.T) { h := setup(t) ctx := context.Background() if err := h.b.step(ctx); err != nil { t.Fatal(err) } h.events = append(h.events, map[string]any{"id": "call-a", "type": prefix + "tool_use", "name": "Read"}, map[string]any{"id": "call-b", "type": prefix + "tool_use", "name": "Read"}, map[string]any{"id": "result-b", "type": prefix + "tool_result", "tool_use_id": "call-b"}, map[string]any{"id": "result-a", "type": prefix + "tool_result", "tool_use_id": "call-a"}, ) if err := h.b.step(ctx); err != nil { t.Fatal(err) } if len(h.messages) != 4 { t.Fatalf("got %d messages", len(h.messages)) } for i, want := range []string{"call-a", "call-b", "call-b", "call-a"} { if h.messages[i]["call_id"] != want { t.Errorf("message %d: call_id = %v, want %s", i, h.messages[i]["call_id"], want) } } }) } } // first returns the oldest active run, which single-run tests inspect. func (j *journal) first() *run { if len(j.Runs) == 0 { return nil } return j.Runs[0] } func TestJournalStorageConfig(t *testing.T) { base := Config{MulticaURL: "http://127.0.0.1:1", MulticaToken: "m", WorkspaceID: workspaceID, QoderURL: "http://127.0.0.1:2", QoderToken: "q", EnvironmentID: "env_1"} load := func(context.Context) ([]byte, error) { return nil, nil } save := func(context.Context, []byte) error { return nil } external := base external.LoadState, external.SaveState = load, save if _, err := New(external, nil); err != nil { t.Fatalf("external journal needs no state directory: %v", err) } onDisk := base onDisk.StateDir = t.TempDir() if _, err := New(onDisk, nil); err != nil { t.Fatalf("on-disk journal rejected: %v", err) } for name, cfg := range map[string]Config{ "no journal": base, "load only": func() Config { c := base; c.LoadState = load; c.StateDir = t.TempDir(); return c }(), "save only": func() Config { c := base; c.SaveState = save; c.StateDir = t.TempDir(); return c }(), "no environment": func() Config { c := external; c.EnvironmentID = ""; return c }(), } { if _, err := New(cfg, nil); err == nil { t.Errorf("%s accepted", name) } } }