mirror of
https://github.com/coder/coder.git
synced 2026-09-23 05:43:53 +08:00
Fixes CODAGT-447. Alternative implementation of https://github.com/coder/coder/pull/26212 and https://github.com/coder/coder/pull/25978 - Adds up to the first 1000 characters of `README.md` (with leading frontmatter stripped) to `chattool.list_templates` output - Adds up to 800 characters of `README.md` to `chattool.read_template`. **Note:** skipping `toolsdk` versions to keep scope small. > 🤖 Generated by Coder Agents
289 lines
9.2 KiB
Go
289 lines
9.2 KiB
Go
package chattool_test
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"charm.land/fantasy"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
|
"github.com/coder/coder/v2/coderd/x/chatd/chattool"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestReadTemplate_IncludesPresets(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, _ := dbtestutil.NewDB(t)
|
|
user := dbgen.User(t, db, database.User{})
|
|
org := dbgen.Organization(t, db, database.Organization{})
|
|
_ = dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
|
UserID: user.ID,
|
|
OrganizationID: org.ID,
|
|
})
|
|
|
|
tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
|
OrganizationID: org.ID,
|
|
CreatedBy: user.ID,
|
|
})
|
|
tmpl := dbgen.Template(t, db, database.Template{
|
|
OrganizationID: org.ID,
|
|
CreatedBy: user.ID,
|
|
ActiveVersionID: tv.ID,
|
|
})
|
|
|
|
// Create a preset with parameters.
|
|
const usEastLargeDesiredPrebuildInstances = 3
|
|
preset := dbgen.Preset(t, db, database.InsertPresetParams{
|
|
TemplateVersionID: tv.ID,
|
|
Name: "us-east-large",
|
|
IsDefault: true,
|
|
Description: "US East large instance",
|
|
Icon: "/icon/us.png",
|
|
DesiredInstances: sql.NullInt32{
|
|
Int32: usEastLargeDesiredPrebuildInstances,
|
|
Valid: true,
|
|
},
|
|
})
|
|
_ = dbgen.PresetParameter(t, db, database.InsertPresetParametersParams{
|
|
TemplateVersionPresetID: preset.ID,
|
|
Names: []string{"region", "instance_type"},
|
|
Values: []string{"us-east", "large"},
|
|
})
|
|
|
|
// Create a second preset without parameters.
|
|
_ = dbgen.Preset(t, db, database.InsertPresetParams{
|
|
TemplateVersionID: tv.ID,
|
|
Name: "empty-preset",
|
|
})
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
tool := chattool.ReadTemplate(db, org.ID, chattool.ReadTemplateOptions{
|
|
OwnerID: user.ID,
|
|
})
|
|
|
|
resp, err := tool.Run(ctx, fantasy.ToolCall{
|
|
ID: "call-1",
|
|
Name: "read_template",
|
|
Input: `{"template_id":"` + tmpl.ID.String() + `"}`,
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, resp.IsError, "unexpected error: %s", resp.Content)
|
|
|
|
var result map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(resp.Content), &result))
|
|
|
|
// Verify template info is present.
|
|
tmplInfo, ok := result["template"].(map[string]any)
|
|
require.True(t, ok)
|
|
require.Equal(t, tmpl.ID.String(), tmplInfo["id"])
|
|
|
|
// Verify presets are present.
|
|
presetsRaw, ok := result["presets"].([]any)
|
|
require.True(t, ok, "expected presets in response")
|
|
require.Len(t, presetsRaw, 2)
|
|
|
|
// Find the preset with parameters.
|
|
var foundPreset map[string]any
|
|
for _, p := range presetsRaw {
|
|
pm := p.(map[string]any)
|
|
if pm["name"] == "us-east-large" {
|
|
foundPreset = pm
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, foundPreset, "expected to find us-east-large preset")
|
|
require.Equal(t, preset.ID.String(), foundPreset["id"])
|
|
require.Equal(t, true, foundPreset["default"])
|
|
require.Equal(t, "US East large instance", foundPreset["description"])
|
|
require.Equal(t, "/icon/us.png", foundPreset["icon"])
|
|
// Prebuild count round-trips so the LLM can prefer presets
|
|
// backed by prebuilt workspaces.
|
|
require.EqualValues(t, usEastLargeDesiredPrebuildInstances, foundPreset["desired_prebuild_instances"])
|
|
|
|
// Verify preset parameters.
|
|
presetParamsRaw, ok := foundPreset["parameters"].([]any)
|
|
require.True(t, ok)
|
|
require.Len(t, presetParamsRaw, 2)
|
|
|
|
paramMap := make(map[string]string)
|
|
for _, pp := range presetParamsRaw {
|
|
ppm := pp.(map[string]any)
|
|
paramMap[ppm["name"].(string)] = ppm["value"].(string)
|
|
}
|
|
require.Equal(t, "us-east", paramMap["region"])
|
|
require.Equal(t, "large", paramMap["instance_type"])
|
|
|
|
// Verify the empty preset has correct defaults.
|
|
var emptyPreset map[string]any
|
|
for _, p := range presetsRaw {
|
|
pm := p.(map[string]any)
|
|
if pm["name"] == "empty-preset" {
|
|
emptyPreset = pm
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, emptyPreset, "expected to find empty-preset")
|
|
require.Equal(t, false, emptyPreset["default"])
|
|
_, hasDesc := emptyPreset["description"]
|
|
require.False(t, hasDesc, "empty-preset should not have description")
|
|
_, hasIcon := emptyPreset["icon"]
|
|
require.False(t, hasIcon, "empty-preset should not have icon")
|
|
_, hasPrebuilds := emptyPreset["desired_prebuild_instances"]
|
|
require.False(t, hasPrebuilds, "empty-preset should not have desired_prebuild_instances")
|
|
emptyParams, ok := emptyPreset["parameters"].([]any)
|
|
require.True(t, ok)
|
|
require.Empty(t, emptyParams, "empty-preset should have no parameters")
|
|
}
|
|
|
|
func TestReadTemplate_NoPresets(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, _ := dbtestutil.NewDB(t)
|
|
user := dbgen.User(t, db, database.User{})
|
|
org := dbgen.Organization(t, db, database.Organization{})
|
|
_ = dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
|
UserID: user.ID,
|
|
OrganizationID: org.ID,
|
|
})
|
|
|
|
tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
|
OrganizationID: org.ID,
|
|
CreatedBy: user.ID,
|
|
})
|
|
tmpl := dbgen.Template(t, db, database.Template{
|
|
OrganizationID: org.ID,
|
|
CreatedBy: user.ID,
|
|
ActiveVersionID: tv.ID,
|
|
})
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
tool := chattool.ReadTemplate(db, org.ID, chattool.ReadTemplateOptions{
|
|
OwnerID: user.ID,
|
|
})
|
|
|
|
resp, err := tool.Run(ctx, fantasy.ToolCall{
|
|
ID: "call-2",
|
|
Name: "read_template",
|
|
Input: `{"template_id":"` + tmpl.ID.String() + `"}`,
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, resp.IsError)
|
|
|
|
var result map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(resp.Content), &result))
|
|
|
|
// Presets key should be absent when there are no presets.
|
|
_, hasPresets := result["presets"]
|
|
require.False(t, hasPresets, "presets key should be absent when there are none")
|
|
}
|
|
|
|
func TestReadTemplate_Readme(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Seed the database, user, and organization once and reuse them across
|
|
// subtests; each subtest only adds its own template (and version).
|
|
db, _ := dbtestutil.NewDB(t)
|
|
user := dbgen.User(t, db, database.User{})
|
|
org := dbgen.Organization(t, db, database.Organization{})
|
|
_ = dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
|
UserID: user.ID,
|
|
OrganizationID: org.ID,
|
|
})
|
|
|
|
readTemplateInfo := func(t *testing.T, activeVersionID uuid.UUID) map[string]any {
|
|
t.Helper()
|
|
tmpl := dbgen.Template(t, db, database.Template{
|
|
OrganizationID: org.ID,
|
|
CreatedBy: user.ID,
|
|
ActiveVersionID: activeVersionID,
|
|
})
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
tool := chattool.ReadTemplate(db, org.ID, chattool.ReadTemplateOptions{
|
|
OwnerID: user.ID,
|
|
})
|
|
resp, err := tool.Run(ctx, fantasy.ToolCall{
|
|
ID: "call-1",
|
|
Name: "read_template",
|
|
Input: `{"template_id":"` + tmpl.ID.String() + `"}`,
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, resp.IsError, "unexpected error: %s", resp.Content)
|
|
|
|
var result map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(resp.Content), &result))
|
|
tmplInfo, ok := result["template"].(map[string]any)
|
|
require.True(t, ok)
|
|
return tmplInfo
|
|
}
|
|
|
|
readTemplateInfoForReadme := func(t *testing.T, readme string) map[string]any {
|
|
t.Helper()
|
|
tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
|
OrganizationID: org.ID,
|
|
CreatedBy: user.ID,
|
|
Readme: readme,
|
|
})
|
|
return readTemplateInfo(t, tv.ID)
|
|
}
|
|
|
|
t.Run("Surfaced", func(t *testing.T) {
|
|
t.Parallel()
|
|
readme := "---\ndescription: Go template.\n---\n# Title\n\nUse Docker.\n"
|
|
tmplInfo := readTemplateInfoForReadme(t, readme)
|
|
require.Equal(t, "Title\nUse Docker.", tmplInfo["readme"])
|
|
})
|
|
|
|
t.Run("EmptyOmitsField", func(t *testing.T) {
|
|
t.Parallel()
|
|
tmplInfo := readTemplateInfoForReadme(t, " \n\t\n")
|
|
_, ok := tmplInfo["readme"]
|
|
require.False(t, ok, "readme should be omitted when blank")
|
|
})
|
|
|
|
t.Run("NotTruncatedUnderCap", func(t *testing.T) {
|
|
t.Parallel()
|
|
readme := "# Title\n\n" + strings.Repeat("x", 3000)
|
|
tmplInfo := readTemplateInfoForReadme(t, readme)
|
|
require.Equal(t, "Title\n"+strings.Repeat("x", 3000), tmplInfo["readme"])
|
|
})
|
|
|
|
// Images are dropped but code blocks are preserved as text (detail view).
|
|
t.Run("DropsImagesKeepsCode", func(t *testing.T) {
|
|
t.Parallel()
|
|
readme := "# Setup\n\n\n\nRun the installer.\n\n```sh\nmake build\n```\n\nDone.\n"
|
|
tmplInfo := readTemplateInfoForReadme(t, readme)
|
|
require.Equal(t, "Setup\nRun the installer.\nmake build\nDone.", tmplInfo["readme"])
|
|
})
|
|
|
|
// READMEs larger than the cap are truncated with a trailing ellipsis so a
|
|
// single large document cannot dominate the response.
|
|
t.Run("TruncatedOverCap", func(t *testing.T) {
|
|
t.Parallel()
|
|
readme := strings.Repeat("x", 9000)
|
|
tmplInfo := readTemplateInfoForReadme(t, readme)
|
|
got, ok := tmplInfo["readme"].(string)
|
|
require.True(t, ok)
|
|
gotRunes := []rune(got)
|
|
require.Len(t, gotRunes, chattool.ReadTemplateReadmeMaxRunes)
|
|
require.Equal(t, '…', gotRunes[len(gotRunes)-1])
|
|
})
|
|
|
|
// A template whose active version row is missing must not fail
|
|
// read_template; the version fetch is best-effort and readme is simply
|
|
// omitted.
|
|
t.Run("MissingVersionOmitsField", func(t *testing.T) {
|
|
t.Parallel()
|
|
tmplInfo := readTemplateInfo(t, uuid.New())
|
|
_, ok := tmplInfo["readme"]
|
|
require.False(t, ok, "readme should be omitted when the version is missing")
|
|
})
|
|
}
|