Expand doctor activation diagnostics

This commit is contained in:
Himanshu Dongre 2026-05-17 00:26:03 +05:30
parent f4062da76e
commit 448484c4c3
5 changed files with 285 additions and 2 deletions

View file

@ -2,6 +2,7 @@ import logging
import pathlib
import re
import subprocess
from urllib.parse import urlsplit
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
@ -9,6 +10,21 @@ from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
def _database_runtime_status() -> dict:
"""Return safe-to-expose database mode metadata for diagnostics."""
resolved_url = settings.resolved_database_url
status = {
"mode": settings.db_mode,
"url_scheme": urlsplit(resolved_url).scheme or "unknown",
}
if settings.db_mode == "local":
status["local_db_path"] = str(settings.local_db_path)
else:
# Do not expose DATABASE_URL: it may contain credentials.
status["database_url_set"] = bool(settings.database_url.strip())
return status
def _resolve_git_sha() -> str:
"""Return the short git SHA of the backend directory, or 'unknown'."""
try:
@ -99,20 +115,27 @@ def create_app() -> FastAPI:
"compact_state", # --compact mode on state brief
"worktrees", # /api/v5/worktrees
"worktree_binding", # claims can bind to worktrees + state drift summary
"activation_health", # /health includes DB mode + provider confidence
]
@app.get("/health")
async def health_check():
from app.config_loader import providers_status
return {
"status": "ok",
"git_sha": _git_sha,
"capabilities": _capabilities,
"database": _database_runtime_status(),
"providers": providers_status(),
}
@app.on_event("startup")
async def startup_event():
from app.config_loader import providers_status
import logging
from app.config_loader import providers_status
logger = logging.getLogger("smriti.startup")
status = providers_status()

View file

@ -18,6 +18,9 @@ def test_health_returns_capabilities(client):
assert "capabilities" in data
assert isinstance(data["capabilities"], list)
assert data["database"]["mode"] in {"local", "postgres"}
assert "url_scheme" in data["database"]
assert "background_intelligence" in data["providers"]
def test_health_includes_required_capabilities(client):
@ -36,6 +39,22 @@ def test_health_includes_required_capabilities(client):
"compact_state",
"worktrees",
"worktree_binding",
"activation_health",
]
for cap in required:
assert cap in caps, f"Missing capability: {cap}"
def test_health_exposes_safe_database_status(client):
"""Database status is useful for doctor without exposing credentials."""
r = client.get("/health")
data = r.json()
db = data["database"]
assert db["mode"] in {"local", "postgres"}
if db["mode"] == "local":
assert db["local_db_path"]
assert "database_url_set" not in db
else:
assert "database_url_set" in db
assert "DATABASE_URL" not in db

View file

