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
8 changes: 7 additions & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,13 @@ func (s *Server) handleMigrateProject(w http.ResponseWriter, r *http.Request) {
jsonError(w, http.StatusBadRequest, "old_project and new_project are required")
return
}
if body.OldProject == body.NewProject {
// Normalize both names using the same rules the store applies so that
// case-only differences (e.g. "repo_name" vs "Repo_Name") are treated as
// identical and do not trigger a migration that would create duplicates.
// See: https://github.com/Gentleman-Programming/engram/issues/438
normalizedOld, _ := store.NormalizeProject(body.OldProject)
normalizedNew, _ := store.NormalizeProject(body.NewProject)
if normalizedOld == normalizedNew {
jsonResponse(w, http.StatusOK, map[string]any{"status": "skipped", "reason": "names are identical"})
return
}
Expand Down
44 changes: 44 additions & 0 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1904,3 +1904,47 @@ func TestJudgeAndCompareRoutesValidateInput(t *testing.T) {
t.Fatalf("expected missing observation 404, got %d body=%q", compareRec.Code, compareRec.Body.String())
}
}

// TestMigrateProjectCaseOnlySkipped asserts that POST /projects/migrate
// returns status "skipped" when old_project and new_project differ only by
// case — fixing #438 where the exact-string comparison let case-only renames
// slip through and create duplicate projects.
//
// The test seeds a session under "repo_name" so that the store would actually
// migrate if the server did not guard against case-only differences first.
func TestMigrateProjectCaseOnlySkipped(t *testing.T) {
st := newServerTestStore(t)
h := New(st, 0).Handler()

// Seed a session under the lowercase project name so the store has data
// to migrate; without the fix the handler would call store.MigrateProject
// and rename "repo_name" → "Repo_Name", creating a duplicate.
seedReq := httptest.NewRequest(http.MethodPost, "/sessions", strings.NewReader(
`{"id":"s-case-migrate","project":"repo_name","directory":"/tmp/repo"}`,
))
seedReq.Header.Set("Content-Type", "application/json")
seedRec := httptest.NewRecorder()
h.ServeHTTP(seedRec, seedReq)
if seedRec.Code != http.StatusCreated {
t.Fatalf("seed session: expected 201, got %d body=%s", seedRec.Code, seedRec.Body.String())
}

body := bytes.NewBufferString(`{"old_project":"repo_name","new_project":"Repo_Name"}`)
req := httptest.NewRequest(http.MethodPost, "/projects/migrate", body)
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()

h.ServeHTTP(rec, req)

if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
}

var resp map[string]any
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp["status"] != "skipped" {
t.Fatalf("expected status=skipped for case-only difference, got %v (full response: %#v)", resp["status"], resp)
}
}
6 changes: 3 additions & 3 deletions plugin/claude-code/scripts/_helpers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ detect_project() {
if [ -n "$url" ]; then
# Handles both SSH (git@github.com:user/repo.git) and HTTPS (https://github.com/user/repo.git)
local name
name=$(echo "$url" | sed 's/\.git$//' | sed 's|.*[/:]||')
name=$(echo "$url" | sed 's/\.git$//' | sed 's|.*[/:]||' | tr '[:upper:]' '[:lower:]')
if [ -n "$name" ]; then
echo "$name"
return
Expand All @@ -24,10 +24,10 @@ detect_project() {
local root
root=$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null)
if [ -n "$root" ]; then
basename "$root"
basename "$root" | tr '[:upper:]' '[:lower:]'
return
fi

# Final fallback: cwd basename (current behavior)
basename "$dir"
basename "$dir" | tr '[:upper:]' '[:lower:]'
}
2 changes: 1 addition & 1 deletion plugin/claude-code/scripts/session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ source "${SCRIPT_DIR}/_helpers.sh"
INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
CWD=$(echo "$INPUT" | jq -r '.cwd // empty')
OLD_PROJECT=$(basename "$CWD")
OLD_PROJECT=$(basename "$CWD" | tr '[:upper:]' '[:lower:]')
PROJECT=$(detect_project "$CWD")

# Ensure engram server is running
Expand Down