mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
73f6cd8169
Adds suffix-based agent selection for chatd. Template authors can direct chat traffic to a specific root workspace agent by naming it with the `-coderd-chat` suffix (for example, `coder_agent "dev-coderd-chat"`). When no suffix match exists, chatd falls back to the first root agent by `DisplayOrder`, then `Name`. Multiple suffix matches return an error. The selection logic lives in `coderd/x/chatd/internal/agentselect` and is shared by chatd core plus the workspace chat tools so all chat entry points pick the same agent deterministically. No database migrations, API contract changes, or provider changes. The experimental sandbox template was split out to #23777.
232 lines
4.9 KiB
Go
232 lines
4.9 KiB
Go
package agentselect_test
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/x/chatd/internal/agentselect"
|
|
)
|
|
|
|
func TestFindChatAgent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
newRootAgentWithID := func(id, name string, displayOrder int32) database.WorkspaceAgent {
|
|
return database.WorkspaceAgent{
|
|
ID: uuid.MustParse(id),
|
|
Name: name,
|
|
DisplayOrder: displayOrder,
|
|
}
|
|
}
|
|
|
|
newRootAgent := func(name string, displayOrder int32) database.WorkspaceAgent {
|
|
return newRootAgentWithID(uuid.NewString(), name, displayOrder)
|
|
}
|
|
|
|
newChildAgent := func(name string, displayOrder int32) database.WorkspaceAgent {
|
|
agent := newRootAgent(name, displayOrder)
|
|
agent.ParentID = uuid.NullUUID{UUID: uuid.New(), Valid: true}
|
|
return agent
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
agents []database.WorkspaceAgent
|
|
wantIndex int
|
|
wantErrContains []string
|
|
}{
|
|
{
|
|
name: "SingleSuffixMatch",
|
|
agents: []database.WorkspaceAgent{
|
|
newRootAgent("alpha", 0),
|
|
newRootAgent("dev-coderd-chat", 2),
|
|
newRootAgent("zeta", 1),
|
|
},
|
|
wantIndex: 1,
|
|
},
|
|
{
|
|
name: "SuffixMatchCaseInsensitive",
|
|
agents: []database.WorkspaceAgent{
|
|
newRootAgent("alpha", 0),
|
|
newRootAgent("Dev-Coderd-Chat", 2),
|
|
newRootAgent("zeta", 1),
|
|
},
|
|
wantIndex: 1,
|
|
},
|
|
{
|
|
name: "NoSuffixMatchFallbackDeterministic",
|
|
agents: []database.WorkspaceAgent{
|
|
newRootAgent("zeta", 2),
|
|
newRootAgent("bravo", 1),
|
|
newRootAgent("alpha", 1),
|
|
},
|
|
wantIndex: 2,
|
|
},
|
|
{
|
|
name: "NoSuffixMatchFallbackByName",
|
|
agents: []database.WorkspaceAgent{
|
|
newRootAgent("Bravo", 3),
|
|
newRootAgent("alpha", 3),
|
|
newRootAgent("charlie", 3),
|
|
},
|
|
wantIndex: 1,
|
|
},
|
|
{
|
|
name: "CaseOnlyNameTieFallbackDeterministic",
|
|
agents: []database.WorkspaceAgent{
|
|
newRootAgent("Dev", 0),
|
|
newRootAgent("dev", 0),
|
|
},
|
|
wantIndex: 0,
|
|
},
|
|
{
|
|
name: "ExactNameTieFallbackByID",
|
|
agents: []database.WorkspaceAgent{
|
|
newRootAgentWithID("00000000-0000-0000-0000-000000000002", "dev", 0),
|
|
newRootAgentWithID("00000000-0000-0000-0000-000000000001", "dev", 0),
|
|
},
|
|
wantIndex: 1,
|
|
},
|
|
{
|
|
name: "MultipleSuffixMatchesError",
|
|
agents: []database.WorkspaceAgent{
|
|
newRootAgent("alpha-coderd-chat", 2),
|
|
newRootAgent("beta-coderd-chat", 1),
|
|
newRootAgent("gamma", 0),
|
|
},
|
|
wantErrContains: []string{
|
|
fmt.Sprintf(
|
|
"multiple agents match the chat suffix %q",
|
|
agentselect.Suffix,
|
|
),
|
|
"alpha-coderd-chat",
|
|
"beta-coderd-chat",
|
|
"only one agent should use this suffix",
|
|
},
|
|
},
|
|
{
|
|
name: "ChildAgentSuffixIgnored",
|
|
agents: []database.WorkspaceAgent{
|
|
newRootAgent("alpha", 1),
|
|
newChildAgent("child-coderd-chat", 0),
|
|
newRootAgent("bravo", 0),
|
|
},
|
|
wantIndex: 2,
|
|
},
|
|
{
|
|
name: "ChildAgentSuffixIgnoredWithRootMatch",
|
|
agents: []database.WorkspaceAgent{
|
|
newRootAgent("alpha", 0),
|
|
newChildAgent("child-coderd-chat", 1),
|
|
newRootAgent("root-coderd-chat", 2),
|
|
},
|
|
wantIndex: 2,
|
|
},
|
|
{
|
|
name: "EmptyAgentList",
|
|
agents: []database.WorkspaceAgent{},
|
|
wantErrContains: []string{
|
|
"no eligible workspace agents found",
|
|
},
|
|
},
|
|
{
|
|
name: "OnlyChildAgents",
|
|
agents: []database.WorkspaceAgent{
|
|
newChildAgent("alpha", 0),
|
|
newChildAgent("beta-coderd-chat", 1),
|
|
},
|
|
wantErrContains: []string{
|
|
"no eligible workspace agents found",
|
|
},
|
|
},
|
|
{
|
|
name: "SingleRootAgent",
|
|
agents: []database.WorkspaceAgent{
|
|
newRootAgent("solo", 5),
|
|
},
|
|
wantIndex: 0,
|
|
},
|
|
{
|
|
name: "SuffixAgentWinsRegardlessOfOrder",
|
|
agents: []database.WorkspaceAgent{
|
|
newRootAgent("alpha", 0),
|
|
newRootAgent("zeta", 1),
|
|
newRootAgent("preferred-coderd-chat", 99),
|
|
},
|
|
wantIndex: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, err := agentselect.FindChatAgent(tt.agents)
|
|
if len(tt.wantErrContains) > 0 {
|
|
require.Error(t, err)
|
|
for _, wantErr := range tt.wantErrContains {
|
|
require.ErrorContains(t, err, wantErr)
|
|
}
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.agents[tt.wantIndex], got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsChatAgent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "ExactSuffix",
|
|
input: "agent-coderd-chat",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "UppercaseSuffix",
|
|
input: "agent-CODERD-CHAT",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "MixedCaseSuffix",
|
|
input: "agent-Coderd-Chat",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "NoSuffix",
|
|
input: "my-agent",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "SuffixOnly",
|
|
input: "-coderd-chat",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "PartialSuffix",
|
|
input: "agent-coderd",
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.Equal(t, tt.want, agentselect.IsChatAgent(tt.input))
|
|
})
|
|
}
|
|
}
|