Skip to content
Open
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
4 changes: 2 additions & 2 deletions .beads/hooks/post-checkout
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 ---
4 changes: 2 additions & 2 deletions .beads/hooks/post-merge
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 ---
4 changes: 2 additions & 2 deletions .beads/hooks/pre-commit
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 ---
4 changes: 2 additions & 2 deletions .beads/hooks/pre-push
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 ---
4 changes: 2 additions & 2 deletions .beads/hooks/prepare-commit-msg
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 ---
3 changes: 3 additions & 0 deletions .beads/issues.jsonl
Original file line number Diff line number Diff line change
@@ -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}
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:ca08a54f -->
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:7510c1e2 -->
## Beads Issue Tracker

This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.
Expand All @@ -56,6 +56,8 @@ bd close <id> # 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.
Expand All @@ -68,7 +70,6 @@ bd close <id> # 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"
```
Expand All @@ -82,3 +83,4 @@ bd close <id> # Complete work
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds
<!-- END BEADS INTEGRATION -->

6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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


<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:ca08a54f -->
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:7510c1e2 -->
## Beads Issue Tracker

This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.
Expand All @@ -46,6 +46,8 @@ bd close <id> # 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.
Expand All @@ -58,7 +60,6 @@ bd close <id> # 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"
```
Expand All @@ -72,3 +73,4 @@ bd close <id> # Complete work
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds
<!-- END BEADS INTEGRATION -->

4 changes: 2 additions & 2 deletions src/api/notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ 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)
);
}

res.json(notes);
res.json({ data: notes });
});

// Get a single note by ID
Expand Down
126 changes: 126 additions & 0 deletions tests/search.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
});
79 changes: 79 additions & 0 deletions tests/tag-filter.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading