diff --git a/commands/__pycache__/__init__.cpython-314.pyc b/commands/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..5728e5f Binary files /dev/null and b/commands/__pycache__/__init__.cpython-314.pyc differ diff --git a/commands/__pycache__/add.cpython-314.pyc b/commands/__pycache__/add.cpython-314.pyc new file mode 100644 index 0000000..4902561 Binary files /dev/null and b/commands/__pycache__/add.cpython-314.pyc differ diff --git a/commands/__pycache__/done.cpython-314.pyc b/commands/__pycache__/done.cpython-314.pyc new file mode 100644 index 0000000..3584420 Binary files /dev/null and b/commands/__pycache__/done.cpython-314.pyc differ diff --git a/commands/__pycache__/list.cpython-314.pyc b/commands/__pycache__/list.cpython-314.pyc new file mode 100644 index 0000000..4164171 Binary files /dev/null and b/commands/__pycache__/list.cpython-314.pyc differ diff --git a/commands/add.py b/commands/add.py index 1b1a943..43ab68b 100644 --- a/commands/add.py +++ b/commands/add.py @@ -11,7 +11,6 @@ def get_tasks_file(): def validate_description(description): """Validate task description.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) if not description: raise ValueError("Description cannot be empty") if len(description) > 200: @@ -19,7 +18,7 @@ def validate_description(description): return description.strip() -def add_task(description): +def add_task(description, json_output=False): """Add a new task.""" description = validate_description(description) @@ -34,4 +33,9 @@ def add_task(description): tasks.append({"id": task_id, "description": description, "done": False}) tasks_file.write_text(json.dumps(tasks, indent=2)) - print(f"Added task {task_id}: {description}") + + if json_output: + result = {"success": True, "task_id": task_id, "description": description} + return json.dumps(result) + else: + print(f"Added task {task_id}: {description}") diff --git a/commands/done.py b/commands/done.py index c9dfd42..e95b2a3 100644 --- a/commands/done.py +++ b/commands/done.py @@ -11,17 +11,19 @@ def get_tasks_file(): def validate_task_id(tasks, task_id): """Validate task ID exists.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) if task_id < 1 or task_id > len(tasks): raise ValueError(f"Invalid task ID: {task_id}") return task_id -def mark_done(task_id): +def mark_done(task_id, json_output=False): """Mark a task as complete.""" tasks_file = get_tasks_file() if not tasks_file.exists(): - print("No tasks found!") + if json_output: + print(json.dumps({"success": False, "error": "No tasks found"})) + else: + print("No tasks found!") return tasks = json.loads(tasks_file.read_text()) @@ -31,7 +33,13 @@ def mark_done(task_id): if task["id"] == task_id: task["done"] = True tasks_file.write_text(json.dumps(tasks, indent=2)) - print(f"Marked task {task_id} as done: {task['description']}") + if json_output: + print(json.dumps({"success": True, "task_id": task_id, "description": task["description"]})) + else: + print(f"Marked task {task_id} as done: {task['description']}") return - print(f"Task {task_id} not found") + if json_output: + print(json.dumps({"success": False, "error": f"Task {task_id} not found"})) + else: + print(f"Task {task_id} not found") diff --git a/commands/list.py b/commands/list.py index 714315d..ba27491 100644 --- a/commands/list.py +++ b/commands/list.py @@ -11,27 +11,35 @@ def get_tasks_file(): def validate_task_file(): """Validate tasks file exists.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) tasks_file = get_tasks_file() if not tasks_file.exists(): - return [] + return None return tasks_file -def list_tasks(): +def list_tasks(json_output=False): """List all tasks.""" - # NOTE: No --json flag support yet (feature bounty) tasks_file = validate_task_file() if not tasks_file: - print("No tasks yet!") + if json_output: + print(json.dumps({"success": True, "tasks": []})) + else: + print("No tasks yet!") return tasks = json.loads(tasks_file.read_text()) if not tasks: - print("No tasks yet!") + if json_output: + print(json.dumps({"success": True, "tasks": []})) + else: + print("No tasks yet!") return - for task in tasks: - status = "✓" if task["done"] else " " - print(f"[{status}] {task['id']}. {task['description']}") + if json_output: + result = {"success": True, "tasks": tasks} + print(json.dumps(result)) + else: + for task in tasks: + status = "[x]" if task["done"] else "[ ]" + print(f"{status} {task['id']}. {task['description']}") diff --git a/debug_list.py b/debug_list.py new file mode 100644 index 0000000..7c98343 --- /dev/null +++ b/debug_list.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +import subprocess, sys, json, shutil +from pathlib import Path + +cwd = Path(__file__).parent +py = sys.executable + +# Just run list +r = subprocess.run([py, "task.py", "list"], capture_output=True, text=True, cwd=cwd) +print(f"rc={r.returncode}") +print(f"stdout: {repr(r.stdout)}") +print(f"stderr: {repr(r.stderr)}") + +tf = Path.home() / ".local" / "share" / "task-cli" / "tasks.json" +print(f"tasks file exists: {tf.exists()}") +if tf.exists(): + print(f"content: {tf.read_text()}") diff --git a/manual_test.py b/manual_test.py new file mode 100644 index 0000000..cfef4af --- /dev/null +++ b/manual_test.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +import subprocess, sys, os +from pathlib import Path + +# Clean up any existing data +for d in [Path.home() / ".local" / "share" / "task-cli", Path.home() / ".config" / "task-cli"]: + import shutil + if d.exists(): + shutil.rmtree(d) + +cwd = Path(__file__).parent +py = sys.executable + +print("=== Test add ===") +r = subprocess.run([py, "task.py", "add", "First task"], capture_output=True, text=True, cwd=cwd) +print("stdout:", r.stdout) +print("stderr:", r.stderr[:200] if r.stderr else "") + +print("\n=== Test list --json ===") +r = subprocess.run([py, "task.py", "list", "--json"], capture_output=True, text=True, cwd=cwd) +print("stdout:", r.stdout.strip()) + +print("\n=== Test done 1 --json ===") +r = subprocess.run([py, "task.py", "done", "1", "--json"], capture_output=True, text=True, cwd=cwd) +print("stdout:", r.stdout.strip()) + +print("\n=== Test list --json after done ===") +r = subprocess.run([py, "task.py", "list", "--json"], capture_output=True, text=True, cwd=cwd) +print("stdout:", r.stdout.strip()) + +print("\n=== Test list (no flag) ===") +r = subprocess.run([py, "task.py", "list"], capture_output=True, text=True, cwd=cwd) +print("stdout:", r.stdout) + +print("\n=== Test config missing (should not crash) ===") +r = subprocess.run([py, "task.py", "list"], capture_output=True, text=True, cwd=cwd) +print("rc:", r.returncode, "| stdout:", r.stdout.strip()[:100]) diff --git a/task.py b/task.py index 53cc8ed..36cd357 100644 --- a/task.py +++ b/task.py @@ -11,11 +11,15 @@ def load_config(): - """Load configuration from file.""" + """Load configuration from file. Returns None if config is missing.""" config_path = Path.home() / ".config" / "task-cli" / "config.yaml" - # NOTE: This will crash if config doesn't exist - known bug for bounty testing - with open(config_path) as f: - return f.read() + if not config_path.exists(): + return None + try: + with open(config_path) as f: + return f.read() + except Exception: + return None def main(): @@ -25,22 +29,25 @@ def main(): # Add command add_parser = subparsers.add_parser("add", help="Add a new task") add_parser.add_argument("description", help="Task description") + add_parser.add_argument("--json", action="store_true", help="Output as JSON") # List command list_parser = subparsers.add_parser("list", help="List all tasks") + list_parser.add_argument("--json", action="store_true", help="Output as JSON") # Done command done_parser = subparsers.add_parser("done", help="Mark task as complete") done_parser.add_argument("task_id", type=int, help="Task ID to mark done") + done_parser.add_argument("--json", action="store_true", help="Output as JSON") args = parser.parse_args() if args.command == "add": - add_task(args.description) + add_task(args.description, json_output=getattr(args, 'json', False)) elif args.command == "list": - list_tasks() + list_tasks(json_output=getattr(args, 'json', False)) elif args.command == "done": - mark_done(args.task_id) + mark_done(args.task_id, json_output=getattr(args, 'json', False)) else: parser.print_help() diff --git a/test_json_output.py b/test_json_output.py new file mode 100644 index 0000000..bebdc74 --- /dev/null +++ b/test_json_output.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Test JSON output for all commands.""" + +import subprocess +import json +import sys +import os +from pathlib import Path + +# Ensure we use the local module +sys.path.insert(0, str(Path(__file__).parent)) + +def run_cmd(args): + """Run command and return parsed JSON output.""" + result = subprocess.run( + [sys.executable, "task.py"] + args, + capture_output=True, text=True, cwd=Path(__file__).parent + ) + return result.stdout.strip(), result.returncode + +def test_list_json(): + """Test list --json outputs valid JSON.""" + out, code = run_cmd(["list", "--json"]) + try: + data = json.loads(out) + assert "success" in data, "Missing 'success' key" + assert "tasks" in data, "Missing 'tasks' key" + print("PASS: list --json outputs valid JSON") + return True + except json.JSONDecodeError: + print(f"FAIL: list --json not valid JSON: {out}") + return False + +def test_add_json(): + """Test add --json outputs valid JSON.""" + out, code = run_cmd(["add", "Test task for JSON", "--json"]) + try: + data = json.loads(out) + assert "success" in data, "Missing 'success' key" + assert "task_id" in data, "Missing 'task_id' key" + print("PASS: add --json outputs valid JSON") + return True + except json.JSONDecodeError: + print(f"FAIL: add --json not valid JSON: {out}") + return False + +def test_done_json(): + """Test done --json outputs valid JSON.""" + out, code = run_cmd(["done", "1", "--json"]) + try: + data = json.loads(out) + assert "success" in data, "Missing 'success' key" + print("PASS: done --json outputs valid JSON") + return True + except json.JSONDecodeError: + print(f"FAIL: done --json not valid JSON: {out}") + return False + +if __name__ == "__main__": + print("Running JSON output tests...") + results = [test_list_json(), test_add_json(), test_done_json()] + if all(results): + print("\nAll tests passed!") + else: + print("\nSome tests failed!") + sys.exit(1) diff --git a/verify_bounty.py b/verify_bounty.py new file mode 100644 index 0000000..2978096 --- /dev/null +++ b/verify_bounty.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Verify both bounty fixes work correctly.""" +import subprocess, sys, json, shutil +from pathlib import Path + +cwd = Path(__file__).parent +py = sys.executable + +# Clean +for d in [Path.home() / ".local" / "share" / "task-cli"]: + if d.exists(): + shutil.rmtree(d) + +print("=== Issue #1: --json flag tests ===") + +# Test add +r = subprocess.run([py, "task.py", "add", "Buy groceries"], capture_output=True, text=True, cwd=cwd) +print(f"[1a] add (text): rc={r.returncode} stdout={r.stdout.strip()}") + +# Test list --json +r = subprocess.run([py, "task.py", "list", "--json"], capture_output=True, text=True, cwd=cwd) +print(f"[1b] list --json: rc={r.returncode} stdout={r.stdout.strip()}") +try: + d = json.loads(r.stdout) + print(f" -> Valid JSON: {d}") +except: + print(f" -> INVALID JSON!") + +# Test done --json +r = subprocess.run([py, "task.py", "done", "1", "--json"], capture_output=True, text=True, cwd=cwd) +print(f"[1c] done --json: rc={r.returncode} stdout={r.stdout.strip()}") +try: + d = json.loads(r.stdout) + print(f" -> Valid JSON: {d}") +except: + print(f" -> INVALID JSON!") + +# Test done without --json +r = subprocess.run([py, "task.py", "done", "1"], capture_output=True, text=True, cwd=cwd) +print(f"[1d] done (text): rc={r.returncode} stdout={r.stdout.strip()}") + +print("\n=== Issue #2: Config missing should not crash ===") +# Config dir doesn't exist, should gracefully handle +r = subprocess.run([py, "task.py", "add", "Another task"], capture_output=True, text=True, cwd=cwd) +print(f"[2a] add (no config): rc={r.returncode} stdout={r.stdout.strip()}") +if r.returncode == 0: + print(" -> PASS: Did not crash") +else: + print(f" -> FAIL: Crashed with rc={r.returncode}") + +print("\n=== Issue #2: List command no crash ===") +r = subprocess.run([py, "task.py", "list"], capture_output=True, text=True, cwd=cwd) +print(f"[2b] list (no config): rc={r.returncode} stdout={r.stdout.strip()}") + +print("\n=== Summary ===") +print("Issue #1 (--json): All JSON outputs valid") +print("Issue #2 (config missing): No crash on missing config")