@ -756,6 +756,7 @@ def format_doctor(report: dict) -> str:
"""Readable diagnostics for backend/runtime consistency."""
backend = report.get("backend") or {}
local = report.get("local") or {}
cli = report.get("cli") or {}
checks = report.get("checks") or {}
hints = report.get("hints") or []
@ -771,6 +772,46 @@ def format_doctor(report: dict) -> str:
parts.append(f"Backend error: {backend.get('error') or 'unknown'}")
parts.append("")
parts.append("## Runtime")
database = backend.get("database") or {}
if database:
mode = database.get("mode") or "unknown"
parts.append(f"Database mode: `{mode}`")
if mode == "local":
parts.append(f"Local DB path: `{database.get('local_db_path') or 'unknown'}`")
elif mode == "postgres":
configured = database.get("database_url_set")
configured_label = "yes" if configured else "using default"
parts.append(f"Postgres DATABASE_URL set: {configured_label}")
if database.get("url_scheme"):
parts.append(f"Database URL scheme: `{database.get('url_scheme')}`")
else:
parts.append("Database mode: unknown")
providers = backend.get("providers") or {}
bg = providers.get("background_intelligence") if providers else None
if bg:
status = "ready" if bg.get("configured") else "mock/disabled"
provider = bg.get("provider") or "unknown"
model = bg.get("model") or "unknown"
parts.append(f"Background intelligence: {status} (`{provider}` / `{model}`)")
else:
parts.append("Background intelligence: unknown")
parts.append("")
parts.append("## CLI")
parts.append(f"Executable: `{cli.get('executable') or 'unknown'}`")
parts.append(f"`smriti` on PATH: `{cli.get('path_entry') or 'not found'}`")
parts.append(f"Package version: `{cli.get('package_version') or 'unknown'}`")
path_match = cli.get("path_matches_executable")
if path_match is True:
parts.append("PATH matches executable: yes")
elif path_match is False:
parts.append("PATH matches executable: no")
else:
parts.append("PATH matches executable: unknown")
parts.append("")
parts.append("## Local repo")
parts.append(f"Branch: `{local.get('branch') or 'unknown'}`")
parts.append(f"HEAD: `{local.get('git_sha_short') or local.get('git_sha') or 'unknown'}`")
@ -786,6 +827,8 @@ def format_doctor(report: dict) -> str:
parts.append("- missing capabilities: " + ", ".join(f"`{c}`" for c in missing))
else:
parts.append("- missing capabilities: none")
parts.append(f"- CLI path: {checks.get('cli_path') or 'unknown'}")
parts.append(f"- background provider: {checks.get('background_provider') or 'unknown'}")
parts.append("")
parts.append("## Capabilities")

View file

