Files
DocsGPT/tests/devices/test_remote_device_decision.py
T
Alex 574f96341e refactor: rename the application package to docsgpt
The backend import package is now docsgpt, the name it will carry on PyPI;
application was far too generic to install into anyone's site-packages.
git mv plus a mechanical rewrite of every import, dotted string and path
reference: 734 Python files, the compose files, Dockerfile, workflows, docs,
setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage
config, .gitignore. Behaviour is unchanged.

Kept for one release:
- A top-level application package whose meta-path finder resolves
  application.x.y to the already-imported docsgpt.x.y object, so old imports
  and entry points (celery -A application.app.celery,
  uvicorn application.asgi:asgi_app) keep working with a FutureWarning.
- Celery registers every application.* task name as an alias of its
  docsgpt.* task on start-up, so messages queued by the previous release still
  run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries
  the previous release wrote are left unread instead of firing twice.

The backend image builds from the repository root (docker build -f
docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore
allow-lists docsgpt/ and application/ and keeps caches, local data, .env
files, the sample index files and the Dockerfile out. Compose and the image
workflows point at the new context.
2026-09-07 10:20:43 +01:00

59 lines
2.5 KiB
Python

"""Tests for ``RemoteDeviceTool._decide_approval`` (two-mode model)."""
from __future__ import annotations
import pytest
from docsgpt.agents.tools.remote_device import RemoteDeviceTool
def _tool(monkeypatch, *, sticky=False):
tool = RemoteDeviceTool.__new__(RemoteDeviceTool)
tool.device_id = "dev_abc"
tool.user_id = "alice"
monkeypatch.setattr(tool, "_matches_sticky", lambda command: sticky)
return tool
@pytest.mark.unit
class TestDecideApproval:
def test_full_passthrough(self, monkeypatch):
tool = _tool(monkeypatch)
reason, mode = tool._decide_approval({"approval_mode": "full"}, "rm /tmp/x")
assert (reason, mode) == ("full_access_passthrough", "full")
def test_full_denylist_forces_prompt(self, monkeypatch):
tool = _tool(monkeypatch)
reason, mode = tool._decide_approval({"approval_mode": "full"}, "rm -rf /")
assert (reason, mode) == ("denylist_forced_prompt", "ask")
def test_ask_requires_approval(self, monkeypatch):
tool = _tool(monkeypatch, sticky=False)
reason, mode = tool._decide_approval({"approval_mode": "ask"}, "ls -la")
assert (reason, mode) == ("user_approval_required", "ask")
def test_ask_sticky_auto_approves(self, monkeypatch):
tool = _tool(monkeypatch, sticky=True)
reason, mode = tool._decide_approval({"approval_mode": "ask"}, "ls -la")
assert (reason, mode) == ("sticky_auto_approve", "full")
def test_ask_sticky_denylist_still_forces_prompt(self, monkeypatch):
# Regression: a "don't ask again" sticky pattern must not let a
# denylisted command (e.g. sticky ``rm *`` -> ``rm -rf /``, or sticky
# ``git push *`` -> ``git push --force --mirror``) auto-run. The
# denylist forces a prompt on every path, including the sticky one.
from docsgpt.devices.denylist import check_denylist
assert check_denylist("rm -rf /") is not None
assert check_denylist("git push --force --mirror") is not None
tool = _tool(monkeypatch, sticky=True)
for command in ("rm -rf /", "git push --force --mirror"):
reason, mode = tool._decide_approval({"approval_mode": "ask"}, command)
assert (reason, mode) == ("denylist_forced_prompt", "ask")
def test_missing_mode_defaults_to_ask(self, monkeypatch):
tool = _tool(monkeypatch, sticky=False)
reason, mode = tool._decide_approval({}, "ls -la")
assert (reason, mode) == ("user_approval_required", "ask")