fix(memory): normalize empty path to id in EnsureProject#154
Conversation
…h MCP callers MCP callers invoke EnsureProject with path="" because they have no filesystem path. This caused a UNIQUE constraint violation (projects.path is UNIQUE) when a second project was saved — both rows tried to claim path="". Fix: - Normalize path="" to path=id at the top of EnsureProject. Non-filesystem projects already use id-as-path on disk; this preserves that invariant and guarantees uniqueness (id is the PK). - Change ON CONFLICT(id) DO UPDATE to keep the existing path when the incoming path equals the id (synthetic path), preventing an MCP call from silently clobbering a real filesystem path set by the orchestrator. Adds a regression test: EnsureProject with empty path for two distinct project IDs must succeed without error, and each project's path must equal its id. Signed-off-by: wcatz <waynecataldo@gmail.com>
📝 WalkthroughWalkthroughEnsureProject now treats empty Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/memory/store_test.go (1)
1390-1426: Consider adding coverage for the "don't clobber existing filesystem path" invariant.The PR description explicitly calls out: "guard the ON CONFLICT DO UPDATE so that a synthetic path==id fallback never overwrites an existing real filesystem path already stored." The current test exercises two empty-path projects and idempotent re-calls, but both re-call scenarios have
existing.path == idalready, so they don't actually hit the branch where theCASEmust preserve a pre-existing real filesystem path against a syntheticpath==idoverwrite attempt.A targeted test would directly exercise the core regression:
🧪 Suggested additional test
func TestEnsureProject_EmptyPath_DoesNotClobberFilesystemPath(t *testing.T) { s := testStore(t) ctx := context.Background() // Orchestrator registers the project with a real filesystem path first. const id = "a7293a04b38a" const fsPath = "/home/wayne/git/dingo" if err := s.EnsureProject(ctx, id, fsPath, "dingo"); err != nil { t.Fatalf("EnsureProject (fs): %v", err) } // MCP later calls with empty path for the same ID — must NOT overwrite fsPath. if err := s.EnsureProject(ctx, id, "", "dingo"); err != nil { t.Fatalf("EnsureProject (mcp): %v", err) } projects, err := s.ListProjects(ctx) if err != nil { t.Fatalf("ListProjects: %v", err) } for _, p := range projects { if p.ID == id && p.Path != fsPath { t.Errorf("expected path to remain %q, got %q", fsPath, p.Path) } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/memory/store_test.go` around lines 1390 - 1426, Add a new unit test that verifies EnsureProject does not overwrite an existing real filesystem path when later called with an empty path: use testStore() and EnsureProject(ctx, id, fsPath, ...) to create a project with a real filesystem path, then call EnsureProject(ctx, id, "", ...) and assert via ListProjects() that the project's Path remains the original fsPath; name the test TestEnsureProject_EmptyPath_DoesNotClobberFilesystemPath and reference EnsureProject, ListProjects, and testStore to locate the relevant store API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@internal/memory/store_test.go`:
- Around line 1390-1426: Add a new unit test that verifies EnsureProject does
not overwrite an existing real filesystem path when later called with an empty
path: use testStore() and EnsureProject(ctx, id, fsPath, ...) to create a
project with a real filesystem path, then call EnsureProject(ctx, id, "", ...)
and assert via ListProjects() that the project's Path remains the original
fsPath; name the test TestEnsureProject_EmptyPath_DoesNotClobberFilesystemPath
and reference EnsureProject, ListProjects, and testStore to locate the relevant
store API.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0bfc72c9-99de-452f-8548-7cffaf686584
📒 Files selected for processing (2)
internal/memory/store.gointernal/memory/store_test.go
Summary
Fixes #153
ghost_memory_savewas failing withUNIQUE constraint failed: projects.pathwhen the MCP server calledEnsureProjectwithpath="". Sinceprojects.pathhas a UNIQUE constraint, any two projects called this way would collide.path=""topath=idat the top ofEnsureProject— matches the invariant already on disk for name-as-ID projects (e.g.id=a7293a04b38a, path=a7293a04b38a)ON CONFLICT DO UPDATEso a syntheticpath=idfallback never clobbers a real filesystem path already storedTest plan
TestEnsureProject_EmptyPath_NoUniqueConflict— two projects withpath="", no conflict, idempotent repeat,path==idpost-savego test ./internal/memory/...passesSummary by CodeRabbit
Release Notes