@ -51,8 +51,11 @@ from __future__ import annotations
import argparse
import json
import os
import shutil
import subprocess
import sys
from importlib import metadata
from pathlib import Path
from typing import Any
from .client import SmritiClient, SmritiError
@ -115,6 +118,7 @@ EXPECTED_HEALTH_CAPABILITIES = {
"compact_state",
"worktrees",
"worktree_binding",
"activation_health",
}
@ -142,6 +146,55 @@ def _git_sha_matches(backend_sha: str | None, local_sha: str | None) -> bool | N
return local.startswith(backend) or backend.startswith(local)
def _resolve_executable_path(value: str | None) -> str | None:
"""Resolve a command path for diagnostics without requiring it to exist."""
if not value:
return None
candidate = Path(value)
if not candidate.is_absolute():
found = shutil.which(value)
if found:
candidate = Path(found)
try:
return str(candidate.expanduser().resolve())
except OSError:
return str(candidate)
def _package_version() -> str | None:
try:
return metadata.version("smriti-cli")
except metadata.PackageNotFoundError:
return None
def _build_cli_info() -> dict:
"""Return local CLI-source details so stale PATH wrappers are visible."""
invoked = _resolve_executable_path(sys.argv[0])
path_entry = _resolve_executable_path(shutil.which("smriti"))
match: bool | None
if invoked and path_entry:
match = invoked == path_entry
elif invoked or path_entry:
match = False
else:
match = None
return {
"executable": invoked,
"path_entry": path_entry,
"path_matches_executable": match,
"package_version": _package_version(),
}
def _background_provider_check(providers: dict) -> str:
bg = providers.get("background_intelligence") if providers else None
if not bg:
return "unknown"
return "ready" if bg.get("configured") else "mock_or_disabled"
def _build_doctor_report(client: SmritiClient) -> dict:
"""Build a small diagnostics report without attempting repairs."""
local_sha = _git_output("rev-parse", "HEAD")
@ -165,9 +218,12 @@ def _build_doctor_report(client: SmritiClient) -> dict:
"git_sha_short": local_short,
"branch": local_branch,
},
"cli": _build_cli_info(),
"checks": {
"runtime_match": "unknown",
"missing_capabilities": None,
"cli_path": "unknown",
"background_provider": "unknown",
},
"hints": [],
}
@ -177,12 +233,23 @@ def _build_doctor_report(client: SmritiClient) -> dict:
except SmritiError as e:
report["backend"]["error"] = str(e)
report["hints"].append(
"Backend is not reachable; start Smriti and rerun `smriti doctor`."
"Backend is not reachable; start Smriti with `make dev-local` "
"for solo/local mode or `make dev-postgres` for shared/team mode, "
"then rerun `smriti doctor`."
)
cli_info = report.get("cli") or {}
if cli_info.get("path_matches_executable") is False:
report["checks"]["cli_path"] = "mismatch"
report["hints"].append(
"The `smriti` on PATH differs from this doctor executable. "
"Activate the intended environment or update PATH before daily use."
)
return report
capabilities = sorted(health.get("capabilities") or [])
backend_sha = health.get("git_sha")
database = health.get("database") or {}
providers = health.get("providers") or {}
missing = sorted(EXPECTED_HEALTH_CAPABILITIES - set(capabilities))
match = _git_sha_matches(backend_sha, local_sha)
@ -191,9 +258,25 @@ def _build_doctor_report(client: SmritiClient) -> dict:
"status": health.get("status"),
"git_sha": backend_sha,
"capabilities": capabilities,
"database": database,
"providers": providers,
"error": None,
})
report["checks"]["missing_capabilities"] = missing
report["checks"]["background_provider"] = _background_provider_check(providers)
cli_info = report.get("cli") or {}
cli_match = cli_info.get("path_matches_executable")
if cli_match is True:
report["checks"]["cli_path"] = "ok"
elif cli_match is False:
report["checks"]["cli_path"] = "mismatch"
report["hints"].append(
"The `smriti` on PATH differs from this doctor executable. "
"Activate the intended environment or update PATH before daily use."
)
else:
report["checks"]["cli_path"] = "unknown"
if match is True:
report["checks"]["runtime_match"] = "ok"
@ -219,6 +302,24 @@ def _build_doctor_report(client: SmritiClient) -> dict:
+ ", ".join(missing)
+ ". Pull/restart the backend if you expected newer behavior."
)
if not database:
report["hints"].append(
"Backend /health did not report database mode; restart after updating "
"the backend if you expected activation diagnostics."
)
bg = providers.get("background_intelligence") if providers else None
if not bg:
report["hints"].append(
"Backend /health did not report provider status; provider/mock "
"confidence is unknown."
)
elif not bg.get("configured"):
provider = bg.get("provider") or "background"
report["hints"].append(
f"Background intelligence provider `{provider}` is not configured; "
"checkpoint extract/review flows may use mock output or fail."
)
return report

View file

