Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions internal/setup/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,14 +249,14 @@ func vscodeUserDir() string {
home, _ := userHome()
switch runtimeGOOS {
case "windows":
if appData := os.Getenv("APPDATA"); appData != "" {
if appData := os.Getenv("APPDATA"); appData != "" && filepath.IsAbs(appData) {
return filepath.Join(appData, "Code", "User")
}
return filepath.Join(home, "AppData", "Roaming", "Code", "User")
case "darwin":
return filepath.Join(home, "Library", "Application Support", "Code", "User")
default:
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" && filepath.IsAbs(xdg) {
return filepath.Join(xdg, "Code", "User")
}
return filepath.Join(home, ".config", "Code", "User")
Expand All @@ -278,7 +278,7 @@ func vscodePromptPath() string {

func kilocodeConfigDir() string {
home, _ := userHome()
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" && filepath.IsAbs(xdg) {
return filepath.Join(xdg, "kilo")
}
return filepath.Join(home, ".config", "kilo")
Expand Down
13 changes: 10 additions & 3 deletions internal/setup/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,16 @@ func upsertMarkerBlock(path, begin, end, body string) error {

text := strings.ReplaceAll(string(existing), "\r\n", "\n")
start := strings.Index(text, begin)
stop := strings.Index(text, end)
if start != -1 && stop != -1 && stop > start {
stop += len(end)
// Search for the end marker only AFTER the begin marker, so a stray end
// marker in user content above the managed block can't defeat idempotency
// (which would otherwise append a second block).
stop := -1
if start != -1 {
if rel := strings.Index(text[start:], end); rel != -1 {
stop = start + rel + len(end)
}
}
Comment on lines +204 to +208
if start != -1 && stop != -1 {
text = text[:start] + strings.TrimRight(block, "\n") + text[stop:]
} else {
text = strings.TrimRight(text, "\n") + "\n\n" + block
Expand Down
62 changes: 62 additions & 0 deletions internal/setup/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,41 @@ func TestUpsertMarkerBlockPreservesUserContentAndReplaces(t *testing.T) {
}
}

// TestUpsertMarkerBlockStrayEndMarkerStaysIdempotent guards the anchored end
// search: a stray end marker in user content ABOVE the managed block must not
// defeat idempotency (an unanchored search would find it and append a duplicate).
func TestUpsertMarkerBlockStrayEndMarkerStaysIdempotent(t *testing.T) {
resetSetupSeams(t)
home := useTestHome(t)
path := filepath.Join(home, "notes.md")
seed := "# My notes\n\n" + engramMarkerEnd + "\n\nkeep me\n"
if err := os.WriteFile(path, []byte(seed), 0644); err != nil {
t.Fatalf("seed: %v", err)
}

if err := upsertMarkerBlock(path, engramMarkerBegin, engramMarkerEnd, "BODY ONE"); err != nil {
t.Fatalf("first upsert: %v", err)
}
if err := upsertMarkerBlock(path, engramMarkerBegin, engramMarkerEnd, "BODY TWO"); err != nil {
t.Fatalf("second upsert: %v", err)
}

raw, _ := os.ReadFile(path)
text := string(raw)
if !strings.Contains(text, "keep me") {
t.Errorf("user content not preserved: %q", text)
}
if strings.Contains(text, "BODY ONE") {
t.Errorf("stale managed block not replaced: %q", text)
}
if !strings.Contains(text, "BODY TWO") {
t.Errorf("new managed block missing: %q", text)
}
if n := strings.Count(text, engramMarkerBegin); n != 1 {
t.Errorf("expected exactly 1 managed block despite stray end marker, got %d: %q", n, text)
}
}

func TestInstallDeclarativeAgentMCPWriteError(t *testing.T) {
stubRegistryEnv(t)
writeFileFn = func(string, []byte, os.FileMode) error { return errors.New("disk full") }
Expand Down Expand Up @@ -342,3 +377,30 @@ func TestVSCodeUserDirPerPlatform(t *testing.T) {
}
}
}

// TestConfigDirsIgnoreRelativeConfigHome verifies a relative XDG_CONFIG_HOME /
// APPDATA is ignored (falling back to the absolute home path) instead of
// resolving config under the current working directory.
func TestConfigDirsIgnoreRelativeConfigHome(t *testing.T) {
resetSetupSeams(t)
home := useTestHome(t)

t.Run("relative XDG_CONFIG_HOME ignored", func(t *testing.T) {
runtimeGOOS = "linux"
t.Setenv("XDG_CONFIG_HOME", "relative/xdg")
if got, want := vscodeUserDir(), filepath.Join(home, ".config", "Code", "User"); got != want {
t.Errorf("vscodeUserDir with relative XDG = %q, want %q", got, want)
}
if got, want := kilocodeConfigDir(), filepath.Join(home, ".config", "kilo"); got != want {
t.Errorf("kilocodeConfigDir with relative XDG = %q, want %q", got, want)
}
})

t.Run("relative APPDATA ignored", func(t *testing.T) {
runtimeGOOS = "windows"
t.Setenv("APPDATA", "relative/appdata")
if got, want := vscodeUserDir(), filepath.Join(home, "AppData", "Roaming", "Code", "User"); got != want {
t.Errorf("vscodeUserDir with relative APPDATA = %q, want %q", got, want)
}
})
}