diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..57b9845 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,42 @@ +name: Bug report +description: Report a reproducible problem with Nitpick. +title: "bug: " +labels: [bug] +body: + - type: markdown + attributes: + value: | + Please remove credentials, private logs, and proprietary source code before submitting. + - type: input + id: version + attributes: + label: Version or commit + placeholder: "0.1.0 or e71b3b7" + validations: + required: true + - type: input + id: python + attributes: + label: Python version + placeholder: "3.13" + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction steps + description: Include the smallest sanitized configuration or fixture that demonstrates the issue. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..1f39f20 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/FlyLikeAPenguin/nitpick/security/advisories/new + about: Report security issues privately; do not open a public issue. + - name: Roadmap and design discussions + url: https://github.com/FlyLikeAPenguin/nitpick/discussions + about: Discuss ideas and broader design questions with the community. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..b3ada3a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,23 @@ +name: Feature request +description: Suggest an improvement or new provider. +title: "feature: " +labels: [enhancement] +body: + - type: textarea + id: problem + attributes: + label: Problem to solve + description: What workflow is currently difficult, slow, or unsafe? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed outcome + description: Describe the behavior that would make the workflow better. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..a89f71b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ +## What changed? + + + +## Why? + + + +## Validation + +- [ ] `pytest` +- [ ] `ruff check .` +- [ ] Documentation updated where behavior changed +- [ ] No secrets, production logs, or private source code included + +## Risk and operations + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d979920 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + - name: Install development dependencies + run: python -m pip install --upgrade pip && python -m pip install -e ".[dev]" + - name: Run tests + run: pytest + - name: Run lint checks + run: ruff check . diff --git a/.gitignore b/.gitignore index f056019..5566d97 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ data/ logs/ .claude/ +*.egg-info/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c7fcbb1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to Nitpick are documented here. + +## [Unreleased] + +- Add safe-by-default automatic-fix controls. +- Add tests, CI, contributor guidance, and security reporting guidance. +- Improve the README with setup, safety, limitations, and development documentation. + +## [0.1.0] - 2026-07-18 + +- Initial public release of the Datadog-to-Claude incident investigation pipeline. +- Add local dashboard, SQLite deduplication, provider adapters, RCA sinks, issue tracking, notifications, and optional fix PRs. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f351e33 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,14 @@ +# Code of Conduct + +Nitpick is a small open-source project, and contributors are expected to make participation safe and welcoming. + +Examples of expected behavior: + +- Be respectful and constructive. +- Assume good intent while discussing technical trade-offs. +- Focus feedback on the code, documentation, or proposal—not the person. +- Respect privacy and never post credentials, private logs, or private source code. + +Harassment, discrimination, threats, deliberate disruption, and sharing someone else's private information are not acceptable. + +Report unacceptable behavior privately through the repository maintainer's GitHub profile. Security vulnerabilities should follow [SECURITY.md](SECURITY.md) instead. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..03168bd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Contributing to Nitpick + +Thanks for helping make incident response less repetitive. Small documentation, test, and provider improvements are welcome. + +## Before you start + +- Search existing issues before opening a new one. +- For a larger change, open an issue first so the design and scope are clear. +- Never include credentials, production log payloads, or private source code in an issue or pull request. + +## Local development + +Nitpick supports Python 3.12 and newer. + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -e ".[dev]" +pytest +ruff check . +``` + +The unit tests do not call Datadog, Claude, GitHub, Linear, Notion, or Slack. Keep external calls behind provider boundaries and use deterministic fixtures or mocks in tests. + +## Pull requests + +Please keep pull requests focused and include: + +- the problem and user impact +- a short explanation of the implementation +- tests added or updated +- documentation updates for changed behavior +- any security, privacy, or cost implications + +Generated fixes must remain reviewable pull requests. Do not add behavior that writes to a live checkout or bypasses the existing safety controls. + +## Good first contributions + +- Add provider contract tests. +- Improve the dashboard with accessible labels and keyboard support. +- Add sanitized log fixtures for additional error formats. +- Improve setup documentation for Linux and macOS scheduling. diff --git a/README.md b/README.md index 1468d57..2858f39 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,37 @@ # Nitpick -Automated error monitoring and investigation powered by Claude. Fetches errors from your log provider, investigates each with Claude CLI, documents findings, files issues, and optionally opens fix PRs — all on autopilot. +[![CI](https://github.com/FlyLikeAPenguin/nitpick/actions/workflows/ci.yml/badge.svg)](https://github.com/FlyLikeAPenguin/nitpick/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/) + +Datadog errors → Claude root-cause analysis → GitHub issues and reviewable fix PRs. + +Nitpick turns recurring production errors into deduplicated, actionable investigations. It runs locally, keeps costs visible, and never changes a live checkout during investigation. + +> Nitpick is alpha software. Review every generated issue and PR before merging. Automatic fixes are disabled by default. + +## Why Nitpick + +Incident response often starts with the same manual loop: search logs, find the relevant code, write an RCA, file a ticket, and decide whether a fix is safe. Nitpick automates that loop while keeping the evidence, budget, and proposed change visible to the team. + +- **Less duplicate work** — fingerprints group recurring errors before they consume investigation budget. +- **Actionable output** — each investigation can produce an RCA, issue, notification, and optional fix PR. +- **Local-first** — the dashboard and Markdown RCA sink run locally; provider credentials stay in environment variables. +- **Reviewable automation** — fixes happen in isolated git worktrees and are delivered as pull requests, never deployed directly. + +## Safety model + +Nitpick investigates errors with read-only access to configured code paths. Automatic fixes are **off by default**. To enable them, open the dashboard and turn on **Settings → Auto-fix & PR** for the relevant scope. A fix is still limited by a separate Claude budget, an isolated worktree, and a 250-line diff ceiling. + +Before using Nitpick with production logs, review the data flow: matching log entries and configured source-code paths are supplied to Claude CLI, while provider APIs receive only the records needed to create the configured RCA, issue, or notification. + +## Project status + +Nitpick is an actively developed alpha project. The core pipeline, local dashboard, Datadog source, GitHub/Linear trackers, Markdown/Notion sinks, and console/Slack notifiers are implemented. See [issues](https://github.com/FlyLikeAPenguin/nitpick/issues) for current work and limitations. + +![Nitpick workflow preview](docs/social-preview.svg) + +See the [roadmap](docs/ROADMAP.md) for planned work and the [GitHub issues](https://github.com/FlyLikeAPenguin/nitpick/issues) for implementation-level tasks. ## How it works @@ -48,12 +79,13 @@ With the defaults, you need only **Datadog + Claude CLI + `gh`** to get full val ## Quickstart ```bash -git clone && cd nitpick +git clone https://github.com/FlyLikeAPenguin/nitpick.git && cd nitpick python -m venv .venv && source .venv/bin/activate pip install -e . cp .env.example .env -# Edit .env — fill in your Datadog keys at minimum +# Edit .env — fill in your Datadog keys at minimum. +# Start with NITPICK_DRY_RUN=true while validating the setup. ``` Edit `config.yml` to add your services: @@ -88,6 +120,9 @@ NITPICK_DRY_RUN=true python -m src.main run # Start dashboard python -m src.main serve + +# Preview the dashboard without Datadog or Claude credentials +python -m src.main demo ``` ## Configuration @@ -171,7 +206,26 @@ Available at `http://localhost:8111` when running `python -m src.main serve`. - **Costs** — Daily cost chart for the last 30 days. - **Logs** — Live tail of the pipeline log. - **Services** — Add/remove monitored services (edits `config.yml`). -- **Settings** — Toggle investigations, fixes, and notifications globally or per-service. +- **Settings** — Toggle investigations, auto-fix PRs, and notifications globally or per-service. Auto-fix PRs start disabled. + +## Known limitations + +- Datadog is currently the only error-source provider. +- Claude CLI must be installed and authenticated separately. +- The dashboard is intentionally bound to `127.0.0.1`; it is not an authenticated multi-user web service. +- Generated fixes are proposals. Your normal review, CI, and deployment controls still apply. + +## Development + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -e ".[dev]" +pytest +ruff check . +``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow, issue expectations, and pull-request checklist. Please use [SECURITY.md](SECURITY.md) for vulnerability reports rather than opening a public issue. ## Scheduling @@ -211,6 +265,7 @@ Create a timer unit for the pipeline and a service unit for the dashboard. ├── .env # Credentials (not committed) ├── src/ │ ├── main.py # CLI + pipeline orchestrator +│ ├── demo.py # Credential-free dashboard demo data │ ├── config.py # Env + YAML config loader │ ├── models.py # Dataclasses │ ├── fingerprint.py # Error normalisation + hashing @@ -229,6 +284,8 @@ Create a timer unit for the pipeline and a service unit for the dashboard. │ └── notifiers/slack.py # Slack webhook notifier ├── static/ │ └── index.html # Single-page dashboard UI +├── tests/ # Fast unit and integration-boundary tests +├── .github/ # CI, issue forms, and PR guidance ├── data/ │ └── error_cache.db # SQLite database (created on first run) └── logs/ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..14c6a82 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,18 @@ +# Security policy + +## Reporting a vulnerability + +Please do not open a public issue for a security vulnerability. Use [GitHub's private vulnerability reporting](https://github.com/FlyLikeAPenguin/nitpick/security/advisories/new) when available, or contact the maintainer through the GitHub profile. + +Include: + +- the affected version or commit +- a description of the issue and its impact +- reproducible steps or a minimal proof of concept +- any suggested mitigation + +Please redact API keys, tokens, production log contents, and private source code. We will acknowledge reports as soon as practical and coordinate disclosure after a fix or mitigation is available. + +## Scope notes + +Nitpick handles error logs and can provide configured code paths to Claude CLI. Treat those inputs as sensitive, review provider permissions, and run the dashboard only on trusted local interfaces. The dashboard is bound to `127.0.0.1` by default but is not an authentication boundary. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..36f3fd7 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,8 @@ +# Getting help + +- Check the [README](README.md) and [known limitations](README.md#known-limitations) first. +- Search [existing issues](https://github.com/FlyLikeAPenguin/nitpick/issues) before opening a new one. +- Use the bug report form for reproducible defects. +- Use a feature request or discussion for design questions and ideas. + +Never include credentials, private logs, or proprietary source code in public support requests. diff --git a/config.yml b/config.yml index dd1f839..652b401 100644 --- a/config.yml +++ b/config.yml @@ -26,3 +26,6 @@ repos: ignore_patterns: - "DeprecationWarning" - "health_check" + +# Automatic fix branches and PRs are disabled by default. +# Enable them deliberately from the dashboard after reviewing the safety model. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..65785f6 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,24 @@ +# Roadmap + +The roadmap is intentionally short and outcome-focused. Priorities may change as users try Nitpick and report what blocks adoption. + +## Now + +- Make the safe, reviewable workflow easy to install and understand. +- Improve test coverage around provider contracts, cache migrations, and error normalization. +- Document privacy, permissions, cost controls, and operational limitations. + +## Next + +- Add a fixture-backed demo mode that works without Datadog or Claude credentials. +- Add more error-source providers behind the existing provider interface. +- Improve dashboard accessibility and run history details. +- Add release automation and compatibility checks for supported Python versions. + +## Later + +- Support richer incident timelines and related-error grouping. +- Add configurable approval gates before issue creation or fix-PR creation. +- Evaluate hosted or team-oriented deployment only after the local workflow is stable. + +Have a use case that should influence the order? [Open an issue](https://github.com/FlyLikeAPenguin/nitpick/issues/new/choose) with the problem and the desired outcome. diff --git a/docs/social-preview.svg b/docs/social-preview.svg new file mode 100644 index 0000000..e5c4298 --- /dev/null +++ b/docs/social-preview.svg @@ -0,0 +1,48 @@ + + Nitpick — incident triage for developers + Datadog errors become Claude root-cause analysis and reviewable pull requests. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NITPICK + Datadog errors → Claude RCA → reviewable PRs + SAFE BY DEFAULT • LOCAL DASHBOARD • OPEN SOURCE + + diff --git a/pyproject.toml b/pyproject.toml index b24e525..c9d3b15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,56 @@ [project] name = "nitpick" version = "0.1.0" +description = "Safe, reviewable incident triage from Datadog errors to Claude investigations" +readme = "README.md" +license = { file = "LICENSE" } requires-python = ">=3.12" +keywords = ["incident-response", "observability", "sre", "datadog", "claude-code", "root-cause-analysis"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Bug Tracking", +] dependencies = [ "requests>=2.31", "pyyaml>=6.0", "python-dotenv>=1.0", ] +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "ruff>=0.6", +] + +[project.urls] +Homepage = "https://github.com/FlyLikeAPenguin/nitpick" +Repository = "https://github.com/FlyLikeAPenguin/nitpick" +Issues = "https://github.com/FlyLikeAPenguin/nitpick/issues" + +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +include = ["src*"] + [project.scripts] nitpick = "src.main:cli" + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-ra" + +[tool.ruff] +target-version = "py312" +line-length = 100 + +[tool.ruff.lint] +select = ["E", "F", "I"] +ignore = ["E501"] diff --git a/src/cache.py b/src/cache.py index 8e321c7..8bfd782 100644 --- a/src/cache.py +++ b/src/cache.py @@ -92,7 +92,9 @@ # Defaults for settings that can be toggled from the dashboard SETTINGS_DEFAULTS: dict[str, str] = { "investigations_enabled": "true", - "fixes_enabled": "true", + # Creating branches, pushing code, and opening PRs always requires an + # explicit opt-in from the dashboard. + "fixes_enabled": "false", "slack_enabled": "true", "max_errors_per_run": "10", "max_investigations_per_day": "50", diff --git a/src/dashboard.py b/src/dashboard.py index 9eb7499..af0c3de 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -12,7 +12,7 @@ import yaml from .cache import ErrorCache -from .config import Config, PROJECT_ROOT +from .config import PROJECT_ROOT, Config log = logging.getLogger(__name__) diff --git a/src/demo.py b/src/demo.py new file mode 100644 index 0000000..1d5b146 --- /dev/null +++ b/src/demo.py @@ -0,0 +1,123 @@ +"""Seed a credential-free dashboard demo with deterministic sample data.""" + +from __future__ import annotations + +from dataclasses import replace +from datetime import UTC, datetime, timedelta +from pathlib import Path +from uuid import NAMESPACE_URL, uuid5 + +from .cache import ErrorCache +from .config import Config +from .models import ErrorGroup, Investigation + +DEMO_DB_NAME = "demo_error_cache.db" +DEMO_RUN_ID = "demo-run-001" + + +def demo_config(config: Config) -> Config: + """Return a config that points the dashboard at the isolated demo database.""" + return replace(config, db_path=config.db_path.with_name(DEMO_DB_NAME)) + + +def seed_demo_data(db_path: Path) -> None: + """Create a small, repeatable dashboard dataset without external services.""" + cache = ErrorCache(db_path) + now = datetime.now(UTC).replace(microsecond=0) + + groups = [ + ErrorGroup( + fingerprint="demo-checkout-timeout", + service="checkout-api", + error_type="TimeoutError", + message_template="Payment provider timed out after ms", + region="us", + first_seen=now - timedelta(hours=3), + last_seen=now - timedelta(minutes=8), + occurrence_count=37, + status="fixed", + sample_log={"message": "Payment provider timed out after 30000ms"}, + ), + ErrorGroup( + fingerprint="demo-worker-decode", + service="background-worker", + error_type="JSONDecodeError", + message_template="Expected value at line column ", + region="eu", + first_seen=now - timedelta(hours=1), + last_seen=now - timedelta(minutes=22), + occurrence_count=12, + status="documented", + sample_log={"message": "Expected value at line 1 column 2"}, + ), + ErrorGroup( + fingerprint="demo-health-check", + service="catalog-api", + error_type="ConnectionResetError", + message_template="Health-check connection reset by peer", + region="us", + first_seen=now - timedelta(minutes=45), + last_seen=now - timedelta(minutes=41), + occurrence_count=4, + status="new", + sample_log={"message": "Health-check connection reset by peer"}, + ), + ] + for group in groups: + cache.upsert_error(group) + cache.update_error_status(group.fingerprint, group.status) + + investigations = [ + Investigation( + id=str(uuid5(NAMESPACE_URL, "nitpick-demo-checkout-timeout")), + fingerprint="demo-checkout-timeout", + started_at=now - timedelta(hours=2, minutes=55), + completed_at=now - timedelta(hours=2, minutes=54), + root_cause="The payment client uses a 30-second timeout while the provider regularly responds more slowly during peak traffic.", + is_actionable=True, + is_fixable=True, + estimated_fix_lines=8, + severity="high", + affected_files=["src/payments/client.py"], + fix_description="Use the configured provider timeout and retry transient gateway timeouts.", + recommendation="Review the generated fix PR and add a provider-timeout regression test.", + claude_cost_usd=0.18, + claude_input_tokens=2200, + claude_output_tokens=540, + status="complete", + pr_status="merged", + ), + Investigation( + id=str(uuid5(NAMESPACE_URL, "nitpick-demo-worker-decode")), + fingerprint="demo-worker-decode", + started_at=now - timedelta(minutes=58), + completed_at=now - timedelta(minutes=57), + root_cause="A partner payload occasionally arrives empty during a retry window.", + is_actionable=True, + is_fixable=False, + severity="medium", + affected_files=["src/worker/decoder.py"], + recommendation="Treat empty retry payloads as a deferred message and alert if the rate increases.", + claude_cost_usd=0.11, + claude_input_tokens=1500, + claude_output_tokens=390, + status="complete", + ), + ] + for investigation in investigations: + cache.save_investigation(investigation) + + if not any(run["run_id"] == DEMO_RUN_ID for run in cache.get_runs()): + cache.log_run( + DEMO_RUN_ID, + now - timedelta(hours=3), + now - timedelta(hours=2, minutes=54), + { + "errors_fetched": 53, + "new_errors": 3, + "investigated": 2, + "issues_created": 2, + "prs_created": 1, + "total_cost_usd": 0.29, + }, + ) diff --git a/src/fingerprint.py b/src/fingerprint.py index 1066456..75a5db9 100644 --- a/src/fingerprint.py +++ b/src/fingerprint.py @@ -2,7 +2,6 @@ import hashlib import re -from collections import defaultdict from datetime import datetime, timezone from typing import Any diff --git a/src/fixer.py b/src/fixer.py index e4cc994..3b8874b 100644 --- a/src/fixer.py +++ b/src/fixer.py @@ -1,11 +1,9 @@ from __future__ import annotations -import json import logging import shutil import subprocess from pathlib import Path -from typing import Any from .config import Config from .investigator import parse_claude_output @@ -190,11 +188,6 @@ def attempt_fix( investigation.claude_output_tokens += fix_usage.get("output_tokens", 0) # Validate diff size - diff_stat = subprocess.run( - ["git", "diff", "--stat", f"origin/{default_branch}"], - cwd=str(worktree_path), capture_output=True, text=True, timeout=10, - ) - diff_numstat = subprocess.run( ["git", "diff", "--numstat", f"origin/{default_branch}"], cwd=str(worktree_path), capture_output=True, text=True, timeout=10, diff --git a/src/investigator.py b/src/investigator.py index 4854bb7..8549d32 100644 --- a/src/investigator.py +++ b/src/investigator.py @@ -3,7 +3,6 @@ import json import logging import subprocess -import tempfile from datetime import datetime, timezone from typing import Any from uuid import uuid4 diff --git a/src/main.py b/src/main.py index 5f55003..f08821b 100644 --- a/src/main.py +++ b/src/main.py @@ -9,6 +9,7 @@ from .cache import ErrorCache from .config import Config, load_config, validate_config from .dashboard import run_dashboard +from .demo import demo_config, seed_demo_data from .fingerprint import build_dd_link, fingerprint_and_group from .fixer import attempt_fix, cleanup_stale_worktrees from .investigator import investigate @@ -234,9 +235,10 @@ def run_pipeline(config: Config) -> None: def cli() -> None: if len(sys.argv) < 2: - print("Usage: python -m src.main ") + print("Usage: python -m src.main ") print(" run — execute one pipeline cycle") print(" serve — start the monitoring dashboard") + print(" demo — seed a credential-free dashboard demo") sys.exit(1) config = load_config() @@ -253,9 +255,15 @@ def cli() -> None: elif command == "serve": run_dashboard(config) + elif command == "demo": + demo_cfg = demo_config(config) + seed_demo_data(demo_cfg.db_path) + print(f"Demo data ready at {demo_cfg.db_path}") + run_dashboard(demo_cfg) + else: print(f"Unknown command: {command}") - print("Usage: python -m src.main ") + print("Usage: python -m src.main ") sys.exit(1) diff --git a/static/index.html b/static/index.html index 2a9ce95..4ea6c72 100644 --- a/static/index.html +++ b/static/index.html @@ -264,7 +264,7 @@