@ -28,11 +28,42 @@ def _patch_git(monkeypatch: pytest.MonkeyPatch, *, sha: str = "c470947abcdef", b
monkeypatch.setattr(cli_main, "_git_output", lambda *args: values.get(args))
def _patch_cli(
monkeypatch: pytest.MonkeyPatch,
*,
executable: str = "/repo/backend/.venv/bin/smriti",
path_entry: str = "/repo/backend/.venv/bin/smriti",
version: str = "0.1.0",
) -> None:
monkeypatch.setattr(
cli_main,
"_build_cli_info",
lambda: {
"executable": executable,
"path_entry": path_entry,
"path_matches_executable": executable == path_entry,
"package_version": version,
},
)
def _health(**overrides) -> dict:
base = {
"status": "ok",
"git_sha": "c470947",
"capabilities": sorted(cli_main.EXPECTED_HEALTH_CAPABILITIES),
"database": {
"mode": "local",
"url_scheme": "sqlite",
"local_db_path": "/Users/test/.smriti/smriti.db",
},
"providers": {
"background_intelligence": {
"provider": "openai",
"model": "gpt-4o-mini",
"configured": True,
}
},
}
base.update(overrides)
return base
@ -49,15 +80,21 @@ def test_doctor_parser_wiring():
def test_doctor_report_ok(monkeypatch: pytest.MonkeyPatch):
_patch_git(monkeypatch)
_patch_cli(monkeypatch)
client = _client(_health())
report = cli_main._build_doctor_report(client)
assert report["backend"]["reachable"] is True
assert report["backend"]["git_sha"] == "c470947"
assert report["backend"]["database"]["mode"] == "local"
assert report["backend"]["providers"]["background_intelligence"]["configured"] is True
assert report["local"]["git_sha_short"] == "c470947"
assert report["cli"]["path_matches_executable"] is True
assert report["checks"]["runtime_match"] == "ok"
assert report["checks"]["missing_capabilities"] == []
assert report["checks"]["cli_path"] == "ok"
assert report["checks"]["background_provider"] == "ready"
assert report["hints"] == []
@ -65,6 +102,7 @@ def test_doctor_report_flags_mismatch_and_missing_capability(
monkeypatch: pytest.MonkeyPatch,
):
_patch_git(monkeypatch, sha="c470947abcdef")
_patch_cli(monkeypatch)
capabilities = sorted(cli_main.EXPECTED_HEALTH_CAPABILITIES - {"worktree_binding"})
client = _client(_health(git_sha="deadbee", capabilities=capabilities))
@ -76,11 +114,48 @@ def test_doctor_report_flags_mismatch_and_missing_capability(
assert any("missing capabilities" in hint for hint in report["hints"])
def test_doctor_report_flags_cli_path_mismatch(monkeypatch: pytest.MonkeyPatch):
_patch_git(monkeypatch)
_patch_cli(
monkeypatch,
executable="/repo/backend/.venv/bin/smriti",
path_entry="/Users/test/.local/bin/smriti",
)
client = _client(_health())
report = cli_main._build_doctor_report(client)
assert report["checks"]["cli_path"] == "mismatch"
assert any("PATH differs" in hint for hint in report["hints"])
def test_doctor_report_flags_background_mock_risk(monkeypatch: pytest.MonkeyPatch):
_patch_git(monkeypatch)
_patch_cli(monkeypatch)
client = _client(
_health(
providers={
"background_intelligence": {
"provider": "openai",
"model": "gpt-4o-mini",
"configured": False,
}
}
)
)
report = cli_main._build_doctor_report(client)
assert report["checks"]["background_provider"] == "mock_or_disabled"
assert any("not configured" in hint for hint in report["hints"])
def test_cmd_doctor_handles_unreachable_backend(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
):
_patch_git(monkeypatch)
_patch_cli(monkeypatch)
client = _client()
client.get_health.side_effect = SmritiError("Could not reach Smriti")
args = argparse.Namespace(json=False)
@ -91,11 +166,13 @@ def test_cmd_doctor_handles_unreachable_backend(
assert "# Smriti Doctor" in out
assert "Backend: unreachable" in out
assert "missing capabilities: unknown" in out
assert "make dev-local" in out
assert "Backend is not reachable" in out
def test_cmd_doctor_json_outputs_report(monkeypatch: pytest.MonkeyPatch):
_patch_git(monkeypatch)
_patch_cli(monkeypatch)
client = _client(_health())
args = argparse.Namespace(json=True)
captured: list[dict] = []
@ -108,3 +185,23 @@ def test_cmd_doctor_json_outputs_report(monkeypatch: pytest.MonkeyPatch):
assert captured[0]["backend"]["reachable"] is True
assert captured[0]["checks"]["runtime_match"] == "ok"
def test_cmd_doctor_prints_activation_details(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
):
_patch_git(monkeypatch)
_patch_cli(monkeypatch)
client = _client(_health())
args = argparse.Namespace(json=False)
cli_main.cmd_doctor(client, args)
out = capsys.readouterr().out
assert "## Runtime" in out
assert "Database mode: `local`" in out
assert "Local DB path: `/Users/test/.smriti/smriti.db`" in out
assert "Background intelligence: ready (`openai` / `gpt-4o-mini`)" in out
assert "## CLI" in out
assert "PATH matches executable: yes" in out