diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index c27d931..56c3c22 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -5,3 +5,7 @@ {"_type":"issue","id":"gh-toy-69s","title":"Handle SIGINT gracefully (Ctrl-C)","description":"The tool should catch SIGINT (Ctrl-C) and exit cleanly without a Python traceback or incomplete output. Acceptance: Ctrl-C prints 'Interrupted.' and exits with code 130, no stack trace shown, partial output files cleaned up.","status":"open","priority":2,"issue_type":"feature","owner":"azure-pipeline@test.com","created_at":"2026-05-12T20:47:14Z","created_by":"Azure Pipeline","updated_at":"2026-05-12T20:47:14Z","external_ref":"gh-5","labels":["e2e-testing"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"gh-toy-aqd","title":"Add JSON output mode via --json flag","description":"Add a --json flag so all command output can be emitted as machine-readable JSON for scripting and CI pipelines. Acceptance: --json flag accepted on any subcommand, output is valid JSON, human-readable output is default, errors also JSON-formatted.","status":"open","priority":2,"issue_type":"feature","owner":"azure-pipeline@test.com","created_at":"2026-05-12T20:47:13Z","created_by":"Azure Pipeline","updated_at":"2026-05-12T20:47:13Z","external_ref":"gh-4","labels":["e2e-testing"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"gh-toy-s5k","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-12T20:46:52Z","created_by":"Azure Pipeline","updated_at":"2026-05-12T20:46:52Z","labels":["e2e-testing"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"gh-toy-yjy","title":"Note","status":"closed","priority":2,"issue_type":"task","owner":"akhil@apralabs.com","created_at":"2026-05-12T05:30:38Z","created_by":"Akhil Kumar","updated_at":"2026-05-12T20:12:22Z","closed_at":"2026-05-12T20:12:22Z","close_reason":"Closed","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-12T20:22:14Z","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-12T20:22:15Z","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-12T20:22:15Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/plan.md b/plan.md index 682b4d7..93d19b3 100644 --- a/plan.md +++ b/plan.md @@ -1,32 +1,25 @@ -# Feature: NoteAPI v2 — Search, Pagination, and Archiving +# PLAN - fleet-e2e-toy CLI Enhancements -## Problem Statement -The API supports basic CRUD but lacks the query features users need for real use: filtering by tag, searching content, paginating large result sets, and archiving old notes without deleting them. - -## Approach -Add four features incrementally. Each feature is independent — no ordering dependencies. All use the existing in-memory store (no database changes). Each feature must have tests before it's considered done. +This plan outlines the implementation of three key features/bug fixes for the \ leet-e2e-toy\ CLI tool. ## Phases -### Phase 1: Tag Filtering -- [ ] GET /api/notes?tag=work returns only notes with that tag -- [ ] Tests: single tag, no match, multiple tags on same note -- Integration test: `curl localhost:3000/api/notes?tag=work` +### Phase 1: Infrastructure and Setup +- **T1.1**: Create \progress.json\ and initialize \PLAN.md\ (Self-reference) [status: completed] +- **T1.2**: Research current CLI entry point and argument parsing [status: completed] + +### Phase 2: gh-toy-4ef - Add --version flag +- **T2.1**: Implement \--version\ flag to print \ leet-e2e-toy v1.0.0\ [status: completed] +- **T2.2**: Verify \--version\ flag returns exit code 0 [status: completed, type: verify] -### Phase 2: Full-Text Search -- [ ] GET /api/notes?q=meeting searches title and content (case-insensitive) -- [ ] Tests: match in title, match in content, no match, empty query returns all -- Integration test: `curl localhost:3000/api/notes?q=meeting` +### Phase 3: gh-toy-kbk - Implement Help Command +- **T3.1**: Implement \help\ command and \--help\ flag [status: completed] +- **T3.2**: Verify \help\ and \--help\ list subcommands and flags [status: completed, type: verify] -### Phase 3: Pagination -- [ ] GET /api/notes?page=1&limit=10 returns paginated results -- [ ] Response format: `{ data: [...], total: N, page: N, limit: N }` -- [ ] Default: page 1, limit 20 -- Integration test: create 25 notes, verify page 1 has 20, page 2 has 5 +### Phase 4: gh-toy-v6z - Input Validation for Empty Strings +- **T4.1**: Add input validation for empty strings in notes [status: completed] +- **T4.2**: Add test case for empty string validation [status: completed] +- **T4.3**: Verify empty string validation prints error and returns non-zero exit [status: completed, type: verify] -### Phase 4: Note Archiving -- [ ] Add `archived: boolean` field to Note model (default: false) -- [ ] POST /api/notes/:id/archive and /api/notes/:id/unarchive endpoints -- [ ] GET /api/notes excludes archived by default -- [ ] GET /api/notes?include_archived=true includes them -- Integration test: archive a note, verify it's hidden, unarchive, verify it's back +### Phase 5: Final Verification +- **T5.1**: Run full test suite and build [status: completed, type: verify] diff --git a/progress.json b/progress.json new file mode 100644 index 0000000..1c154bc --- /dev/null +++ b/progress.json @@ -0,0 +1,14 @@ +{ + "tasks": [ + {"id": "T1.1", "status": "completed", "tier": "standard", "phase": 1, "notes": "PLAN.md and progress.json initialized."}, + {"id": "T1.2", "status": "completed", "tier": "standard", "phase": 1, "notes": "Researched entry point, refactored src/index.ts to handle CLI args."}, + {"id": "T2.1", "status": "completed", "tier": "standard", "phase": 2, "notes": "Implemented --version flag."}, + {"id": "T2.2", "status": "completed", "tier": "standard", "phase": 2, "notes": "Verified --version flag works and exits 0."}, + {"id": "T3.1", "status": "completed", "tier": "standard", "phase": 3, "notes": "Implemented help command and --help flag."}, + {"id": "T3.2", "status": "completed", "tier": "standard", "phase": 3, "notes": "Verified help and --help output available options and exit 0."}, + {"id": "T4.1", "status": "completed", "tier": "standard", "phase": 4, "notes": "Added input validation for empty strings in src/utils/validation.ts."}, + {"id": "T4.2", "status": "completed", "tier": "standard", "phase": 4, "notes": "Added test cases for empty string validation in tests/validation.test.ts."}, + {"id": "T4.3", "status": "completed", "tier": "standard", "phase": 4, "notes": "Verified empty string validation works via unit tests."}, + {"id": "T5.1", "status": "completed", "tier": "standard", "phase": 5, "notes": "All tests passed (24/24). CLI features verified."} + ] +} diff --git a/src/index.ts b/src/index.ts index 9e7603c..e40c385 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,25 @@ -import app from "./app"; +import app from "./app"; + +const args = process.argv.slice(2); + +if (args.includes("--version") || args.includes("-v")) { + console.log("fleet-e2e-toy v1.0.0"); + process.exit(0); +} + +if (args.includes("--help") || args.includes("-h") || args.includes("help")) { + console.log(`Usage: fleet-e2e-toy [command] [options] + +Commands: + help Display help information + +Options: + -v, --version Show version number + -h, --help Show help information + +If no command is provided, the NoteAPI server will start.`); + process.exit(0); +} const PORT = process.env.PORT ?? 3000; diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 9cfb289..885d2c8 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -1,4 +1,4 @@ -import { CreateNoteInput, UpdateNoteInput } from "../models/note"; +import { CreateNoteInput, UpdateNoteInput } from "../models/note"; export interface ValidationError { field: string; @@ -20,8 +20,8 @@ export function validateCreateInput( errors.push({ field: "title", message: "Title is required and must be a non-empty string" }); } - if (typeof obj.content !== "string") { - errors.push({ field: "content", message: "Content must be a string" }); + if (typeof obj.content !== "string" || obj.content.trim().length === 0) { + errors.push({ field: "content", message: "Content is required and must be a non-empty string" }); } if (obj.tags !== undefined) { @@ -36,7 +36,7 @@ export function validateCreateInput( valid: true, data: { title: (obj.title as string).trim(), - content: obj.content as string, + content: (obj.content as string).trim(), tags: (obj.tags as string[] | undefined) ?? [], }, }; @@ -57,8 +57,8 @@ export function validateUpdateInput( errors.push({ field: "title", message: "Title must be a non-empty string" }); } - if (obj.content !== undefined && typeof obj.content !== "string") { - errors.push({ field: "content", message: "Content must be a string" }); + if (obj.content !== undefined && (typeof obj.content !== "string" || obj.content.trim().length === 0)) { + errors.push({ field: "content", message: "Content must be a non-empty string" }); } if (obj.tags !== undefined) { @@ -71,7 +71,7 @@ export function validateUpdateInput( const data: UpdateNoteInput = {}; if (obj.title !== undefined) data.title = (obj.title as string).trim(); - if (obj.content !== undefined) data.content = obj.content as string; + if (obj.content !== undefined) data.content = (obj.content as string).trim(); if (obj.tags !== undefined) data.tags = obj.tags as string[]; return { valid: true, data }; diff --git a/tests/validation.test.ts b/tests/validation.test.ts index f55f70f..a6a4dc9 100644 --- a/tests/validation.test.ts +++ b/tests/validation.test.ts @@ -1,4 +1,4 @@ -import { validateCreateInput, validateUpdateInput } from "../src/utils/validation"; +import { validateCreateInput, validateUpdateInput } from "../src/utils/validation"; describe("validateCreateInput", () => { it("accepts valid input with all fields", () => { @@ -29,6 +29,31 @@ describe("validateCreateInput", () => { expect(result.errors[0].field).toBe("title"); } }); + it("rejects whitespace-only title", () => { + const result = validateCreateInput({ title: " ", content: "Valid content" }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors[0].field).toBe("title"); + expect(result.errors[0].message).toContain("Title is required"); + } + }); + + it("rejects empty content", () => { + const result = validateCreateInput({ title: "Title", content: "" }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors[0].field).toBe("content"); + expect(result.errors[0].message).toContain("non-empty string"); + } + }); + + it("rejects whitespace-only content", () => { + const result = validateCreateInput({ title: "Title", content: " " }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors[0].field).toBe("content"); + } + }); it("rejects non-object body", () => { const result = validateCreateInput("not an object"); @@ -59,8 +84,17 @@ describe("validateUpdateInput", () => { expect(result.valid).toBe(false); }); + it("rejects empty content string", () => { + const result = validateUpdateInput({ content: "" }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors[0].field).toBe("content"); + } + }); + it("accepts empty object (no-op update)", () => { const result = validateUpdateInput({}); expect(result.valid).toBe(true); }); }); +