Global Settings

Auto-fix & PR
-
Attempt automated fixes and open PRs for fixable errors
+
Attempt automated fixes and open PRs for fixable errors (off by default)
Slack notifications
diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 0000000..68968ed --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,26 @@ +from pathlib import Path + +from src.cache import SETTINGS_DEFAULTS, ErrorCache +from src.demo import seed_demo_data + + +def test_auto_fixes_are_disabled_by_default() -> None: + assert SETTINGS_DEFAULTS["fixes_enabled"] == "false" + + +def test_new_cache_uses_safe_fix_default(tmp_path: Path) -> None: + cache = ErrorCache(tmp_path / "cache.db") + + assert cache.get_setting("fixes_enabled") == "false" + + +def test_demo_data_is_repeatable_and_has_dashboard_records(tmp_path: Path) -> None: + db_path = tmp_path / "demo.db" + + seed_demo_data(db_path) + seed_demo_data(db_path) + cache = ErrorCache(db_path) + + assert cache.get_stats()["total_errors"] == 3 + assert len(cache.get_investigations()) == 2 + assert len(cache.get_runs()) == 1 diff --git a/tests/test_fingerprint.py b/tests/test_fingerprint.py new file mode 100644 index 0000000..f8adc5c --- /dev/null +++ b/tests/test_fingerprint.py @@ -0,0 +1,23 @@ +from src.fingerprint import compute_fingerprint, normalize_message + + +def test_normalize_message_removes_dynamic_identifiers() -> None: + message = "request 123456 failed for 10.2.3.4 at 2026-07-18T09:10:11Z" + + assert normalize_message(message) == "request failed for at " + + +def test_fingerprint_is_stable_for_dynamic_values() -> None: + first = compute_fingerprint( + "payments", + "TimeoutError", + "request 123456 failed at 2026-07-18T09:10:11Z", + ) + second = compute_fingerprint( + "payments", + "TimeoutError", + "request 987654 failed at 2026-07-18T09:11:12Z", + ) + + assert first == second + diff --git a/tests/test_investigator.py b/tests/test_investigator.py new file mode 100644 index 0000000..0151600 --- /dev/null +++ b/tests/test_investigator.py @@ -0,0 +1,18 @@ +import json + +from src.investigator import parse_claude_output + + +def test_parse_claude_output_extracts_result_and_usage() -> None: + stdout = json.dumps( + { + "result": '```json\n{"root_cause": "database timeout"}\n```', + "usage": {"input_tokens": 12, "output_tokens": 8}, + "total_cost_usd": 0.04, + } + ) + + result, usage = parse_claude_output(stdout) + + assert result == {"root_cause": "database timeout"} + assert usage == {"input_tokens": 12, "output_tokens": 8, "cost_usd": 0.04}