From 63cb8392d04b2cf243005f95a8f5dc552e6275f4 Mon Sep 17 00:00:00 2001 From: Akhil Kumar Date: Tue, 12 May 2026 01:29:36 -0400 Subject: [PATCH 1/3] bd init: initialize beads issue tracking --- .beads/hooks/post-checkout | 4 ++-- .beads/hooks/post-merge | 4 ++-- .beads/hooks/pre-commit | 4 ++-- .beads/hooks/pre-push | 4 ++-- .beads/hooks/prepare-commit-msg | 4 ++-- AGENTS.md | 6 ++++-- CLAUDE.md | 6 ++++-- 7 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.beads/hooks/post-checkout b/.beads/hooks/post-checkout index d485872..7d35c68 100644 --- a/.beads/hooks/post-checkout +++ b/.beads/hooks/post-checkout @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# --- BEGIN BEADS INTEGRATION v1.0.3 --- +# --- BEGIN BEADS INTEGRATION v1.0.4 --- # This section is managed by beads. Do not remove these markers. if command -v bd >/dev/null 2>&1; then export BD_GIT_HOOK=1 @@ -21,4 +21,4 @@ if command -v bd >/dev/null 2>&1; then fi if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi fi -# --- END BEADS INTEGRATION v1.0.3 --- +# --- END BEADS INTEGRATION v1.0.4 --- diff --git a/.beads/hooks/post-merge b/.beads/hooks/post-merge index 5aa3315..1f458ba 100644 --- a/.beads/hooks/post-merge +++ b/.beads/hooks/post-merge @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# --- BEGIN BEADS INTEGRATION v1.0.3 --- +# --- BEGIN BEADS INTEGRATION v1.0.4 --- # This section is managed by beads. Do not remove these markers. if command -v bd >/dev/null 2>&1; then export BD_GIT_HOOK=1 @@ -21,4 +21,4 @@ if command -v bd >/dev/null 2>&1; then fi if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi fi -# --- END BEADS INTEGRATION v1.0.3 --- +# --- END BEADS INTEGRATION v1.0.4 --- diff --git a/.beads/hooks/pre-commit b/.beads/hooks/pre-commit index d7ac3d9..ad1fb16 100644 --- a/.beads/hooks/pre-commit +++ b/.beads/hooks/pre-commit @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# --- BEGIN BEADS INTEGRATION v1.0.3 --- +# --- BEGIN BEADS INTEGRATION v1.0.4 --- # This section is managed by beads. Do not remove these markers. if command -v bd >/dev/null 2>&1; then export BD_GIT_HOOK=1 @@ -21,4 +21,4 @@ if command -v bd >/dev/null 2>&1; then fi if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi fi -# --- END BEADS INTEGRATION v1.0.3 --- +# --- END BEADS INTEGRATION v1.0.4 --- diff --git a/.beads/hooks/pre-push b/.beads/hooks/pre-push index 5af9e7b..35c2a69 100644 --- a/.beads/hooks/pre-push +++ b/.beads/hooks/pre-push @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# --- BEGIN BEADS INTEGRATION v1.0.3 --- +# --- BEGIN BEADS INTEGRATION v1.0.4 --- # This section is managed by beads. Do not remove these markers. if command -v bd >/dev/null 2>&1; then export BD_GIT_HOOK=1 @@ -21,4 +21,4 @@ if command -v bd >/dev/null 2>&1; then fi if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi fi -# --- END BEADS INTEGRATION v1.0.3 --- +# --- END BEADS INTEGRATION v1.0.4 --- diff --git a/.beads/hooks/prepare-commit-msg b/.beads/hooks/prepare-commit-msg index f0aec3c..a72277d 100644 --- a/.beads/hooks/prepare-commit-msg +++ b/.beads/hooks/prepare-commit-msg @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# --- BEGIN BEADS INTEGRATION v1.0.3 --- +# --- BEGIN BEADS INTEGRATION v1.0.4 --- # This section is managed by beads. Do not remove these markers. if command -v bd >/dev/null 2>&1; then export BD_GIT_HOOK=1 @@ -21,4 +21,4 @@ if command -v bd >/dev/null 2>&1; then fi if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi fi -# --- END BEADS INTEGRATION v1.0.3 --- +# --- END BEADS INTEGRATION v1.0.4 --- diff --git a/AGENTS.md b/AGENTS.md index 9390d72..6e6b438 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ cp -rf source dest # NOT: cp -r source dest - `apt-get` - use `-y` flag - `brew` - use `HOMEBREW_NO_AUTO_UPDATE=1` env var - + ## Beads Issue Tracker This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands. @@ -56,6 +56,8 @@ bd close # Complete work - Run `bd prime` for detailed command reference and session close protocol - Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files +**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns. + ## Session Completion **When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. @@ -68,7 +70,6 @@ bd close # Complete work 4. **PUSH TO REMOTE** - This is MANDATORY: ```bash git pull --rebase - bd dolt push git push git status # MUST show "up to date with origin" ``` @@ -82,3 +83,4 @@ bd close # Complete work - NEVER say "ready to push when you are" - YOU must push - If push fails, resolve and retry until it succeeds + diff --git a/CLAUDE.md b/CLAUDE.md index 1d92bb5..349ee89 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,7 @@ REST API for managing notes with tags and search. Node.js + Express + TypeScript - Don't install a database — the in-memory store is intentional - + ## Beads Issue Tracker This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands. @@ -46,6 +46,8 @@ bd close # Complete work - Run `bd prime` for detailed command reference and session close protocol - Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files +**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns. + ## Session Completion **When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. @@ -58,7 +60,6 @@ bd close # Complete work 4. **PUSH TO REMOTE** - This is MANDATORY: ```bash git pull --rebase - bd dolt push git push git status # MUST show "up to date with origin" ``` @@ -72,3 +73,4 @@ bd close # Complete work - NEVER say "ready to push when you are" - YOU must push - If push fails, resolve and retry until it succeeds + From 007e929d384ac5663c7e3d2e1892c06508bf9030 Mon Sep 17 00:00:00 2001 From: Akhil Kumar Date: Tue, 12 May 2026 15:46:49 -0400 Subject: [PATCH 2/3] feat: handle search edge cases and ensure case-insensitivity --- .beads/issues.jsonl | 3 ++ src/api/notes.ts | 2 +- tests/search.test.ts | 126 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 tests/search.test.ts diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index d81be7e..b46b38f 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1 +1,4 @@ +{"_type":"issue","id":"gh-toy-yjy","title":"Note","status":"open","priority":2,"issue_type":"task","owner":"akhil@apralabs.com","created_at":"2026-05-12T05:30:38Z","created_by":"Akhil Kumar","updated_at":"2026-05-12T05:30:38Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"gh-toy-06i","title":"Pagination","status":"open","priority":2,"issue_type":"task","owner":"akhil@apralabs.com","created_at":"2026-05-12T05:30:33Z","created_by":"Akhil Kumar","updated_at":"2026-05-12T05:30:33Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"gh-toy-gw1","title":"Full-text","status":"open","priority":2,"issue_type":"task","owner":"akhil@apralabs.com","created_at":"2026-05-12T05:30:24Z","created_by":"Akhil Kumar","updated_at":"2026-05-12T05:30:24Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"gh-toy-bzq","title":"Tag filtering endpoint","description":"GET /api/notes?tag=work returns only notes with that tag. Already partially implemented — needs tests.","status":"open","priority":2,"issue_type":"feature","owner":"azure-pipeline@test.com","created_at":"2026-05-09T03:13:59Z","created_by":"Azure Pipeline","updated_at":"2026-05-09T03:13:59Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/src/api/notes.ts b/src/api/notes.ts index c024028..2e5022e 100644 --- a/src/api/notes.ts +++ b/src/api/notes.ts @@ -20,7 +20,7 @@ router.get("/", (req: Request, res: Response) => { } const q = req.query.q as string | undefined; - if (q) { + if (q && q.trim()) { const lower = q.toLowerCase(); notes = notes.filter( (n) => n.title.toLowerCase().includes(lower) || n.content.toLowerCase().includes(lower) diff --git a/tests/search.test.ts b/tests/search.test.ts new file mode 100644 index 0000000..6cacf82 --- /dev/null +++ b/tests/search.test.ts @@ -0,0 +1,126 @@ +import request from "supertest"; +import app from "../src/app"; +import { noteStore } from "../src/models/note"; + +beforeEach(() => { + noteStore.clear(); +}); + +describe("GET /api/notes - Search Edge Cases", () => { + describe("Empty query", () => { + it("returns all notes when query is empty string", async () => { + await request(app) + .post("/api/notes") + .send({ title: "Hello World", content: "Greeting", tags: [] }); + await request(app) + .post("/api/notes") + .send({ title: "TypeScript", content: "Language", tags: [] }); + + const res = await request(app).get("/api/notes?q="); + expect(res.status).toBe(200); + expect(res.body).toHaveLength(2); + }); + + it("returns all notes when query is whitespace only", async () => { + await request(app) + .post("/api/notes") + .send({ title: "Note 1", content: "Content 1", tags: [] }); + await request(app) + .post("/api/notes") + .send({ title: "Note 2", content: "Content 2", tags: [] }); + + const res = await request(app).get("/api/notes?q=%20%20%20"); + expect(res.status).toBe(200); + expect(res.body).toHaveLength(2); + }); + }); + + describe("Case-insensitivity", () => { + it("finds matches with uppercase query", async () => { + await request(app) + .post("/api/notes") + .send({ title: "Hello World", content: "Greeting", tags: [] }); + + const res = await request(app).get("/api/notes?q=HELLO"); + expect(res.status).toBe(200); + expect(res.body).toHaveLength(1); + expect(res.body[0].title).toBe("Hello World"); + }); + + it("finds matches with mixed case query", async () => { + await request(app) + .post("/api/notes") + .send({ title: "TypeScript Guide", content: "Learn basics", tags: [] }); + + const res = await request(app).get("/api/notes?q=TyPeScRiPt"); + expect(res.status).toBe(200); + expect(res.body).toHaveLength(1); + expect(res.body[0].title).toContain("TypeScript"); + }); + + it("finds matches in content with case variation", async () => { + await request(app) + .post("/api/notes") + .send({ title: "Guide", content: "Learn TypeScript basics", tags: [] }); + + const res = await request(app).get("/api/notes?q=BASICS"); + expect(res.status).toBe(200); + expect(res.body).toHaveLength(1); + }); + }); + + describe("No matches", () => { + it("returns empty array with 200 when no notes match", async () => { + await request(app) + .post("/api/notes") + .send({ title: "Existing note", content: "Some content", tags: [] }); + + const res = await request(app).get("/api/notes?q=nonexistent"); + expect(res.status).toBe(200); + expect(res.body).toEqual([]); + }); + + it("returns empty array with 200 for query with no matches", async () => { + await request(app) + .post("/api/notes") + .send({ title: "Note", content: "Content", tags: [] }); + + const res = await request(app).get("/api/notes?q=xyz123abc"); + expect(res.status).toBe(200); + expect(res.body).toEqual([]); + }); + }); + + describe("Standard matches", () => { + it("finds notes by title", async () => { + await request(app) + .post("/api/notes") + .send({ title: "Hello World", content: "Greeting", tags: [] }); + + const res = await request(app).get("/api/notes?q=Hello"); + expect(res.status).toBe(200); + expect(res.body).toHaveLength(1); + expect(res.body[0].title).toContain("Hello"); + }); + + it("finds notes by content", async () => { + await request(app) + .post("/api/notes") + .send({ title: "API", content: "Best practices for REST APIs", tags: [] }); + + const res = await request(app).get("/api/notes?q=REST"); + expect(res.status).toBe(200); + expect(res.body).toHaveLength(1); + }); + + it("returns empty array for query with no matches in title or content", async () => { + await request(app) + .post("/api/notes") + .send({ title: "One", content: "Another", tags: [] }); + + const res = await request(app).get("/api/notes?q=missing"); + expect(res.status).toBe(200); + expect(res.body).toEqual([]); + }); + }); +}); From fd23850ac47450ef4fbf5d876c4b8682a77d17b8 Mon Sep 17 00:00:00 2001 From: Akhil Kumar Date: Tue, 12 May 2026 15:57:41 -0400 Subject: [PATCH 3/3] feat: implement tag filtering and wrap list response in data property --- src/api/notes.ts | 2 +- tests/tag-filter.test.ts | 79 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/tag-filter.test.ts diff --git a/src/api/notes.ts b/src/api/notes.ts index 2e5022e..734589d 100644 --- a/src/api/notes.ts +++ b/src/api/notes.ts @@ -27,7 +27,7 @@ router.get("/", (req: Request, res: Response) => { ); } - res.json(notes); + res.json({ data: notes }); }); // Get a single note by ID diff --git a/tests/tag-filter.test.ts b/tests/tag-filter.test.ts new file mode 100644 index 0000000..5591b9e --- /dev/null +++ b/tests/tag-filter.test.ts @@ -0,0 +1,79 @@ +import request from "supertest"; +import app from "../src/app"; +import { noteStore } from "../src/models/note"; + +beforeEach(() => { + noteStore.clear(); +}); + +describe("GET /api/notes?tag= — Tag Filtering", () => { + it("returns only notes with the given tag", async () => { + await request(app) + .post("/api/notes") + .send({ title: "Work item", content: "Do work", tags: ["work"] }); + await request(app) + .post("/api/notes") + .send({ title: "Personal item", content: "Buy groceries", tags: ["personal"] }); + + const res = await request(app).get("/api/notes?tag=work"); + expect(res.status).toBe(200); + expect(res.body.data).toHaveLength(1); + expect(res.body.data[0].title).toBe("Work item"); + }); + + it("returns empty array when no notes match the tag", async () => { + await request(app) + .post("/api/notes") + .send({ title: "A note", content: "Content", tags: ["other"] }); + + const res = await request(app).get("/api/notes?tag=nonexistent"); + expect(res.status).toBe(200); + expect(res.body.data).toEqual([]); + }); + + it("returns notes that have the tag among multiple tags", async () => { + await request(app) + .post("/api/notes") + .send({ title: "Multi-tagged", content: "Content", tags: ["work", "urgent", "review"] }); + await request(app) + .post("/api/notes") + .send({ title: "Single-tagged", content: "Content", tags: ["personal"] }); + + const res = await request(app).get("/api/notes?tag=urgent"); + expect(res.status).toBe(200); + expect(res.body.data).toHaveLength(1); + expect(res.body.data[0].title).toBe("Multi-tagged"); + }); + + it("returns multiple notes that all share the same tag", async () => { + await request(app) + .post("/api/notes") + .send({ title: "Note A", content: "Content", tags: ["work"] }); + await request(app) + .post("/api/notes") + .send({ title: "Note B", content: "Content", tags: ["work", "urgent"] }); + await request(app) + .post("/api/notes") + .send({ title: "Note C", content: "Content", tags: ["personal"] }); + + const res = await request(app).get("/api/notes?tag=work"); + expect(res.status).toBe(200); + expect(res.body.data).toHaveLength(2); + const titles = res.body.data.map((n: { title: string }) => n.title); + expect(titles).toContain("Note A"); + expect(titles).toContain("Note B"); + }); + + it("returns all notes when no tag filter is provided", async () => { + await request(app) + .post("/api/notes") + .send({ title: "One", content: "Content", tags: ["a"] }); + await request(app) + .post("/api/notes") + .send({ title: "Two", content: "Content", tags: ["b"] }); + + const res = await request(app).get("/api/notes"); + expect(res.status).toBe(200); + expect(res.body.data).toHaveLength(2); + }); +});