fix(store): reject empty observation titles before persistence#467
fix(store): reject empty observation titles before persistence#467tommanzur wants to merge 8 commits into
Conversation
Empty (or whitespace-only) titles passed to AddObservation were accepted locally but rejected by the cloud sync server (ValidateSyncMutationPayload), silently blocking the mutation queue. Validate at write time so the failure surfaces immediately and the caller gets actionable feedback. Closes Gentleman-Programming#459
Mirror the existing content check. The error message instructs the agent to provide a short, searchable title, since the previous silent success caused stuck cloud sync queues with no feedback loop. Refs Gentleman-Programming#459
|
Thanks @ricardoarz-dev for the pointer to One note for a maintainer: I couldn't add the Happy to adjust the approach or split anything further if it helps review. |
|
#459 is now approved and this PR has the |
…y-observation-titles
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughEmpty/whitespace-only observation title validation is added at three enforcement points: ChangesEmpty Title Validation
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes 🚥 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 unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/store/store_test.go`:
- Around line 8580-8608: The TestAddObservationRejectsEmptyTitle test only
covers titles that are already empty in their raw form, but misses the critical
edge case where a title is non-empty initially but becomes empty after private
tags are stripped (for example, a title containing only
"<private>secret</private>"). Add a new test case to the cases slice in
TestAddObservationRejectsEmptyTitle that covers this scenario by providing a
title that contains only private tags, ensuring this edge path is properly
tested for regression.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: f796fa13-2689-4ba2-89f6-f1f7ca11e7ac
📒 Files selected for processing (4)
internal/mcp/mcp.gointernal/mcp/mcp_test.gointernal/store/store.gointernal/store/store_test.go
Add a test documenting that AddObservation validates the title AFTER stripPrivateTags: a private-only title becomes "[REDACTED]" (non-empty) and is accepted/persisted as such. Closes the coverage gap on the post-strip path raised in review (CodeRabbit), without the incorrect assumption that stripping yields an empty, rejected title.
Alan-TheGentleman
left a comment
There was a problem hiding this comment.
Thanks for tightening the create path. The direction is right, but I am not merging this yet because the invariant is not applied across the full supported surface.
UpdateObservation still accepts empty or whitespace titles, persists them, and enqueues an observation upsert, so mem_update and PATCH /observations/{id} can still create the same stuck sync queue class this PR is meant to prevent. Also, gentle-engram support lives in this repo and needs to stay aligned with Engram behavior, so please verify and cover that supported path as part of this fix rather than treating it as external follow-up.
Once the update path and gentle-engram-supported path are covered with tests, this should be good to re-review.
UpdateObservation accepted empty/whitespace titles, persisted them, and enqueued a sync upsert the cloud server rejects — the same stuck-queue class AddObservation already guards (issue Gentleman-Programming#459), reachable via mem_update and PATCH /observations/{id} (the path the gentle-engram/opencode plugin forwards to). Validate the final post-strip title in UpdateObservation, introduce the ErrEmptyObservationTitle sentinel shared by both add and update paths, and map it to 400 in the HTTP handlers. Cover the store, MCP and HTTP update surfaces with tests.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/server/server_test.go`:
- Around line 172-234: Add a new test function to verify that the POST
/observations endpoint rejects whitespace-only titles with a 400 Bad Request
response and does not persist the observation. Create a test similar to the
existing TestUpdateObservationRejectsEmptyTitle pattern: first establish a
session, then attempt to create an observation with empty string and
whitespace-only title values, asserting that both return HTTP 400 and that no
observation is created (verify by querying the created observation ID or
confirming the count remains zero). This ensures handleAddObservation
store-level empty-title validation is covered across both POST and PATCH routes
as per the PR requirements.
In `@internal/server/server.go`:
- Around line 459-466: The default case in the error handling switch statement
for UpdateObservation currently returns http.StatusNotFound for all unexpected
errors, which incorrectly classifies internal failures as not-found errors.
Change the default branch in this switch statement to return
http.StatusInternalServerError instead of http.StatusNotFound when using the
jsonError function, so that unexpected or unknown errors are properly
communicated as internal server errors rather than resource not-found errors.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: bdc76d8f-2928-471a-84a7-6b3b3b93d631
📒 Files selected for processing (5)
internal/mcp/mcp_test.gointernal/server/server.gointernal/server/server_test.gointernal/store/store.gointernal/store/store_test.go
… route Address CodeRabbit on PR Gentleman-Programming#467: - handleUpdateObservation default branch now returns 500 for unexpected failures instead of 404; ErrObservationNotFound and sql.ErrNoRows stay 404 so the not-found contract is preserved. - Add route+response test for POST /observations rejecting empty and whitespace-only titles with 400 and no persistence.
Alan-TheGentleman
left a comment
There was a problem hiding this comment.
Update path, MCP update, HTTP POST/PATCH status handling, and the gentle-engram/opencode-supported route are now covered. Focused tests pass locally: go test -count=1 ./internal/store ./internal/mcp ./internal/server ./internal/setup. Good to merge.
Summary
Closes #459.
Empty (or whitespace-only) observation titles were accepted by
mem_saveandstore.AddObservation, persisted locally, and queued for cloud sync. The cloudserver rejects them via
ValidateSyncMutationPayload, blocking the entiremutation queue silently — the caller receives success at write time and has no
feedback to retry with a valid title.
This PR pushes the same validation up to the write path, so the failure
surfaces immediately and can be corrected.
Changes
internal/store/store.go:AddObservationrejects empty/whitespace titleswith a clear error, before any DB access. Validated after
stripPrivateTagsso it matches exactly the value that gets persisted and later pushed to the
cloud server.
internal/mcp/mcp.go:handleSavereturns an agent-actionableToolResultErrorbefore reaching the store, with an example title format sothe model knows what to provide on retry.
Why this approach
Per the suggestion on #459, reusing the rule already enforced by
ValidateSyncMutationPayload(diagnostic.go) keeps validation in one logicalplace. The fix is defensive at two layers — MCP for UX, store as the single
source of truth — so the CLI, MCP, and any future entrypoint inherit it.
Test plan
go test ./internal/store/...— new tests for empty/whitespace titles + a positive casego test ./internal/mcp/...— handler returnsToolResultError, nothing persistedgo build ./cmd/engram— binary builds cleanNotes
today, but those cases are already broken downstream (cloud sync blocks them).
The change turns a silent failure into a loud, immediate one.
stripPrivateTagsreplaces<private>…</private>with[REDACTED], not anempty string, so a title made only of private tags is not rejected — that
is intended.
Out of scope
repair tooling and the SQLite workaround documented in Reject empty observation titles before persistence #459's comments handle
that path. This PR prevents new occurrences.
Summary by CodeRabbit