feat(system): test

Co-Authored-By: @devpulse <devpulse@aipass>
This commit is contained in:
AIOSAI
2026-05-12 17:33:34 -07:00
co-authored by @devpulse
parent 599a1f47cc
commit 3b8fa1fa5a
31 changed files with 1586 additions and 387 deletions
+221 -158
View File
@@ -20,15 +20,37 @@ from pathlib import Path
from aipass.prax import logger
from aipass.drone.apps.handlers.json import json_handler
from aipass.drone.apps.handlers.git import lock_handler, status_handler, sync_handler, pr_handler
from aipass.drone.apps.handlers.git import (
lock_handler,
status_handler,
sync_handler,
diff_handler,
log_handler,
commit_handler,
checkout_handler,
)
DRONE_MODULE = {
"name": "git",
"version": "1.0.0",
"description": "Git workflow — PR, status, sync, lock management",
"version": "2.0.0",
"description": "Git workflow — tier-based access, status, diff, log, commit, checkout, sync, lock",
}
_COMMANDS = ("pr", "status", "sync", "lock", "unlock", "system-pr", "merge", "smart-sync", "fix")
_COMMANDS = (
"status",
"diff",
"log",
"lock",
"commit",
"checkout",
"sync",
"unlock",
"system-pr",
"merge",
"smart-sync",
"fix",
"pr",
)
def _detect_branch_dir() -> tuple[str, Path] | None:
@@ -63,42 +85,64 @@ def _detect_branch_dir() -> tuple[str, Path] | None:
def handle_command(command: str | None = None, args: list[str] | None = None) -> dict:
"""Route a git command to the appropriate handler.
Auth is centralized: verify_git_access() is called once at the top,
before any routing. Global-tier commands pass for all callers;
owner-tier commands require devpulse.
Args:
command: The subcommand (pr, status, sync, lock, unlock).
command: The subcommand (status, diff, log, commit, checkout, etc.).
args: Optional list of arguments.
Returns:
Dict with stdout, stderr, and exit_code.
"""
if not args:
if command is None:
print_introspection()
return {"stdout": "", "stderr": "", "exit_code": 0}
args = []
if command in ("--help", "-h") or (args and args[0] in ("--help", "-h")):
print_help()
return {"stdout": "", "stderr": "", "exit_code": 0}
json_handler.log_operation("git_handle_command", {"command": command, "args": args})
if command is None:
print_introspection()
return {"stdout": "", "stderr": "", "exit_code": 0}
cmd: str = command
try:
from aipass.drone.apps.plugins.devpulse_ops.auth import verify_git_access
caller = verify_git_access(cmd)
except PermissionError as exc:
logger.error("git access denied: %s", exc)
return {"stdout": "", "stderr": str(exc), "exit_code": 1}
json_handler.log_operation("git_handle_command", {"command": command, "args": args, "caller": caller})
if command == "system-pr":
return _handle_system_pr(args)
if command == "merge":
return _handle_merge(args)
if command == "smart-sync":
return _handle_smart_sync(args)
if command == "fix":
return _handle_fix(args)
if command == "pr":
return _handle_pr(args)
if command == "status":
return _handle_status()
if command == "sync":
return _handle_sync(args)
if command == "diff":
return _handle_diff(args)
if command == "log":
return _handle_log(args)
if command == "lock":
return _handle_lock()
if command == "commit":
return _handle_commit(args)
if command == "checkout":
return _handle_checkout(args)
if command == "sync":
return _handle_sync(args)
if command == "unlock":
return _handle_unlock(args)
if command == "system-pr":
return _handle_system_pr(args, caller)
if command == "merge":
return _handle_merge(args, caller)
if command == "smart-sync":
return _handle_smart_sync(caller)
if command == "fix":
return _handle_fix(args, caller)
if command == "pr":
return {"stdout": "", "stderr": "Agent PRs are deprecated.", "exit_code": 1}
available = ", ".join(_COMMANDS)
return {
@@ -108,8 +152,8 @@ def handle_command(command: str | None = None, args: list[str] | None = None) ->
}
def _handle_system_pr(args: list[str]) -> dict:
"""Handle the system-pr subcommand (devpulse-only)."""
def _handle_system_pr(args: list[str], caller: str) -> dict:
"""Handle the system-pr subcommand (owner-tier, auth pre-checked)."""
if not args:
return {
"stdout": "",
@@ -120,7 +164,6 @@ def _handle_system_pr(args: list[str]) -> dict:
description = " ".join(args)
try:
from aipass.drone.apps.plugins.devpulse_ops.auth import verify_caller
from aipass.drone.apps.plugins.devpulse_ops.pr_plugin import create_system_pr
except ImportError as exc:
logger.error("Failed to import devpulse_ops plugin: %s", exc)
@@ -130,16 +173,6 @@ def _handle_system_pr(args: list[str]) -> dict:
"exit_code": 1,
}
try:
caller = verify_caller()
except PermissionError as exc:
logger.error("system-pr authorization failed: %s", exc)
return {
"stdout": "",
"stderr": str(exc),
"exit_code": 1,
}
result = create_system_pr(description, caller)
if result["success"]:
@@ -155,8 +188,8 @@ def _handle_system_pr(args: list[str]) -> dict:
}
def _handle_merge(args: list[str]) -> dict:
"""Handle the merge subcommand (devpulse-only)."""
def _handle_merge(args: list[str], caller: str) -> dict:
"""Handle the merge subcommand (owner-tier, auth pre-checked)."""
if not args:
return {
"stdout": "",
@@ -167,7 +200,6 @@ def _handle_merge(args: list[str]) -> dict:
pr_number = args[0]
try:
from aipass.drone.apps.plugins.devpulse_ops.auth import verify_caller
from aipass.drone.apps.plugins.devpulse_ops.merge_plugin import merge_pr
except ImportError as exc:
logger.error("Failed to import devpulse_ops merge plugin: %s", exc)
@@ -177,16 +209,6 @@ def _handle_merge(args: list[str]) -> dict:
"exit_code": 1,
}
try:
caller = verify_caller()
except PermissionError as exc:
logger.error("merge authorization failed: %s", exc)
return {
"stdout": "",
"stderr": str(exc),
"exit_code": 1,
}
result = merge_pr(pr_number, caller)
if result["success"]:
@@ -202,10 +224,9 @@ def _handle_merge(args: list[str]) -> dict:
}
def _handle_smart_sync(args: list[str]) -> dict:
"""Handle the smart-sync subcommand (devpulse-only)."""
def _handle_smart_sync(caller: str) -> dict:
"""Handle the smart-sync subcommand (owner-tier, auth pre-checked)."""
try:
from aipass.drone.apps.plugins.devpulse_ops.auth import verify_caller
from aipass.drone.apps.plugins.devpulse_ops.sync_plugin import smart_sync
except ImportError as exc:
logger.error("Failed to import devpulse_ops sync plugin: %s", exc)
@@ -215,16 +236,6 @@ def _handle_smart_sync(args: list[str]) -> dict:
"exit_code": 1,
}
try:
caller = verify_caller()
except PermissionError as exc:
logger.error("smart-sync authorization failed: %s", exc)
return {
"stdout": "",
"stderr": str(exc),
"exit_code": 1,
}
result = smart_sync(caller)
if result["success"]:
@@ -240,10 +251,9 @@ def _handle_smart_sync(args: list[str]) -> dict:
}
def _handle_fix(args: list[str]) -> dict:
"""Handle the fix subcommand (devpulse-only)."""
def _handle_fix(args: list[str], caller: str) -> dict:
"""Handle the fix subcommand (owner-tier, auth pre-checked)."""
try:
from aipass.drone.apps.plugins.devpulse_ops.auth import verify_caller
from aipass.drone.apps.plugins.devpulse_ops.fix_plugin import fix_git_state
except ImportError as exc:
logger.error("Failed to import devpulse_ops fix plugin: %s", exc)
@@ -253,16 +263,6 @@ def _handle_fix(args: list[str]) -> dict:
"exit_code": 1,
}
try:
caller = verify_caller()
except PermissionError as exc:
logger.error("fix authorization failed: %s", exc)
return {
"stdout": "",
"stderr": str(exc),
"exit_code": 1,
}
dry_run = "--dry-run" in (args or [])
result = fix_git_state(caller, dry_run=dry_run)
@@ -279,42 +279,8 @@ def _handle_fix(args: list[str]) -> dict:
}
def _handle_pr(args: list[str]) -> dict:
"""Handle the PR subcommand."""
if not args:
return {
"stdout": "",
"stderr": "Usage: drone @git pr <description>",
"exit_code": 1,
}
description = " ".join(args)
detected = _detect_branch_dir()
if detected is None:
return {
"stdout": "",
"stderr": "Cannot detect branch directory from CWD. Run from within src/aipass/<branch>/",
"exit_code": 1,
}
branch_name, branch_dir = detected
result = pr_handler.create_pr(branch_name, description, branch_dir)
if result["success"]:
return {
"stdout": f"PR created: {result['pr_url']}\nBranch: {result['feature_branch']}",
"stderr": "",
"exit_code": 0,
}
return {
"stdout": "",
"stderr": result["message"],
"exit_code": 1,
}
def _handle_status() -> dict:
"""Handle the status subcommand."""
"""Handle the status subcommand (global tier)."""
detected = _detect_branch_dir()
if detected is None:
return {
@@ -337,8 +303,96 @@ def _handle_status() -> dict:
}
def _handle_diff(args: list[str]) -> dict:
"""Handle the diff subcommand (global tier)."""
detected = _detect_branch_dir()
if detected is None:
return {
"stdout": "",
"stderr": "Cannot detect branch directory from CWD. Run from within src/aipass/<branch>/",
"exit_code": 1,
}
_, branch_dir = detected
staged = "--staged" in args
result = diff_handler.get_branch_diff(branch_dir, staged=staged)
return {
"stdout": result["diff"] if result["diff"] else result["message"],
"stderr": "",
"exit_code": 0,
}
def _handle_log(args: list[str]) -> dict:
"""Handle the log subcommand (global tier)."""
count = 10
for arg in args:
try:
count = int(arg)
break
except ValueError as exc:
logger.warning("Invalid log count argument '%s': %s", arg, exc)
continue
result = log_handler.get_git_log(count=count)
if result["entries"]:
return {
"stdout": "\n".join(result["entries"]),
"stderr": "",
"exit_code": 0,
}
return {
"stdout": result["message"],
"stderr": "",
"exit_code": 0,
}
def _handle_commit(args: list[str]) -> dict:
"""Handle the commit subcommand (owner tier)."""
if not args:
return {
"stdout": "",
"stderr": "Usage: drone @git commit <message> [--all]",
"exit_code": 1,
}
all_files = "--all" in args
msg_parts = [a for a in args if a != "--all"]
message = " ".join(msg_parts)
if not message:
return {
"stdout": "",
"stderr": "Commit message cannot be empty",
"exit_code": 1,
}
branch_dir = None
if all_files:
detected = _detect_branch_dir()
if detected:
_, branch_dir = detected
return commit_handler.commit_changes(message, branch_dir=branch_dir, all_files=all_files)
def _handle_checkout(args: list[str]) -> dict:
"""Handle the checkout subcommand (owner tier)."""
if not args:
return {
"stdout": "",
"stderr": "Usage: drone @git checkout <main|dev>",
"exit_code": 1,
}
return checkout_handler.checkout_branch(args[0])
def _handle_sync(args: list[str]) -> dict:
"""Handle the sync subcommand."""
"""Handle the sync subcommand (owner tier)."""
autostash = "--autostash" in args
result = sync_handler.sync_main(autostash=autostash)
@@ -356,7 +410,7 @@ def _handle_sync(args: list[str]) -> dict:
def _handle_lock() -> dict:
"""Handle the lock subcommand (check status)."""
"""Handle the lock subcommand (global tier)."""
result = lock_handler.check_lock_status()
return {
"stdout": json.dumps(result, indent=2),
@@ -366,7 +420,7 @@ def _handle_lock() -> dict:
def _handle_unlock(args: list[str]) -> dict:
"""Handle the unlock subcommand (force only)."""
"""Handle the unlock subcommand (owner tier)."""
if "--force" not in args:
return {
"stdout": "",
@@ -398,100 +452,109 @@ def get_help(command: str | None = None) -> str:
Help text string.
"""
if command == "pr":
return (
"git pr <description> — Create a PR with scoped changes\n"
" Stages only files under the caller's branch directory,\n"
" creates a feature branch, commits, pushes, and opens a PR.\n"
)
return "git pr — DEPRECATED. Agent PRs are no longer supported. Devpulse handles git.\n"
if command == "status":
return "git status — Show git status filtered to your branch directory\n"
return "git status — Show git status filtered to your branch directory [global]\n"
if command == "diff":
return (
"git diff [--staged] — Show git diff filtered to your branch directory [global]\n"
" Options:\n"
" --staged Show staged changes only.\n"
)
if command == "log":
return "git log [count] — Show recent git log entries (default: 10) [global]\n"
if command == "lock":
return "git lock — Check current lock status [global]\n Shows lock holder, age, stale/orphan detection.\n"
if command == "commit":
return (
"git commit <message> [--all] — Commit staged changes [owner]\n"
" Options:\n"
" --all Stage all changes under your branch directory first.\n"
)
if command == "checkout":
return "git checkout <main|dev> — Switch branches (main or dev only) [owner]\n"
if command == "sync":
return (
"git sync [--autostash] — Checkout main and pull latest changes\n"
"git sync [--autostash] — Checkout main and pull latest changes [owner]\n"
" Options:\n"
" --autostash Stash local changes before pull and restore after.\n"
" Use when sync fails with 'unstaged changes' error.\n"
)
if command == "lock":
return "git lock — Check current lock status\n Shows lock holder, age, stale/orphan detection.\n"
if command == "unlock":
return "git unlock --force — Force-release the PR lock\n Removes .git_pr.lock regardless of holder.\n"
return "git unlock --force — Force-release the PR lock [owner]\n"
if command == "system-pr":
return (
"git system-pr <description> — Create a system-wide PR (devpulse only)\n"
" Stages all tracked changes, creates a disposable feature branch,\n"
" and opens a PR. Requires devpulse passport authorization.\n"
"git system-pr <description> — Create a system-wide PR [owner]\n"
" Stages all tracked changes, creates a feature branch, and opens a PR.\n"
)
if command == "merge":
return (
"git merge <PR#> — Merge a PR and sync local main (devpulse only)\n"
"git merge <PR#> — Merge a PR and sync local main [owner]\n"
" Runs gh pr merge --merge --delete-branch, then git pull --rebase.\n"
)
if command == "smart-sync":
return (
"git smart-sync — Fetch origin and rebase if behind (devpulse only)\n"
" Detects divergence and rebases safely; aborts on conflict.\n"
)
return "git smart-sync — Fetch origin and rebase if behind [owner]\n"
if command == "fix":
return (
"git fix [--dry-run] — Detect and fix common broken git states (devpulse only)\n"
"git fix [--dry-run] — Detect and fix broken git states [owner]\n"
"\n"
"Detected states and actions:\n"
" Stuck rebase .git/rebase-merge or rebase-apply exists\n"
" → git rebase --abort\n"
" Detached HEAD HEAD is not on a named branch\n"
" → git checkout main\n"
" Diverged local main and origin/main have diverged\n"
" → git fetch + git merge origin/main --no-edit\n"
" Dirty index files are staged but not committed\n"
" → git reset HEAD (unstages all)\n"
" Stuck rebase → git rebase --abort\n"
" Detached HEAD → git checkout main\n"
" Diverged → git fetch + git merge origin/main\n"
" Dirty index → git reset HEAD\n"
"\n"
"Options:\n"
" --dry-run Report detected states and proposed actions without\n"
" executing any fixes. Safe to run anytime.\n"
" --dry-run Report without executing fixes.\n"
)
return (
"git — Git workflow: PR, status, sync, lock management\n"
"git — Tier-based git workflow\n"
"\n"
"Commands:\n"
" pr <description> Create a PR with scoped changes\n"
" system-pr <desc> Create a system-wide PR (devpulse only)\n"
" merge <PR#> Merge a PR (devpulse only)\n"
" smart-sync Fetch + rebase if behind (devpulse only)\n"
" fix Fix broken git states (devpulse only)\n"
"Global (all branches):\n"
" status Show git status for your branch\n"
" sync Checkout main and pull\n"
" diff [--staged] Show git diff for your branch\n"
" log [count] Show recent git log (default: 10)\n"
" lock Check lock status\n"
" unlock --force Force-release the PR lock\n"
"\n"
"Policy: raw git commands are blocked for agents via .claude/settings.json:\n"
" - git checkout* (any form) — use `drone @git sync` instead\n"
" - git add -f* / --force* — use scoped `drone @git pr` instead\n"
"Culturally also avoid: raw git commit/push, gh pr create. Go through drone.\n"
"Owner (devpulse only):\n"
" commit <msg> [--all] Commit staged changes\n"
" checkout <main|dev> Switch branches\n"
" sync [--autostash] Checkout main and pull\n"
" unlock --force Force-release the PR lock\n"
" system-pr <desc> Create a system-wide PR\n"
" merge <PR#> Merge a PR\n"
" smart-sync Fetch + rebase if behind\n"
" fix [--dry-run] Fix broken git states\n"
"\n"
"Deprecated:\n"
" pr Agent PRs removed — devpulse handles git\n"
)
def get_introspective() -> str:
"""Return introspection text showing connected handlers."""
return (
"@git — Git workflow: PR, status, sync, lock management\n"
"@git — Tier-based git workflow (v2.0.0)\n"
"\n"
"Connected Handlers:\n"
" handlers/git/\n"
" - lock_handler.py (acquire_lock, release_lock, check_lock_status, force_unlock)\n"
" - status_handler.py (get_branch_status — scoped git status)\n"
" - diff_handler.py (get_branch_diff — scoped git diff)\n"
" - log_handler.py (get_git_log — recent log entries)\n"
" - commit_handler.py (commit_changes, stage_branch_dir)\n"
" - checkout_handler.py (checkout_branch — main/dev only)\n"
" - sync_handler.py (sync_main — safe main synchronization)\n"
" - pr_handler.py (create_pr — full PR workflow with lockfile)\n"
" - pr_handler.py (create_pr — DEPRECATED)\n"
"\n"
" plugins/devpulse_ops/\n"
" - auth.py (verify_caller — passport-based authorization)\n"
" - auth.py (verify_git_access — tier-based authorization)\n"
" - pr_plugin.py (create_system_pr — system-wide PR workflow)\n"
" - merge_plugin.py (merge_pr — merge PR + sync)\n"
" - sync_plugin.py (smart_sync — fetch + rebase if behind)\n"
" - fix_plugin.py (fix_git_state — detect/fix broken states)\n"
"\n"
"Commands: pr, system-pr, merge, smart-sync, fix, status, sync, lock, unlock\n"
"Access Tiers: global (status, diff, log, lock) | owner (commit, checkout, sync, unlock, system-pr, merge, smart-sync, fix)\n"
)