Files
coder/coderd/x/chatd/chattool/planpath_internal_test.go
T
Michael Suchacz a554de372a fix: use per-chat plan file paths (#24268)
> 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.
2026-04-14 10:50:40 +02:00

133 lines
2.8 KiB
Go

package chattool
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestIsAbsolutePath(t *testing.T) {
t.Parallel()
tests := []struct {
path string
want bool
}{
{"/home/coder/PLAN.md", true},
{"/workspace/project/plan.md", true},
{"plan.md", false},
{"./plan.md", false},
{"../plan.md", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.want, isAbsolutePath(tt.path))
})
}
}
func TestLooksLikePlanFileName(t *testing.T) {
t.Parallel()
require.True(t, looksLikePlanFileName("plan.md"))
require.True(t, looksLikePlanFileName("./Plan.md"))
require.True(t, looksLikePlanFileName("/home/coder/PLAN.md"))
require.False(t, looksLikePlanFileName("/home/coder/README.md"))
}
func TestLooksLikeLegacySharedPlanPath(t *testing.T) {
t.Parallel()
tests := []struct {
name string
requested string
want bool
}{
{
name: "ExactMatch",
requested: "/home/coder/PLAN.md",
want: true,
},
{
name: "CaseInsensitive",
requested: "/home/coder/plan.md",
want: true,
},
{
name: "MixedCase",
requested: "/home/coder/Plan.md",
want: true,
},
{
name: "NestedPath",
requested: "/home/coder/myproject/plan.md",
want: false,
},
{
name: "DifferentHome",
requested: "/Users/dev/PLAN.md",
want: false,
},
{
name: "PerChatPath",
requested: "/home/coder/.coder/plans/PLAN-123e4567-e89b-12d3-a456-426614174000.md",
want: false,
},
{
name: "EmptyString",
requested: "",
want: false,
},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, testCase.want, looksLikeLegacySharedPlanPath(testCase.requested))
})
}
}
func TestRejectSharedPlanPath(t *testing.T) {
t.Parallel()
resp, rejected := rejectSharedPlanPath(
LegacySharedPlanPath,
"/Users/dev",
"/Users/dev/.coder/plans/PLAN-chat.md",
nil,
)
require.True(t, rejected)
require.True(t, resp.IsError)
require.Equal(
t,
sharedPlanPathMessage(
LegacySharedPlanPath,
"/Users/dev/.coder/plans/PLAN-chat.md",
),
resp.Content,
)
}
func TestSharedPlanPathMessage(t *testing.T) {
t.Parallel()
require.Equal(
t,
"the plan path /home/coder/plan.md is no longer supported at the home root; use the chat-specific plan path: /home/coder/.coder/plans/PLAN-chat.md",
sharedPlanPathMessage(
"/home/coder/plan.md",
"/home/coder/.coder/plans/PLAN-chat.md",
),
)
require.Equal(
t,
"the plan path /home/coder/plan.md could not be verified because the workspace is currently unavailable to resolve the chat-specific plan path, try again shortly",
planPathVerificationMessage("/home/coder/plan.md"),
)
}