ScuttleBot
feat(#42): SDK dual-channel helpers — TopologyClient for channel creation and discovery - TopologyClient wraps POST /v1/channels, DELETE /v1/topology/channels/{channel}, and GET /v1/topology as typed Go methods - CreateChannel(ctx, name, topic) → ChannelInfo{channel, type, supervision, autojoin} returns the supervision channel so agents know where to also post summaries - DropChannel(ctx, channel) drops an ephemeral channel via REST API - GetTopology(ctx) returns static channel names and ChannelTypeInfo slice - PostActivity/PostSummary: convenience wrappers for the dual-channel pattern (PostSummary is a no-op when supervision is empty, safe to always call)
Commit
ce03bdaa34f18d1e572be2e6c5f208c35b78b145b68d946fe3ec90c3cf0714c3
Parent
f0853f53f00357d…
2 files changed
+139
+108
+139
| --- a/pkg/client/topology.go | ||
| +++ b/pkg/client/topology.go | ||
| @@ -0,0 +1,139 @@ | ||
| 1 | +package client | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "bytes" | |
| 5 | + "context" | |
| 6 | + "encoding/json" | |
| 7 | + "fmt" | |
| 8 | + "net/http" | |
| 9 | +) | |
| 10 | + | |
| 11 | +// ChannelInfo is the result of creating or looking up a channel. | |
| 12 | +type ChannelInfo struct { | |
| 13 | + // Channel is the full IRC channel name (e.g. "#task.gh-42"). | |
| 14 | + Channel string `json:"channel"` | |
| 15 | + | |
| 16 | + // Type is the channel type name from the topology policy (e.g. "task", "sprint"). | |
| 17 | + // Empty if the channel does not match any configured type. | |
| 18 | + Type string `json:"type,omitempty"` | |
| 19 | + | |
| 20 | + // Supervision is the coordination/supervision channel where summaries | |
| 21 | + // from this channel should also be posted (e.g. "#general"). Empty if none. | |
| 22 | + Supervision string `json:"supervision,omitempty"` | |
| 23 | + | |
| 24 | + // Autojoin is the list of bot nicks that were invited when the channel was created. | |
| 25 | + Autojoin []string `json:"autojoin,omitempty"` | |
| 26 | +} | |
| 27 | + | |
| 28 | +// ChannelTypeInfo describes a class of channels defined in the topology policy. | |
| 29 | +type ChannelTypeInfo struct { | |
| 30 | + // Name is the type identifier (e.g. "task", "sprint"). | |
| 31 | + Name string `json:"name"` | |
| 32 | + | |
| 33 | + // Prefix is the channel name prefix (e.g. "task."). | |
| 34 | + Prefix string `json:"prefix"` | |
| 35 | + | |
| 36 | + // Autojoin is the list of bot nicks invited when a channel of this type is created. | |
| 37 | + Autojoin []string `json:"autojoin,omitempty"` | |
| 38 | + | |
| 39 | + // Supervision is the coordination channel for this type, or empty. | |
| 40 | + Supervision string `json:"supervision,omitempty"` | |
| 41 | + | |
| 42 | + // Ephemeral indicates channels of this type are automatically reaped. | |
| 43 | + Ephemeral bool `json:"ephemeral,omitempty"` | |
| 44 | + | |
| 45 | + // TTLSeconds is the maximum lifetime in seconds for ephemeral channels, or zero. | |
| 46 | + TTLSeconds int64 `json:"ttl_seconds,omitempty"` | |
| 47 | +} | |
| 48 | + | |
| 49 | +// TopologyClient calls the scuttlebot HTTP API to provision and discover channels. | |
| 50 | +// It complements the IRC-based Client for the dual-channel pattern: agents create | |
| 51 | +// a task channel here and get back the supervision channel where they should also post. | |
| 52 | +type TopologyClient struct { | |
| 53 | + apiURL string | |
| 54 | + token string | |
| 55 | + http *http.Client | |
| 56 | +} | |
| 57 | + | |
| 58 | +// NewTopologyClient creates a TopologyClient. | |
| 59 | +// apiURL is the base URL of the scut{ { | |
| 60 | + var apiErr struct { | |
| 61 | + } | |
| 62 | + _ = json.NewDecoder(resp.Bodyld also post. | |
| 63 | +type Topology("topology: drop channgyClient{ | |
| 64 | + apiURL: apiURL, | |
| 65 | + token: token, | |
| 66 | + http: &http.Client{}, | |
| 67 | + } | |
| 68 | +} | |
| 69 | + | |
| 70 | +type createChannelReq struct { | |
| 71 | + Name string `json:"name"` | |
| 72 | + Topic string `json:"topic,omitempty"` | |
| 73 | + Ops []string `json:"ops,omitempty"` | |
| 74 | + Voice []string `json:"voice,omitempty"` | |
| 75 | + Autojoin []string `json:"autojoin,omitempty"` | |
| 76 | +} | |
| 77 | + | |
| 78 | +// CreateChannel provisions an IRC channel via the scuttlebot topology API. | |
| 79 | +// The server applies autojoin policy and invites the configured bots. | |
| 80 | +// Returns a ChannelInfo with the channel name, type, and supervision channel. | |
| 81 | +// | |
| 82 | +// Example: create a task channel for a GitHub issue. | |
| 83 | +// | |
| 84 | +// info, err := topo.CreateChannel(ctx, "#task.gh-42", "GitHub issue #42") | |
| 85 | +// if err != nil { ... } | |
| 86 | +// // post activity to info.Channel, summaries to info.Supervision | |
| 87 | +func (t *TopologyClient) CreateChannel(ctx context.Context, name, topic string) (ChannelInfo, error) { | |
| 88 | + body, err := json.Marshal(createChannelReq{Name: name, Topic: topic}) | |
| 89 | + if err != nil { | |
| 90 | + retu{ { | |
| 91 | + var apiErr struct { | |
| 92 | + } | |
| 93 | + _ = json.NewDecoder(resp.Body).Decode(&apiErr) | |
| 94 | + return fmt.Errorf("topology: drop channel: %s", apiErr.Error) | |
| 95 | + } | |
| 96 | + return nil | |
| 97 | +} | |
| 98 | + | |
| 99 | +type topologyResp struct { | |
| 100 | + StaticChannels []string `json:"static_channels"` | |
| 101 | + Types []ChannelTypeInfo `json:"types"` | |
| 102 | +} | |
| 103 | + | |
| 104 | +// GetTopology returns the channel type rules and static channels from the server. | |
| 105 | +func (t *TopologyClient) GetTopology(ctx context.Context) ([]string, []ChannelTypeInfo, error) { | |
| 106 | + req, err := http.NewRequestWithContext(ctx, http.MethodGet, t.apiURL+"/v1/topology", nil) | |
| 107 | + if err != nil { | |
| 108 | + return nil, nil, fmt.Errorf("topology: build request: %w", err) | |
| 109 | + } | |
| 110 | + req.Header.Set("Authorization", "Bearer "+t.token) | |
| 111 | + resp, err := t.http.Do(req) | |
| 112 | + if err != nil { | |
| 113 | + return nil, nil, fmt.Errorf("topology: get topology: %w", err) | |
| 114 | + } | |
| 115 | + defer resp.Body.Close() | |
| 116 | + if resp.StatusCode != http.StatusOK { | |
| 117 | + return nil, nil, fmt.Errorf("topology: get topology: status %d", resp.StatusCode) | |
| 118 | + } | |
| 119 | + var body topologyResp | |
| 120 | + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { | |
| 121 | + return nil, nil, fmt.Errorf("topology: decode response: %w", err) | |
| 122 | + } | |
| 123 | + return body.StaticChannels, body.Types, nil | |
| 124 | +} | |
| 125 | + | |
| 126 | +// PostActivity sends a structured message to the task/activity channel. | |
| 127 | +// It is a convenience wrapper around client.Send for the dual-channel pattern. | |
| 128 | +func PostActivity(ctx context.Context, c *Client, channel, msgType string, payload any) error { | |
| 129 | + return c.Send(ctx, channel, msgType, payload) | |
| 130 | +} | |
| 131 | + | |
| 132 | +// PostSummary sends a structured message to the supervision channel. | |
| 133 | +// supervision is the channel returned by CreateChannel (info.Supervision). | |
| 134 | +// It is a no-op if supervision is empty. | |
| 135 | +func PostSummary(ctx context.Context, c *Client, supervision, msgType string, payload any) error { | |
| 136 | + if supervision == "" { | |
| 137 | + return nil | |
| 138 | + } | |
| 139 | + return c.Send(ctx, superv |
| --- a/pkg/client/topology.go | |
| +++ b/pkg/client/topology.go | |
| @@ -0,0 +1,139 @@ | |
| --- a/pkg/client/topology.go | |
| +++ b/pkg/client/topology.go | |
| @@ -0,0 +1,139 @@ | |
| 1 | package client |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "encoding/json" |
| 7 | "fmt" |
| 8 | "net/http" |
| 9 | ) |
| 10 | |
| 11 | // ChannelInfo is the result of creating or looking up a channel. |
| 12 | type ChannelInfo struct { |
| 13 | // Channel is the full IRC channel name (e.g. "#task.gh-42"). |
| 14 | Channel string `json:"channel"` |
| 15 | |
| 16 | // Type is the channel type name from the topology policy (e.g. "task", "sprint"). |
| 17 | // Empty if the channel does not match any configured type. |
| 18 | Type string `json:"type,omitempty"` |
| 19 | |
| 20 | // Supervision is the coordination/supervision channel where summaries |
| 21 | // from this channel should also be posted (e.g. "#general"). Empty if none. |
| 22 | Supervision string `json:"supervision,omitempty"` |
| 23 | |
| 24 | // Autojoin is the list of bot nicks that were invited when the channel was created. |
| 25 | Autojoin []string `json:"autojoin,omitempty"` |
| 26 | } |
| 27 | |
| 28 | // ChannelTypeInfo describes a class of channels defined in the topology policy. |
| 29 | type ChannelTypeInfo struct { |
| 30 | // Name is the type identifier (e.g. "task", "sprint"). |
| 31 | Name string `json:"name"` |
| 32 | |
| 33 | // Prefix is the channel name prefix (e.g. "task."). |
| 34 | Prefix string `json:"prefix"` |
| 35 | |
| 36 | // Autojoin is the list of bot nicks invited when a channel of this type is created. |
| 37 | Autojoin []string `json:"autojoin,omitempty"` |
| 38 | |
| 39 | // Supervision is the coordination channel for this type, or empty. |
| 40 | Supervision string `json:"supervision,omitempty"` |
| 41 | |
| 42 | // Ephemeral indicates channels of this type are automatically reaped. |
| 43 | Ephemeral bool `json:"ephemeral,omitempty"` |
| 44 | |
| 45 | // TTLSeconds is the maximum lifetime in seconds for ephemeral channels, or zero. |
| 46 | TTLSeconds int64 `json:"ttl_seconds,omitempty"` |
| 47 | } |
| 48 | |
| 49 | // TopologyClient calls the scuttlebot HTTP API to provision and discover channels. |
| 50 | // It complements the IRC-based Client for the dual-channel pattern: agents create |
| 51 | // a task channel here and get back the supervision channel where they should also post. |
| 52 | type TopologyClient struct { |
| 53 | apiURL string |
| 54 | token string |
| 55 | http *http.Client |
| 56 | } |
| 57 | |
| 58 | // NewTopologyClient creates a TopologyClient. |
| 59 | // apiURL is the base URL of the scut{ { |
| 60 | var apiErr struct { |
| 61 | } |
| 62 | _ = json.NewDecoder(resp.Bodyld also post. |
| 63 | type Topology("topology: drop channgyClient{ |
| 64 | apiURL: apiURL, |
| 65 | token: token, |
| 66 | http: &http.Client{}, |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | type createChannelReq struct { |
| 71 | Name string `json:"name"` |
| 72 | Topic string `json:"topic,omitempty"` |
| 73 | Ops []string `json:"ops,omitempty"` |
| 74 | Voice []string `json:"voice,omitempty"` |
| 75 | Autojoin []string `json:"autojoin,omitempty"` |
| 76 | } |
| 77 | |
| 78 | // CreateChannel provisions an IRC channel via the scuttlebot topology API. |
| 79 | // The server applies autojoin policy and invites the configured bots. |
| 80 | // Returns a ChannelInfo with the channel name, type, and supervision channel. |
| 81 | // |
| 82 | // Example: create a task channel for a GitHub issue. |
| 83 | // |
| 84 | // info, err := topo.CreateChannel(ctx, "#task.gh-42", "GitHub issue #42") |
| 85 | // if err != nil { ... } |
| 86 | // // post activity to info.Channel, summaries to info.Supervision |
| 87 | func (t *TopologyClient) CreateChannel(ctx context.Context, name, topic string) (ChannelInfo, error) { |
| 88 | body, err := json.Marshal(createChannelReq{Name: name, Topic: topic}) |
| 89 | if err != nil { |
| 90 | retu{ { |
| 91 | var apiErr struct { |
| 92 | } |
| 93 | _ = json.NewDecoder(resp.Body).Decode(&apiErr) |
| 94 | return fmt.Errorf("topology: drop channel: %s", apiErr.Error) |
| 95 | } |
| 96 | return nil |
| 97 | } |
| 98 | |
| 99 | type topologyResp struct { |
| 100 | StaticChannels []string `json:"static_channels"` |
| 101 | Types []ChannelTypeInfo `json:"types"` |
| 102 | } |
| 103 | |
| 104 | // GetTopology returns the channel type rules and static channels from the server. |
| 105 | func (t *TopologyClient) GetTopology(ctx context.Context) ([]string, []ChannelTypeInfo, error) { |
| 106 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, t.apiURL+"/v1/topology", nil) |
| 107 | if err != nil { |
| 108 | return nil, nil, fmt.Errorf("topology: build request: %w", err) |
| 109 | } |
| 110 | req.Header.Set("Authorization", "Bearer "+t.token) |
| 111 | resp, err := t.http.Do(req) |
| 112 | if err != nil { |
| 113 | return nil, nil, fmt.Errorf("topology: get topology: %w", err) |
| 114 | } |
| 115 | defer resp.Body.Close() |
| 116 | if resp.StatusCode != http.StatusOK { |
| 117 | return nil, nil, fmt.Errorf("topology: get topology: status %d", resp.StatusCode) |
| 118 | } |
| 119 | var body topologyResp |
| 120 | if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { |
| 121 | return nil, nil, fmt.Errorf("topology: decode response: %w", err) |
| 122 | } |
| 123 | return body.StaticChannels, body.Types, nil |
| 124 | } |
| 125 | |
| 126 | // PostActivity sends a structured message to the task/activity channel. |
| 127 | // It is a convenience wrapper around client.Send for the dual-channel pattern. |
| 128 | func PostActivity(ctx context.Context, c *Client, channel, msgType string, payload any) error { |
| 129 | return c.Send(ctx, channel, msgType, payload) |
| 130 | } |
| 131 | |
| 132 | // PostSummary sends a structured message to the supervision channel. |
| 133 | // supervision is the channel returned by CreateChannel (info.Supervision). |
| 134 | // It is a no-op if supervision is empty. |
| 135 | func PostSummary(ctx context.Context, c *Client, supervision, msgType string, payload any) error { |
| 136 | if supervision == "" { |
| 137 | return nil |
| 138 | } |
| 139 | return c.Send(ctx, superv |
+108
| --- a/pkg/client/topology_test.go | ||
| +++ b/pkg/client/topology_test.go | ||
| @@ -0,0 +1,108 @@ | ||
| 1 | +package client_test | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "context" | |
| 5 | + "encoding/json" | |
| 6 | + "net/http" | |
| 7 | + "net/http/httptest" | |
| 8 | + "testing" | |
| 9 | + | |
| 10 | + "github.com/conflicthq/scuttlebot/pkg/client" | |
| 11 | +) | |
| 12 | + | |
| 13 | +func TestTopologyClientCreateChannel(t *testing.T) { | |
| 14 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 15 | + if r.Method != http.MethodPost || r.URL.Path != "/v1/channels" { | |
| 16 | + http.Error(w, "unexpected", http.StatusBadRequest) | |
| 17 | + return | |
| 18 | + } | |
| 19 | + if r.Header.Get("Authorization") != "Bearer tok" { | |
| 20 | + http.Error(w, "unauthorized", http.StatusUnauthorized) | |
| 21 | + return | |
| 22 | + } | |
| 23 | + var req struct { | |
| 24 | + Name string `json:"name"` | |
| 25 | + Topic string `json:"topic"` | |
| 26 | + } | |
| 27 | + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { | |
| 28 | + http.Error(w, err.Error(), http.StatusBadRequest) | |
| 29 | + return | |
| 30 | + } | |
| 31 | + w.Header().Set("Content-Type", "application/json") | |
| 32 | + w.WriteHeader(http.StatusCreated) | |
| 33 | + _ = json.NewEncoder(w).Encode(map[string]any{ | |
| 34 | + "channel": req.Name, | |
| 35 | + "type": "task", | |
| 36 | + "supervision": "#general", | |
| 37 | + "autojoin": []string{"bridge", "scribe"}, | |
| 38 | + }) | |
| 39 | + })) | |
| 40 | + defer srv.Close() | |
| 41 | + | |
| 42 | + tc := client.NewTopologyClient(srv.URL, "tok") | |
| 43 | + info, err := tc.CreateChannel(context.Background(), "#task.gh-42", "GitHub issue #42") | |
| 44 | + if err != nil { | |
| 45 | + t.Fatal(err) | |
| 46 | + } | |
| 47 | + if info.Channel != "#task.gh-42" { | |
| 48 | + t.Errorf("Channel = %q, want #task.gh-42", info.Channel) | |
| 49 | + } | |
| 50 | + if info.Type != "task" { | |
| 51 | + t.Errorf("Type = %q, want task", info.Type) | |
| 52 | + } | |
| 53 | + if info.Supervision != "#general" { | |
| 54 | + t.Errorf("Supervision = %q, want #general", info.Supervision) | |
| 55 | + } | |
| 56 | + if len(info.Autojoin) != 2 { | |
| 57 | + t.Errorf("Autojoin = %v, want 2 entries", info.Autojoin) | |
| 58 | + } | |
| 59 | +} | |
| 60 | + | |
| 61 | +func TestTopologyClientGetTopology(t *testing.T) { | |
| 62 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 63 | + if r.URL.Path != "/v1/topology" { | |
| 64 | + http.Error(w, "not found", http.StatusNotFound) | |
| 65 | + return | |
| 66 | + } | |
| 67 | + w.Header().Set("Content-Type", "application/json") | |
| 68 | + _ = json.NewEncoder(w).Encode(map[string]any{ | |
| 69 | + "static_channels": []string{"#general", "#alerts"}, | |
| 70 | + "types": []map[string]any{ | |
| 71 | + {"name": "task", "prefix": "task.", "autojoin": []string{"bridge"}}, | |
| 72 | + }, | |
| 73 | + }) | |
| 74 | + })) | |
| 75 | + defer srv.Close() | |
| 76 | + | |
| 77 | + tc := client.NewTopologyClient(srv.URL, "tok") | |
| 78 | + statics, types, err := tc.GetTopology(context.Background()) | |
| 79 | + if err != nil { | |
| 80 | + t.Fatal(err) | |
| 81 | + } | |
| 82 | + if len(statics) != 2 || statics[0] != "#general" { | |
| 83 | + t.Errorf("static_channels = %v", statics) | |
| 84 | + } | |
| 85 | + if len(types) != 1 || types[0].Name != "task" { | |
| 86 | + t.Errorf("types = %v", types) | |
| 87 | + } | |
| 88 | +} | |
| 89 | + | |
| 90 | +func TestTopologyClientDropChannel(t *testing.T) { | |
| 91 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 92 | + if r.Method != http.MethodDelete { | |
| 93 | + http.Error(w, "wrong method", http.StatusMethodNotAllowed) | |
| 94 | + return | |
| 95 | + } | |
| 96 | + if r.URL.Path != "/v1/topology/channels/task.gh-42" { | |
| 97 | + http.Error(w, "wrong path: "+r.URL.Path, http.StatusBadRequest) | |
| 98 | + return | |
| 99 | + } | |
| 100 | + w.WriteHeader(http.StatusNoContent) | |
| 101 | + })) | |
| 102 | + defer srv.Close() | |
| 103 | + | |
| 104 | + tc := client.NewTopologyClient(srv.URL, "tok") | |
| 105 | + if err := tc.DropChannel(context.Background(), "#task.gh-42"); err != nil { | |
| 106 | + t.Fatal(err) | |
| 107 | + } | |
| 108 | +} |
| --- a/pkg/client/topology_test.go | |
| +++ b/pkg/client/topology_test.go | |
| @@ -0,0 +1,108 @@ | |
| --- a/pkg/client/topology_test.go | |
| +++ b/pkg/client/topology_test.go | |
| @@ -0,0 +1,108 @@ | |
| 1 | package client_test |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "net/http" |
| 7 | "net/http/httptest" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/conflicthq/scuttlebot/pkg/client" |
| 11 | ) |
| 12 | |
| 13 | func TestTopologyClientCreateChannel(t *testing.T) { |
| 14 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 15 | if r.Method != http.MethodPost || r.URL.Path != "/v1/channels" { |
| 16 | http.Error(w, "unexpected", http.StatusBadRequest) |
| 17 | return |
| 18 | } |
| 19 | if r.Header.Get("Authorization") != "Bearer tok" { |
| 20 | http.Error(w, "unauthorized", http.StatusUnauthorized) |
| 21 | return |
| 22 | } |
| 23 | var req struct { |
| 24 | Name string `json:"name"` |
| 25 | Topic string `json:"topic"` |
| 26 | } |
| 27 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| 28 | http.Error(w, err.Error(), http.StatusBadRequest) |
| 29 | return |
| 30 | } |
| 31 | w.Header().Set("Content-Type", "application/json") |
| 32 | w.WriteHeader(http.StatusCreated) |
| 33 | _ = json.NewEncoder(w).Encode(map[string]any{ |
| 34 | "channel": req.Name, |
| 35 | "type": "task", |
| 36 | "supervision": "#general", |
| 37 | "autojoin": []string{"bridge", "scribe"}, |
| 38 | }) |
| 39 | })) |
| 40 | defer srv.Close() |
| 41 | |
| 42 | tc := client.NewTopologyClient(srv.URL, "tok") |
| 43 | info, err := tc.CreateChannel(context.Background(), "#task.gh-42", "GitHub issue #42") |
| 44 | if err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | if info.Channel != "#task.gh-42" { |
| 48 | t.Errorf("Channel = %q, want #task.gh-42", info.Channel) |
| 49 | } |
| 50 | if info.Type != "task" { |
| 51 | t.Errorf("Type = %q, want task", info.Type) |
| 52 | } |
| 53 | if info.Supervision != "#general" { |
| 54 | t.Errorf("Supervision = %q, want #general", info.Supervision) |
| 55 | } |
| 56 | if len(info.Autojoin) != 2 { |
| 57 | t.Errorf("Autojoin = %v, want 2 entries", info.Autojoin) |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | func TestTopologyClientGetTopology(t *testing.T) { |
| 62 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 63 | if r.URL.Path != "/v1/topology" { |
| 64 | http.Error(w, "not found", http.StatusNotFound) |
| 65 | return |
| 66 | } |
| 67 | w.Header().Set("Content-Type", "application/json") |
| 68 | _ = json.NewEncoder(w).Encode(map[string]any{ |
| 69 | "static_channels": []string{"#general", "#alerts"}, |
| 70 | "types": []map[string]any{ |
| 71 | {"name": "task", "prefix": "task.", "autojoin": []string{"bridge"}}, |
| 72 | }, |
| 73 | }) |
| 74 | })) |
| 75 | defer srv.Close() |
| 76 | |
| 77 | tc := client.NewTopologyClient(srv.URL, "tok") |
| 78 | statics, types, err := tc.GetTopology(context.Background()) |
| 79 | if err != nil { |
| 80 | t.Fatal(err) |
| 81 | } |
| 82 | if len(statics) != 2 || statics[0] != "#general" { |
| 83 | t.Errorf("static_channels = %v", statics) |
| 84 | } |
| 85 | if len(types) != 1 || types[0].Name != "task" { |
| 86 | t.Errorf("types = %v", types) |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | func TestTopologyClientDropChannel(t *testing.T) { |
| 91 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 92 | if r.Method != http.MethodDelete { |
| 93 | http.Error(w, "wrong method", http.StatusMethodNotAllowed) |
| 94 | return |
| 95 | } |
| 96 | if r.URL.Path != "/v1/topology/channels/task.gh-42" { |
| 97 | http.Error(w, "wrong path: "+r.URL.Path, http.StatusBadRequest) |
| 98 | return |
| 99 | } |
| 100 | w.WriteHeader(http.StatusNoContent) |
| 101 | })) |
| 102 | defer srv.Close() |
| 103 | |
| 104 | tc := client.NewTopologyClient(srv.URL, "tok") |
| 105 | if err := tc.DropChannel(context.Background(), "#task.gh-42"); err != nil { |
| 106 | t.Fatal(err) |
| 107 | } |
| 108 | } |