Replace Python CLI with Gitea issue tracker setup
Switch from a custom CLI ticketing tool to Gitea-native issue tracking. Add issue templates (bug, feature, task) and a curated initial backlog of 20 tickets covering project foundation, testing, security, docs, features, and community maintenance.
This commit is contained in:
36
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
36
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: "🐛 Bug Report"
|
||||||
|
about: "Report something that is broken or not working as expected."
|
||||||
|
title: "[Bug] "
|
||||||
|
labels: ["bug", "triage"]
|
||||||
|
assignees: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- A clear, concise description of the bug. -->
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
<!-- What did you expect to happen? -->
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
|
||||||
|
<!-- What actually happened? Include logs or screenshots if helpful. -->
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- **OS / Distro:**
|
||||||
|
- **Browser / Runtime version:**
|
||||||
|
- **Commit / Release:** <!-- e.g. main @ a1b2c3d -->
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
<!-- Any other details that might help diagnose the issue. -->
|
||||||
|
|
||||||
10
.gitea/ISSUE_TEMPLATE/config.yml
Normal file
10
.gitea/ISSUE_TEMPLATE/config.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
|
||||||
|
contact_links:
|
||||||
|
- name: "❓ Ask a Question"
|
||||||
|
url: https://gitea.arcline-project.local/
|
||||||
|
about: "Use the discussions or chat instead."
|
||||||
|
- name: "📖 Project Docs"
|
||||||
|
url: https://docs.arcline-project.local/
|
||||||
|
about: "Read the documentation before filing an issue."
|
||||||
|
|
||||||
28
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
28
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: "✨ Feature Request"
|
||||||
|
about: "Propose a new feature or enhancement."
|
||||||
|
title: "[Feature] "
|
||||||
|
labels: ["enhancement", "triage"]
|
||||||
|
assignees: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
<!-- What problem are you trying to solve? Why is this valuable? -->
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
<!-- Describe the feature or enhancement you'd like to see. -->
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
<!-- Are there other ways to solve this? Why did you choose this approach? -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] <!-- Checkable criteria for when this is "done" -->
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
<!-- Screenshots, references, or anything else that helps. -->
|
||||||
|
|
||||||
26
.gitea/ISSUE_TEMPLATE/task.md
Normal file
26
.gitea/ISSUE_TEMPLATE/task.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
name: "📋 Task / Epic"
|
||||||
|
about: "Track a general task, investigation, or epic."
|
||||||
|
title: "[Task] "
|
||||||
|
labels: ["task"]
|
||||||
|
assignees: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
<!-- One or two sentences describing the task. -->
|
||||||
|
|
||||||
|
## Subtasks
|
||||||
|
|
||||||
|
- [ ]
|
||||||
|
- [ ]
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
<!-- Does this block or is it blocked by other issues? -->
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
<!-- Links, design docs, or relevant context. -->
|
||||||
|
|
||||||
310
ISSUES.md
Normal file
310
ISSUES.md
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
# Initial Ticket Backlog
|
||||||
|
|
||||||
|
Curated list of tickets to file in Gitea Issues. Copy each block into a new issue using the matching template.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Project Foundation
|
||||||
|
|
||||||
|
### 1. Repository bootstrapping
|
||||||
|
|
||||||
|
> **Template:** 📋 Task / Epic
|
||||||
|
> **Labels:** `task`
|
||||||
|
> **Priority:** P0
|
||||||
|
|
||||||
|
**Title:** `[P0] [Task] Bootstrap repo structure and tooling`
|
||||||
|
|
||||||
|
- [ ] Add `.gitignore`, `.editorconfig`, `CONTRIBUTING.md`
|
||||||
|
- [ ] Set up branch protection on `main`
|
||||||
|
- [ ] Configure CI linting (shellcheck, markdownlint, yamllint)
|
||||||
|
- [ ] Add pre-commit hooks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. README & onboarding docs
|
||||||
|
|
||||||
|
> **Template:** 📋 Task / Epic
|
||||||
|
> **Labels:** `documentation`
|
||||||
|
> **Priority:** P0
|
||||||
|
|
||||||
|
**Title:** `[P0] [Task] Write project README and onboarding guide`
|
||||||
|
|
||||||
|
- [ ] High-level architecture overview
|
||||||
|
- [ ] Local development setup instructions
|
||||||
|
- [ ] Contribution workflow (branching, commits, reviews)
|
||||||
|
- [ ] Link to this tickets repo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. CI pipeline
|
||||||
|
|
||||||
|
> **Template:** 📋 Task / Epic
|
||||||
|
> **Labels:** `task`
|
||||||
|
> **Priority:** P0
|
||||||
|
|
||||||
|
**Title:** `[P0] [Task] Set up CI pipeline`
|
||||||
|
|
||||||
|
- [ ] Build step (compile / install deps)
|
||||||
|
- [ ] Lint step (static analysis)
|
||||||
|
- [ ] Test step (unit + integration)
|
||||||
|
- [ ] Status badge in README
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### 4. Unit-test scaffolding
|
||||||
|
|
||||||
|
> **Template:** 📋 Task / Epic
|
||||||
|
> **Labels:** `task`
|
||||||
|
> **Priority:** P1
|
||||||
|
|
||||||
|
**Title:** `[P1] [Task] Scaffold unit-test framework`
|
||||||
|
|
||||||
|
- [ ] Choose and wire up a test runner
|
||||||
|
- [ ] Add a single passing smoke test
|
||||||
|
- [ ] Document how to run tests locally
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Integration tests
|
||||||
|
|
||||||
|
> **Template:** 📋 Task / Epic
|
||||||
|
> **Labels:** `task`
|
||||||
|
> **Priority:** P1
|
||||||
|
|
||||||
|
**Title:** `[P1] [Task] Add integration / end-to-end test harness`
|
||||||
|
|
||||||
|
- [ ] Spin up a local environment for tests
|
||||||
|
- [ ] Write 2-3 critical-path integration tests
|
||||||
|
- [ ] Wire into CI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Test coverage reporting
|
||||||
|
|
||||||
|
> **Template:** ✨ Feature Request
|
||||||
|
> **Labels:** `enhancement`
|
||||||
|
> **Priority:** P2
|
||||||
|
|
||||||
|
**Title:** `[P2] [Feature] Add test coverage reporting`
|
||||||
|
|
||||||
|
> **Problem:** No visibility into what code paths have tests.
|
||||||
|
> **Solution:** Generate coverage reports in CI and block merges below a threshold (e.g. 80 %).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Security & Quality
|
||||||
|
|
||||||
|
### 7. Dependency scanning
|
||||||
|
|
||||||
|
> **Template:** ✨ Feature Request
|
||||||
|
> **Labels:** `enhancement`
|
||||||
|
> **Priority:** P1
|
||||||
|
|
||||||
|
**Title:** `[P1] [Feature] Automated dependency vulnerability scanning`
|
||||||
|
|
||||||
|
> **Problem:** Outdated or vulnerable dependencies slip through.
|
||||||
|
> **Solution:** Add [Dependabot](https://github.com/dependabot) (or Renovate) + a SAST scanner in CI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Secrets management
|
||||||
|
|
||||||
|
> **Template:** 📋 Task / Epic
|
||||||
|
> **Labels:** `task`
|
||||||
|
> **Priority:** P0
|
||||||
|
|
||||||
|
**Title:** `[P0] [Task] Audit and secure secrets management`
|
||||||
|
|
||||||
|
- [ ] Verify no secrets are committed to the repo
|
||||||
|
- [ ] Set up `.env` conventions
|
||||||
|
- [ ] Document how to inject secrets in dev and prod
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Linting & formatting
|
||||||
|
|
||||||
|
> **Template:** 📋 Task / Epic
|
||||||
|
> **Labels:** `task`
|
||||||
|
> **Priority:** P1
|
||||||
|
|
||||||
|
**Title:** `[P1] [Task] Enforce consistent linting and formatting`
|
||||||
|
|
||||||
|
- [ ] Choose and configure formatters / linters for each language in the stack
|
||||||
|
- [ ] Add pre-commit hook (or CI check) that blocks violations
|
||||||
|
- [ ] Format the existing codebase
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Documentation
|
||||||
|
|
||||||
|
### 10. API / interface docs
|
||||||
|
|
||||||
|
> **Template:** 📋 Task / Epic
|
||||||
|
> **Labels:** `documentation`
|
||||||
|
> **Priority:** P1
|
||||||
|
|
||||||
|
**Title:** `[P1] [Task] Generate and publish API documentation`
|
||||||
|
|
||||||
|
- [ ] Select a doc generator (e.g. OpenAPI, Sphinx, JSDoc)
|
||||||
|
- [ ] Add docstrings / annotations to public interfaces
|
||||||
|
- [ ] Publish to a static site or docs host
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. Architecture Decision Records
|
||||||
|
|
||||||
|
> **Template:** ✨ Feature Request
|
||||||
|
> **Labels:** `enhancement`, `documentation`
|
||||||
|
> **Priority:** P2
|
||||||
|
|
||||||
|
**Title:** `[P2] [Feature] Adopt Architecture Decision Records (ADRs)`
|
||||||
|
|
||||||
|
> **Problem:** Design decisions are made in chat and forgotten.
|
||||||
|
> **Solution:** Maintain a `docs/adr/` directory with lightweight decision records.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Placeholder Bugs (for testing the flow)
|
||||||
|
|
||||||
|
### 12. Template: Bug Report
|
||||||
|
|
||||||
|
> **Template:** 🐛 Bug Report
|
||||||
|
> **Labels:** `bug`
|
||||||
|
> **Priority:** P2
|
||||||
|
|
||||||
|
**Title:** `[P2] [Bug] Example bug — page title wraps on small viewports`
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Steps** | Resize browser to 320 px wide, open the landing page. |
|
||||||
|
| **Expected** | Title stays on one line or truncates gracefully. |
|
||||||
|
| **Actual** | Title wraps and overlaps the nav bar. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13. Template: Bug Report
|
||||||
|
|
||||||
|
> **Template:** 🐛 Bug Report
|
||||||
|
> **Labels:** `bug`
|
||||||
|
> **Priority:** P3
|
||||||
|
|
||||||
|
**Title:** `[P3] [Bug] Example bug — console warns about missing key prop`
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Steps** | Open dev tools, navigate to the dashboard. |
|
||||||
|
| **Expected** | No React key warnings. |
|
||||||
|
| **Actual** | `Warning: Each child in a list should have a unique "key" prop.` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
### 14. Health-check endpoint
|
||||||
|
|
||||||
|
> **Template:** ✨ Feature Request
|
||||||
|
> **Labels:** `enhancement`
|
||||||
|
> **Priority:** P1
|
||||||
|
|
||||||
|
**Title:** `[P1] [Feature] Add health-check endpoint`
|
||||||
|
|
||||||
|
> **Problem:** No way for load balancers or monitoring to know if the service is alive.
|
||||||
|
> **Solution:** Expose `GET /health` returning `{"status":"ok"}` and a quick DB ping.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 15. Structured logging
|
||||||
|
|
||||||
|
> **Template:** ✨ Feature Request
|
||||||
|
> **Labels:** `enhancement`
|
||||||
|
> **Priority:** P1
|
||||||
|
|
||||||
|
**Title:** `[P1] [Feature] Switch to structured (JSON) logging`
|
||||||
|
|
||||||
|
> **Problem:** Logs are plain-text and hard to query.
|
||||||
|
> **Solution:** Emit JSON log lines with consistent keys (`level`, `message`, `timestamp`, `trace_id`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 16. Feature flags
|
||||||
|
|
||||||
|
> **Template:** ✨ Feature Request
|
||||||
|
> **Labels:** `enhancement`
|
||||||
|
> **Priority:** P2
|
||||||
|
|
||||||
|
**Title:** `[P2] [Feature] Introduce feature-flag system`
|
||||||
|
|
||||||
|
> **Problem:** Can't safely ship partial or experimental features.
|
||||||
|
> **Solution:** Use a simple flag provider (env vars initially) to gate features.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 17. Observability / metrics
|
||||||
|
|
||||||
|
> **Template:** ✨ Feature Request
|
||||||
|
> **Labels:** `enhancement`
|
||||||
|
> **Priority:** P2
|
||||||
|
|
||||||
|
**Title:** `[P2] [Feature] Export app metrics for monitoring`
|
||||||
|
|
||||||
|
> **Problem:** Can't answer "is the system healthy?" without looking at logs.
|
||||||
|
> **Solution:** Export request duration, error rate, and DB query metrics.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Community & Maintenance
|
||||||
|
|
||||||
|
### 18. CONTRIBUTING guide
|
||||||
|
|
||||||
|
> **Template:** 📋 Task / Epic
|
||||||
|
> **Labels:** `documentation`, `good first issue`
|
||||||
|
> **Priority:** P1
|
||||||
|
|
||||||
|
**Title:** `[P1] [Task] Write CONTRIBUTING.md`
|
||||||
|
|
||||||
|
- [ ] Code style and conventions
|
||||||
|
- [ ] PR template and review expectations
|
||||||
|
- [ ] Local development quick-start
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 19. Issue triage process
|
||||||
|
|
||||||
|
> **Template:** 📋 Task / Epic
|
||||||
|
> **Labels:** `task`
|
||||||
|
> **Priority:** P2
|
||||||
|
|
||||||
|
**Title:** `[P2] [Task] Define and document the issue triage process`
|
||||||
|
|
||||||
|
- [ ] Weekly triage meeting cadence
|
||||||
|
- [ ] SLA for first response on new issues
|
||||||
|
- [ ] How issues move from `triage` → `accepted`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 20. Release checklist
|
||||||
|
|
||||||
|
> **Template:** 📋 Task / Epic
|
||||||
|
> **Labels:** `task`
|
||||||
|
> **Priority:** P2
|
||||||
|
|
||||||
|
**Title:** `[P2] [Task] Create a release checklist**
|
||||||
|
|
||||||
|
- [ ] Version bump convention
|
||||||
|
- [ ] CHANGELOG update step
|
||||||
|
- [ ] Tagging and release notes
|
||||||
|
- [ ] Post-deploy smoke-test commands
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filing Instructions
|
||||||
|
|
||||||
|
1. Open the **Issues** tab in this repo.
|
||||||
|
2. Pick the template that matches the ticket above.
|
||||||
|
3. Copy-paste the title, description, labels, and priority.
|
||||||
|
4. Submit.
|
||||||
|
|
||||||
|
Repeat until the entire backlog has been filed. Triage and assign each ticket in a follow-up pass.
|
||||||
|
|
||||||
84
README.md
84
README.md
@@ -1,62 +1,62 @@
|
|||||||
# Tickets
|
# Tickets
|
||||||
|
|
||||||
A lightweight CLI ticket tracker that stores tickets as JSON in your home directory (`~/.tickets/tickets.json`).
|
Central issue tracker and ticket backlog for the **Arcline Project**.
|
||||||
|
|
||||||
|
All work is tracked as **Gitea Issues** in this repository. No external tooling required — just open an issue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
### Creating a Ticket
|
||||||
# Install in development mode
|
|
||||||
pip install -e ".[dev]"
|
|
||||||
|
|
||||||
# Run tests
|
1. Go to the **Issues** tab in this repo.
|
||||||
pytest tests/ -v
|
2. Click **New Issue**.
|
||||||
```
|
3. Pick a template:
|
||||||
|
- 🐛 **Bug Report** — something is broken.
|
||||||
|
- ✨ **Feature Request** — propose something new.
|
||||||
|
- 📋 **Task / Epic** — a general task, spike, or umbrella issue.
|
||||||
|
4. Fill in the fields, attach labels, and submit.
|
||||||
|
|
||||||
## Commands
|
> The templates live in `.gitea/ISSUE_TEMPLATE/` if you want to tweak them.
|
||||||
|
|
||||||
| Command | Alias | Description |
|
### Labels
|
||||||
|----------------------|-------|------------------------|
|
|
||||||
| `tickets create` | `c` | Create a new ticket |
|
|
||||||
| `tickets list` | `ls` | List tickets |
|
|
||||||
| `tickets view <id>` | `v` | Show ticket details |
|
|
||||||
| `tickets update <id>`| `u` | Modify a ticket |
|
|
||||||
| `tickets delete <id>`| `rm` | Remove a ticket |
|
|
||||||
|
|
||||||
### Examples
|
| Label | Purpose |
|
||||||
|
|----------------|----------------------------------------------|
|
||||||
|
| `bug` | Defect or unexpected behaviour |
|
||||||
|
| `enhancement` | New capability or improvement |
|
||||||
|
| `task` | General work item |
|
||||||
|
| `epic` | Large body of work spanning multiple issues |
|
||||||
|
| `triage` | Needs prioritisation / hasn't been reviewed |
|
||||||
|
| `documentation`| Docs, READMEs, comments |
|
||||||
|
| `good first issue` | Beginner-friendly starting point |
|
||||||
|
| `blocked` | Waiting on something else |
|
||||||
|
| `wontfix` | Acknowledged but not planned |
|
||||||
|
|
||||||
```bash
|
### Prioritisation
|
||||||
# Create tickets
|
|
||||||
tickets create "Fix login bug" -d "Users can't sign in" -p high -a alice -t bug auth
|
|
||||||
tickets create "Add dark mode" -p low -t feature
|
|
||||||
|
|
||||||
# List all tickets
|
Prefix issue titles to signal urgency at a glance:
|
||||||
tickets list
|
|
||||||
|
|
||||||
# Filter by status or priority
|
- `[P0]` — Drop everything. Critical / blocking.
|
||||||
tickets list -s todo
|
- `[P1]` — Important. Should be in the current iteration.
|
||||||
tickets list -p critical
|
- `[P2]` — Nice to have. Backlog.
|
||||||
|
- `[P3]` — Someday / maybe.
|
||||||
|
|
||||||
# View details
|
Example: `[P1] [Bug] Login page returns 500 on `\n` in username`
|
||||||
tickets view abc12345
|
|
||||||
|
|
||||||
# Update a ticket
|
---
|
||||||
tickets update abc12345 -s done -a bob
|
|
||||||
tickets update abc12345 -t "Better title" -p critical
|
|
||||||
|
|
||||||
# Delete a ticket
|
## Workflow
|
||||||
tickets delete abc12345 -f
|
|
||||||
```
|
|
||||||
|
|
||||||
## Fields
|
1. **Open** an issue using a template.
|
||||||
|
2. **Triage** — assign labels, priority, milestone, and an owner.
|
||||||
|
3. **Work** — branch off `main`, reference the issue in commits (`#42`).
|
||||||
|
4. **Close** — when the work lands, close the issue (or let a PR close it automatically with `Closes #42`).
|
||||||
|
|
||||||
- **title** — short summary (required)
|
---
|
||||||
- **description** — longer details (optional)
|
|
||||||
- **priority** — `low`, `medium` (default), `high`, `critical`
|
|
||||||
- **status** — `todo` (default), `in_progress`, `done`, `cancelled`
|
|
||||||
- **assignee** — who's working on it (optional)
|
|
||||||
- **tags** — list of labels (optional)
|
|
||||||
|
|
||||||
## Storage
|
## Backlog
|
||||||
|
|
||||||
All tickets are persisted to `~/.tickets/tickets.json`. Since this is a plain JSON file, you can version-control it with git or back it up easily.
|
See [`ISSUES.md`](./ISSUES.md) for the initial curated backlog of tickets ready to be filed.
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
[build-system]
|
|
||||||
requires = ["setuptools>=68.0", "wheel"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
|
||||||
[project]
|
|
||||||
name = "tickets"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "A lightweight CLI ticket tracker stored in git"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
license = {text = "MIT"}
|
|
||||||
authors = [{name = "Arcline Dev", email = "dev@arcline-project.local"}]
|
|
||||||
|
|
||||||
[project.scripts]
|
|
||||||
tickets = "tickets.cli:entry_point"
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
|
||||||
dev = ["pytest>=8.0", "pytest-cov>=5.0"]
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
"""Tickets - A lightweight CLI ticket tracker."""
|
|
||||||
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
"""Command-line interface for the tickets tool."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
import textwrap
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
from tickets.models import Priority, Status, Ticket
|
|
||||||
from tickets.store import DEFAULT_STORE_FILE, DEFAULT_STORE_DIR, TicketStore
|
|
||||||
|
|
||||||
|
|
||||||
def _build_parser() -> argparse.ArgumentParser:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
prog="tickets",
|
|
||||||
description="Lightweight CLI ticket tracker. Tickets are stored as JSON.",
|
|
||||||
)
|
|
||||||
sub = parser.add_subparsers(dest="command", required=True)
|
|
||||||
|
|
||||||
# ---- create ----
|
|
||||||
p_create = sub.add_parser("create", aliases=["c"], help="Create a new ticket")
|
|
||||||
p_create.add_argument("title", help="Ticket title")
|
|
||||||
p_create.add_argument("-d", "--description", default="", help="Description")
|
|
||||||
p_create.add_argument("-p", "--priority", default="medium",
|
|
||||||
choices=[e.value for e in Priority], help="Priority level")
|
|
||||||
p_create.add_argument("-a", "--assignee", default="", help="Assignee")
|
|
||||||
p_create.add_argument("-t", "--tags", nargs="*", default=[], help="Tags")
|
|
||||||
|
|
||||||
# ---- list ----
|
|
||||||
p_list = sub.add_parser("list", aliases=["ls"], help="List tickets")
|
|
||||||
p_list.add_argument("-s", "--status", default=None,
|
|
||||||
choices=[e.value for e in Status], help="Filter by status")
|
|
||||||
p_list.add_argument("-p", "--priority", default=None,
|
|
||||||
choices=[e.value for e in Priority], help="Filter by priority")
|
|
||||||
|
|
||||||
# ---- view ----
|
|
||||||
p_view = sub.add_parser("view", aliases=["v", "show"], help="View a ticket")
|
|
||||||
p_view.add_argument("ticket_id", help="Ticket ID (short hash)")
|
|
||||||
|
|
||||||
# ---- update ----
|
|
||||||
p_update = sub.add_parser("update", aliases=["u"], help="Update a ticket")
|
|
||||||
p_update.add_argument("ticket_id", help="Ticket ID (short hash)")
|
|
||||||
p_update.add_argument("-t", "--title", default=None, help="New title")
|
|
||||||
p_update.add_argument("-d", "--description", default=None, help="New description")
|
|
||||||
p_update.add_argument("-p", "--priority", default=None,
|
|
||||||
choices=[e.value for e in Priority], help="New priority")
|
|
||||||
p_update.add_argument("-s", "--status", default=None,
|
|
||||||
choices=[e.value for e in Status], help="New status")
|
|
||||||
p_update.add_argument("-a", "--assignee", default=None, help="New assignee")
|
|
||||||
p_update.add_argument("--tags", nargs="*", default=None, help="New tags")
|
|
||||||
|
|
||||||
# ---- delete ----
|
|
||||||
p_delete = sub.add_parser("delete", aliases=["rm", "d"], help="Delete a ticket")
|
|
||||||
p_delete.add_argument("ticket_id", help="Ticket ID (short hash)")
|
|
||||||
p_delete.add_argument("--force", "-f", action="store_true", help="Skip confirmation")
|
|
||||||
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
def _format_ticket(ticket: Ticket) -> str:
|
|
||||||
"""Pretty-print a single ticket."""
|
|
||||||
header = (
|
|
||||||
f"{'=' * 60}\n"
|
|
||||||
f" #{ticket.id} {ticket.title}\n"
|
|
||||||
f"{'=' * 60}\n"
|
|
||||||
f" Status: {ticket.status.value:<12} Priority: {ticket.priority.value}\n"
|
|
||||||
)
|
|
||||||
if ticket.assignee:
|
|
||||||
header += f" Assignee: {ticket.assignee}\n"
|
|
||||||
if ticket.tags:
|
|
||||||
header += f" Tags: {', '.join(ticket.tags)}\n"
|
|
||||||
header += (
|
|
||||||
f" Created: {ticket.created_at}\n"
|
|
||||||
f" Updated: {ticket.updated_at}\n"
|
|
||||||
)
|
|
||||||
if ticket.description:
|
|
||||||
header += f"{'-' * 60}\n{textwrap.indent(ticket.description, ' ')}\n"
|
|
||||||
header += f"{'=' * 60}"
|
|
||||||
return header
|
|
||||||
|
|
||||||
|
|
||||||
def _format_ticket_short(ticket: Ticket) -> str:
|
|
||||||
"""Single-line ticket representation for list view."""
|
|
||||||
status_icon = {
|
|
||||||
Status.TODO: "[ ]",
|
|
||||||
Status.IN_PROGRESS: "[~]",
|
|
||||||
Status.DONE: "[✓]",
|
|
||||||
Status.CANCELLED: "[x]",
|
|
||||||
}.get(ticket.status, "[?]")
|
|
||||||
|
|
||||||
return (
|
|
||||||
f" {status_icon} #{ticket.id} "
|
|
||||||
f"{ticket.priority.value:<8} "
|
|
||||||
f"{ticket.title[:60]}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_create(store: TicketStore, args: argparse.Namespace) -> int:
|
|
||||||
ticket = Ticket(
|
|
||||||
title=args.title,
|
|
||||||
description=args.description,
|
|
||||||
priority=args.priority,
|
|
||||||
assignee=args.assignee,
|
|
||||||
tags=args.tags,
|
|
||||||
)
|
|
||||||
store.add(ticket)
|
|
||||||
print(f"Created ticket #{ticket.id}: {ticket.title}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_list(store: TicketStore, args: argparse.Namespace) -> int:
|
|
||||||
tickets = store.list_tickets(status=args.status, priority=args.priority)
|
|
||||||
if not tickets:
|
|
||||||
print("No tickets found.")
|
|
||||||
return 0
|
|
||||||
print(f"\n Tickets ({len(tickets)}):")
|
|
||||||
print(f" {'─' * 60}")
|
|
||||||
for t in tickets:
|
|
||||||
print(_format_ticket_short(t))
|
|
||||||
print()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_view(store: TicketStore, args: argparse.Namespace) -> int:
|
|
||||||
ticket = _resolve_ticket(store, args.ticket_id)
|
|
||||||
if ticket is None:
|
|
||||||
return 1
|
|
||||||
print(_format_ticket(ticket))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_update(store: TicketStore, args: argparse.Namespace) -> int:
|
|
||||||
ticket = _resolve_ticket(store, args.ticket_id)
|
|
||||||
if ticket is None:
|
|
||||||
return 1
|
|
||||||
ticket.update(
|
|
||||||
title=args.title,
|
|
||||||
description=args.description,
|
|
||||||
priority=args.priority,
|
|
||||||
status=args.status,
|
|
||||||
assignee=args.assignee,
|
|
||||||
tags=args.tags,
|
|
||||||
)
|
|
||||||
store[ticket.id] = ticket # persist
|
|
||||||
print(f"Updated ticket #{ticket.id}: {ticket.title}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_delete(store: TicketStore, args: argparse.Namespace) -> int:
|
|
||||||
ticket = _resolve_ticket(store, args.ticket_id)
|
|
||||||
if ticket is None:
|
|
||||||
return 1
|
|
||||||
if not args.force:
|
|
||||||
print(_format_ticket(ticket))
|
|
||||||
try:
|
|
||||||
confirm = input(f"\nDelete ticket #{ticket.id}? [y/N] ").strip().lower()
|
|
||||||
except (EOFError, KeyboardInterrupt):
|
|
||||||
print("\nCancelled.")
|
|
||||||
return 1
|
|
||||||
if confirm not in ("y", "yes"):
|
|
||||||
print("Cancelled.")
|
|
||||||
return 0
|
|
||||||
del store[ticket.id]
|
|
||||||
print(f"Deleted ticket #{ticket.id}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_ticket(store: TicketStore, ticket_id: str) -> Ticket | None:
|
|
||||||
if ticket_id not in store:
|
|
||||||
# Try prefix matching
|
|
||||||
matches = [tid for tid in store if tid.startswith(ticket_id)]
|
|
||||||
if len(matches) == 1:
|
|
||||||
return store[matches[0]]
|
|
||||||
if len(matches) > 1:
|
|
||||||
print(f"Ambiguous ID prefix '{ticket_id}'. Matches: {', '.join(matches)}")
|
|
||||||
return None
|
|
||||||
print(f"Ticket '#{ticket_id}' not found.")
|
|
||||||
return None
|
|
||||||
return store[ticket_id]
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
|
||||||
parser = _build_parser()
|
|
||||||
args = parser.parse_args(argv)
|
|
||||||
store = TicketStore()
|
|
||||||
|
|
||||||
handlers = {
|
|
||||||
"create": cmd_create,
|
|
||||||
"c": cmd_create,
|
|
||||||
"list": cmd_list,
|
|
||||||
"ls": cmd_list,
|
|
||||||
"view": cmd_view,
|
|
||||||
"v": cmd_view,
|
|
||||||
"show": cmd_view,
|
|
||||||
"update": cmd_update,
|
|
||||||
"u": cmd_update,
|
|
||||||
"delete": cmd_delete,
|
|
||||||
"rm": cmd_delete,
|
|
||||||
"d": cmd_delete,
|
|
||||||
}
|
|
||||||
|
|
||||||
handler = handlers.get(args.command)
|
|
||||||
if handler is None:
|
|
||||||
print(f"Unknown command: {args.command}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
return handler(store, args)
|
|
||||||
|
|
||||||
|
|
||||||
def entry_point() -> None:
|
|
||||||
"""Thin wrapper that calls main and exits; used as console_scripts entry."""
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
entry_point()
|
|
||||||
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
"""Ticket data models."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Self
|
|
||||||
|
|
||||||
|
|
||||||
class Priority(str, Enum):
|
|
||||||
LOW = "low"
|
|
||||||
MEDIUM = "medium"
|
|
||||||
HIGH = "high"
|
|
||||||
CRITICAL = "critical"
|
|
||||||
|
|
||||||
|
|
||||||
class Status(str, Enum):
|
|
||||||
TODO = "todo"
|
|
||||||
IN_PROGRESS = "in_progress"
|
|
||||||
DONE = "done"
|
|
||||||
CANCELLED = "cancelled"
|
|
||||||
|
|
||||||
|
|
||||||
class Ticket:
|
|
||||||
"""Represents a single ticket."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
title: str,
|
|
||||||
description: str = "",
|
|
||||||
priority: Priority = Priority.MEDIUM,
|
|
||||||
status: Status = Status.TODO,
|
|
||||||
assignee: str = "",
|
|
||||||
tags: list[str] | None = None,
|
|
||||||
ticket_id: str | None = None,
|
|
||||||
created_at: str | None = None,
|
|
||||||
updated_at: str | None = None,
|
|
||||||
) -> None:
|
|
||||||
self.id = ticket_id or str(uuid.uuid4())[:8]
|
|
||||||
self.title = title
|
|
||||||
self.description = description
|
|
||||||
self.priority = priority if isinstance(priority, Priority) else Priority(priority)
|
|
||||||
self.status = status if isinstance(status, Status) else Status(status)
|
|
||||||
self.assignee = assignee
|
|
||||||
self.tags: list[str] = tags or []
|
|
||||||
self.created_at = created_at or datetime.now(timezone.utc).isoformat()
|
|
||||||
self.updated_at = updated_at or self.created_at
|
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
|
||||||
return {
|
|
||||||
"id": self.id,
|
|
||||||
"title": self.title,
|
|
||||||
"description": self.description,
|
|
||||||
"priority": self.priority.value,
|
|
||||||
"status": self.status.value,
|
|
||||||
"assignee": self.assignee,
|
|
||||||
"tags": self.tags,
|
|
||||||
"created_at": self.created_at,
|
|
||||||
"updated_at": self.updated_at,
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: dict) -> Self:
|
|
||||||
return cls(
|
|
||||||
ticket_id=data["id"],
|
|
||||||
title=data["title"],
|
|
||||||
description=data.get("description", ""),
|
|
||||||
priority=data.get("priority", "medium"),
|
|
||||||
status=data.get("status", "todo"),
|
|
||||||
assignee=data.get("assignee", ""),
|
|
||||||
tags=data.get("tags", []),
|
|
||||||
created_at=data["created_at"],
|
|
||||||
updated_at=data["updated_at"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
title: str | None = None,
|
|
||||||
description: str | None = None,
|
|
||||||
priority: Priority | str | None = None,
|
|
||||||
status: Status | str | None = None,
|
|
||||||
assignee: str | None = None,
|
|
||||||
tags: list[str] | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Update mutable fields and bump updated_at."""
|
|
||||||
if title is not None:
|
|
||||||
self.title = title
|
|
||||||
if description is not None:
|
|
||||||
self.description = description
|
|
||||||
if priority is not None:
|
|
||||||
self.priority = priority if isinstance(priority, Priority) else Priority(priority)
|
|
||||||
if status is not None:
|
|
||||||
self.status = status if isinstance(status, Status) else Status(status)
|
|
||||||
if assignee is not None:
|
|
||||||
self.assignee = assignee
|
|
||||||
if tags is not None:
|
|
||||||
self.tags = tags
|
|
||||||
self.updated_at = datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
"""JSON-file-backed ticket persistence layer."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import MutableMapping
|
|
||||||
|
|
||||||
from tickets.models import Ticket
|
|
||||||
|
|
||||||
DEFAULT_STORE_DIR = Path.home() / ".tickets"
|
|
||||||
DEFAULT_STORE_FILE = "tickets.json"
|
|
||||||
|
|
||||||
|
|
||||||
class TicketStore(MutableMapping[str, Ticket]):
|
|
||||||
"""Dictionary-like store that persists tickets as JSON.
|
|
||||||
|
|
||||||
Tickets are keyed by their short ID string.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, filepath: str | Path | None = None) -> None:
|
|
||||||
if filepath is None:
|
|
||||||
filepath = DEFAULT_STORE_DIR / DEFAULT_STORE_FILE
|
|
||||||
self._filepath = Path(filepath)
|
|
||||||
self._tickets: dict[str, Ticket] = {}
|
|
||||||
self._load()
|
|
||||||
|
|
||||||
# -- dict-like interface ------------------------------------------------
|
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Ticket:
|
|
||||||
return self._tickets[key]
|
|
||||||
|
|
||||||
def __setitem__(self, key: str, value: Ticket) -> None:
|
|
||||||
self._tickets[key] = value
|
|
||||||
self._save()
|
|
||||||
|
|
||||||
def __delitem__(self, key: str) -> None:
|
|
||||||
del self._tickets[key]
|
|
||||||
self._save()
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return iter(self._tickets)
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self._tickets)
|
|
||||||
|
|
||||||
def __contains__(self, key: object) -> bool:
|
|
||||||
return key in self._tickets
|
|
||||||
|
|
||||||
# -- persistence --------------------------------------------------------
|
|
||||||
|
|
||||||
def _load(self) -> None:
|
|
||||||
if not self._filepath.exists():
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
data = json.loads(self._filepath.read_text(encoding="utf-8"))
|
|
||||||
except (json.JSONDecodeError, OSError):
|
|
||||||
return
|
|
||||||
if not isinstance(data, list):
|
|
||||||
return
|
|
||||||
self._tickets = {
|
|
||||||
t.id: t
|
|
||||||
for item in data
|
|
||||||
if isinstance(item, dict)
|
|
||||||
for t in [Ticket.from_dict(item)]
|
|
||||||
}
|
|
||||||
|
|
||||||
def _save(self) -> None:
|
|
||||||
self._filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
data = [t.to_dict() for t in self._tickets.values()]
|
|
||||||
self._filepath.write_text(
|
|
||||||
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
|
|
||||||
# -- helpers -------------------------------------------------------------
|
|
||||||
|
|
||||||
def list_tickets(
|
|
||||||
self,
|
|
||||||
status: str | None = None,
|
|
||||||
priority: str | None = None,
|
|
||||||
) -> list[Ticket]:
|
|
||||||
"""Return tickets, optionally filtered by status or priority."""
|
|
||||||
results = list(self._tickets.values())
|
|
||||||
if status is not None:
|
|
||||||
results = [t for t in results if t.status.value == status]
|
|
||||||
if priority is not None:
|
|
||||||
results = [t for t in results if t.priority.value == priority]
|
|
||||||
return sorted(results, key=lambda t: t.updated_at, reverse=True)
|
|
||||||
|
|
||||||
def add(self, ticket: Ticket) -> Ticket:
|
|
||||||
"""Add a ticket and persist immediately."""
|
|
||||||
self._tickets[ticket.id] = ticket
|
|
||||||
self._save()
|
|
||||||
return ticket
|
|
||||||
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
"""Tests for the tickets package."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from tickets.cli import main
|
|
||||||
from tickets.models import Priority, Status, Ticket
|
|
||||||
from tickets.store import TicketStore
|
|
||||||
|
|
||||||
|
|
||||||
class TestTicketModel:
|
|
||||||
def test_create_minimal_ticket(self) -> None:
|
|
||||||
t = Ticket(title="Fix login bug")
|
|
||||||
assert t.title == "Fix login bug"
|
|
||||||
assert t.description == ""
|
|
||||||
assert t.priority == Priority.MEDIUM
|
|
||||||
assert t.status == Status.TODO
|
|
||||||
assert t.id and len(t.id) == 8
|
|
||||||
|
|
||||||
def test_create_full_ticket(self) -> None:
|
|
||||||
t = Ticket(
|
|
||||||
title="Deploy v2.0",
|
|
||||||
description="Ship the new release",
|
|
||||||
priority=Priority.CRITICAL,
|
|
||||||
status=Status.IN_PROGRESS,
|
|
||||||
assignee="alice",
|
|
||||||
tags=["deploy", "urgent"],
|
|
||||||
)
|
|
||||||
assert t.priority == Priority.CRITICAL
|
|
||||||
assert t.status == Status.IN_PROGRESS
|
|
||||||
assert t.assignee == "alice"
|
|
||||||
assert "urgent" in t.tags
|
|
||||||
|
|
||||||
def test_roundtrip_dict(self) -> None:
|
|
||||||
t = Ticket(title="Test", description="desc", tags=["a", "b"])
|
|
||||||
rt = Ticket.from_dict(t.to_dict())
|
|
||||||
assert rt.title == t.title
|
|
||||||
assert rt.description == t.description
|
|
||||||
assert rt.tags == t.tags
|
|
||||||
assert rt.priority == t.priority
|
|
||||||
|
|
||||||
def test_update_fields(self) -> None:
|
|
||||||
t = Ticket(title="Old")
|
|
||||||
t.update(title="New", status="done", priority="high")
|
|
||||||
assert t.title == "New"
|
|
||||||
assert t.status == Status.DONE
|
|
||||||
assert t.priority == Priority.HIGH
|
|
||||||
assert t.updated_at != t.created_at
|
|
||||||
|
|
||||||
def test_accept_enum_or_string(self) -> None:
|
|
||||||
t = Ticket(title="X", priority=Priority.LOW, status=Status.DONE)
|
|
||||||
assert t.priority == Priority.LOW
|
|
||||||
t.update(priority="critical", status="cancelled")
|
|
||||||
assert t.priority == Priority.CRITICAL
|
|
||||||
assert t.status == Status.CANCELLED
|
|
||||||
|
|
||||||
|
|
||||||
class TestTicketStore:
|
|
||||||
def test_add_and_retrieve(self) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as d:
|
|
||||||
fp = Path(d) / "t.json"
|
|
||||||
store = TicketStore(fp)
|
|
||||||
t = Ticket(title="Hello")
|
|
||||||
store.add(t)
|
|
||||||
assert t.id in store
|
|
||||||
assert store[t.id].title == "Hello"
|
|
||||||
|
|
||||||
def test_delete(self) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as d:
|
|
||||||
fp = Path(d) / "t.json"
|
|
||||||
store = TicketStore(fp)
|
|
||||||
store.add(Ticket(title="rm me"))
|
|
||||||
tid = next(iter(store))
|
|
||||||
del store[tid]
|
|
||||||
assert tid not in store
|
|
||||||
|
|
||||||
def test_list_filtering(self) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as d:
|
|
||||||
fp = Path(d) / "t.json"
|
|
||||||
store = TicketStore(fp)
|
|
||||||
store.add(Ticket("a", status=Status.TODO, priority=Priority.LOW))
|
|
||||||
store.add(Ticket("b", status=Status.DONE, priority=Priority.HIGH))
|
|
||||||
store.add(Ticket("c", status=Status.DONE, priority=Priority.LOW))
|
|
||||||
assert len(store.list_tickets(status="done")) == 2
|
|
||||||
assert len(store.list_tickets(priority="high")) == 1
|
|
||||||
|
|
||||||
def test_persists_to_disk(self) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as d:
|
|
||||||
fp = Path(d) / "t.json"
|
|
||||||
store = TicketStore(fp)
|
|
||||||
store.add(Ticket("persist"))
|
|
||||||
assert fp.exists()
|
|
||||||
# Reload
|
|
||||||
store2 = TicketStore(fp)
|
|
||||||
assert len(store2) == 1
|
|
||||||
|
|
||||||
|
|
||||||
class TestCLI:
|
|
||||||
def test_create_and_view(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
fp = tmp_path / "t.json"
|
|
||||||
monkeypatch.setattr("tickets.cli.TicketStore", lambda: TicketStore(fp))
|
|
||||||
|
|
||||||
main(["create", "My ticket", "-d", "desc", "-p", "high"])
|
|
||||||
store = TicketStore(fp)
|
|
||||||
assert len(store) == 1
|
|
||||||
tid = next(iter(store))
|
|
||||||
|
|
||||||
main(["view", tid])
|
|
||||||
main(["list", "-s", "todo"])
|
|
||||||
main(["list"])
|
|
||||||
|
|
||||||
def test_update(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
fp = tmp_path / "t.json"
|
|
||||||
monkeypatch.setattr("tickets.cli.TicketStore", lambda: TicketStore(fp))
|
|
||||||
|
|
||||||
main(["create", "Old title"])
|
|
||||||
store = TicketStore(fp)
|
|
||||||
tid = next(iter(store))
|
|
||||||
|
|
||||||
main(["update", tid, "-t", "New title", "-s", "done"])
|
|
||||||
store2 = TicketStore(fp)
|
|
||||||
assert store2[tid].title == "New title"
|
|
||||||
assert store2[tid].status == Status.DONE
|
|
||||||
|
|
||||||
def test_delete_with_force(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
fp = tmp_path / "t.json"
|
|
||||||
monkeypatch.setattr("tickets.cli.TicketStore", lambda: TicketStore(fp))
|
|
||||||
|
|
||||||
main(["create", "To delete"])
|
|
||||||
store = TicketStore(fp)
|
|
||||||
assert len(store) == 1
|
|
||||||
tid = next(iter(store))
|
|
||||||
|
|
||||||
main(["delete", tid, "--force"])
|
|
||||||
store2 = TicketStore(fp)
|
|
||||||
assert len(store2) == 0
|
|
||||||
|
|
||||||
def test_prefix_match(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
fp = tmp_path / "t.json"
|
|
||||||
monkeypatch.setattr("tickets.cli.TicketStore", lambda: TicketStore(fp))
|
|
||||||
|
|
||||||
main(["create", "Ticket 1"])
|
|
||||||
store = TicketStore(fp)
|
|
||||||
tid = next(iter(store))
|
|
||||||
prefix = tid[:4]
|
|
||||||
|
|
||||||
# Should resolve via prefix
|
|
||||||
main(["view", prefix])
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user