mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
a554de372a
> This PR was authored by Mux on behalf of Mike. Chats sharing one workspace (e.g. sibling subagents) all wrote to `/home/coder/PLAN.md`, causing plan file collisions. This change derives a unique plan path per chat from the workspace home directory and chat ID. ## Changes * `write_file`, `edit_files`, and `propose_plan` reject any `plan.md` variant (case-insensitive) at the workspace home root, with a clear error pointing to the chat-specific path. * Root chats receive a `<plan-file-path>` block inlined in the main system prompt with the concrete path. * Prompt and tool descriptions no longer hardcode `/home/coder/PLAN.md`. * Plan path handling is POSIX-only (forward-slash), relying on the contract that workspace agent paths are normalized before reaching chatd. * Updated `ProposePlanTool.stories.tsx` to use per-chat path examples. * Full test coverage for plan path detection, legacy-path rejection in all three tools, inline prompt rendering, and fallback behavior.
220 lines
4.8 KiB
Go
220 lines
4.8 KiB
Go
package chattool_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/mock/gomock"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/x/chatd/chattool"
|
|
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
|
"github.com/coder/coder/v2/codersdk/workspacesdk/agentconnmock"
|
|
)
|
|
|
|
func TestResolveWorkspaceHome(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
resp workspacesdk.LSResponse
|
|
lsErr error
|
|
want string
|
|
wantErr bool
|
|
errMatch string
|
|
}{
|
|
{
|
|
name: "StandardLinuxHome",
|
|
resp: workspacesdk.LSResponse{AbsolutePathString: "/home/coder"},
|
|
want: "/home/coder",
|
|
},
|
|
{
|
|
name: "NonStandardHome",
|
|
resp: workspacesdk.LSResponse{AbsolutePathString: "/Users/dev"},
|
|
want: "/Users/dev",
|
|
},
|
|
{
|
|
name: "LSError",
|
|
lsErr: xerrors.New("list failed"),
|
|
wantErr: true,
|
|
errMatch: "list failed",
|
|
},
|
|
{
|
|
name: "EmptyAbsolutePathString",
|
|
resp: workspacesdk.LSResponse{AbsolutePathString: ""},
|
|
wantErr: true,
|
|
errMatch: "workspace home path is empty",
|
|
},
|
|
{
|
|
name: "WhitespaceOnlyAbsolutePathString",
|
|
resp: workspacesdk.LSResponse{AbsolutePathString: " \t\n "},
|
|
wantErr: true,
|
|
errMatch: "workspace home path is empty",
|
|
},
|
|
}
|
|
|
|
for _, testCase := range tests {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctrl := gomock.NewController(t)
|
|
conn := agentconnmock.NewMockAgentConn(ctrl)
|
|
|
|
conn.EXPECT().LS(
|
|
gomock.Any(),
|
|
"",
|
|
workspacesdk.LSRequest{
|
|
Path: []string{},
|
|
Relativity: workspacesdk.LSRelativityHome,
|
|
},
|
|
).Return(testCase.resp, testCase.lsErr)
|
|
|
|
got, err := chattool.ResolveWorkspaceHome(context.Background(), conn)
|
|
if testCase.wantErr {
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, testCase.errMatch)
|
|
require.Empty(t, got)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, testCase.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlanPathForChat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("StandardHome", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
chatID := uuid.MustParse("123e4567-e89b-12d3-a456-426614174000")
|
|
|
|
got := chattool.PlanPathForChat("/home/coder", chatID)
|
|
|
|
require.Equal(
|
|
t,
|
|
"/home/coder/.coder/plans/PLAN-123e4567-e89b-12d3-a456-426614174000.md",
|
|
got,
|
|
)
|
|
})
|
|
|
|
t.Run("NonStandardHome", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
chatID := uuid.MustParse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
|
|
|
|
got := chattool.PlanPathForChat("/Users/dev", chatID)
|
|
|
|
require.Equal(
|
|
t,
|
|
"/Users/dev/.coder/plans/PLAN-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.md",
|
|
got,
|
|
)
|
|
})
|
|
|
|
t.Run("MatchesExpectedFormat", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
home := "/workspace/home"
|
|
chatID := uuid.MustParse("f47ac10b-58cc-4372-a567-0e02b2c3d479")
|
|
|
|
got := chattool.PlanPathForChat(home, chatID)
|
|
|
|
require.True(t, strings.HasPrefix(got, home+"/.coder/plans/PLAN-"))
|
|
require.True(t, strings.HasSuffix(got, chatID.String()+".md"))
|
|
})
|
|
}
|
|
|
|
func TestLooksLikeHomePlanFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
requested string
|
|
home string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "UppercaseHomeRootPlan",
|
|
requested: "/home/coder/PLAN.md",
|
|
home: "/home/coder",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "LowercaseHomeRootPlan",
|
|
requested: "/home/coder/plan.md",
|
|
home: "/home/coder",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "MixedCaseHomeRootPlan",
|
|
requested: "/home/coder/Plan.md",
|
|
home: "/home/coder",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "UppercaseExtension",
|
|
requested: "/home/coder/PLAN.MD",
|
|
home: "/home/coder",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "CustomHomeRootPlan",
|
|
requested: "/Users/dev/plan.md",
|
|
home: "/Users/dev",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "NestedPlanUnderHome",
|
|
requested: "/home/coder/myproject/plan.md",
|
|
home: "/home/coder",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "PerChatPlanPath",
|
|
requested: "/home/coder/.coder/plans/PLAN-123e4567-e89b-12d3-a456-426614174000.md",
|
|
home: "/home/coder",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "DifferentFilename",
|
|
requested: "/home/coder/README.md",
|
|
home: "/home/coder",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "DifferentExtension",
|
|
requested: "/home/coder/plan.txt",
|
|
home: "/home/coder",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "EmptyPath",
|
|
requested: "",
|
|
home: "/home/coder",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "DifferentHomeMismatch",
|
|
requested: "/home/coder/plan.md",
|
|
home: "/Users/dev",
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, testCase := range tests {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := chattool.LooksLikeHomePlanFile(testCase.requested, testCase.home)
|
|
|
|
require.Equal(t, testCase.want, got)
|
|
})
|
|
}
|
|
}
|