mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-20 00:14:50 +00:00
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.
104 lines
4.0 KiB
Python
104 lines
4.0 KiB
Python
"""A stale control must not take the rest of an agent's guardrails with it.
|
|
|
|
Also pins ``GUARDRAILS_CHECKS_ENABLED`` to the registry keys. The equivalent
|
|
setting for retrievers drifted from its registry once already, and the failure
|
|
is silent both times: an unmatched value is filtered out, not rejected.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
import pytest
|
|
|
|
from docsgpt.core.settings import settings
|
|
from docsgpt.guardrails.config import AgentConfig, GuardrailsConfig
|
|
from docsgpt.guardrails.guardrail_creator import GuardrailCreator
|
|
from docsgpt.guardrails.runtime import resolve_config
|
|
|
|
TWO_CONTROLS = {
|
|
"enabled": True,
|
|
"mode": "scan_all",
|
|
"controls": [
|
|
{"check": "secrets", "stage": "output", "action": "redact"},
|
|
{"check": "pii", "stage": "output", "action": "redact"},
|
|
],
|
|
}
|
|
|
|
|
|
def _checks(config):
|
|
return [c.check for c in config.controls]
|
|
|
|
|
|
@pytest.fixture
|
|
def narrowed(monkeypatch):
|
|
"""Allow only ``pii``, as an operator tightening the allowlist would."""
|
|
monkeypatch.setattr(settings, "GUARDRAILS_CHECKS_ENABLED", ["pii"])
|
|
monkeypatch.setattr(GuardrailCreator, "_bootstrapped", False)
|
|
yield
|
|
|
|
|
|
class TestAllowlistMatchesRegistry:
|
|
def test_default_allowlist_is_empty_meaning_everything(self):
|
|
assert settings.GUARDRAILS_CHECKS_ENABLED == []
|
|
assert GuardrailCreator.enabled_keys() == sorted(GuardrailCreator.checks)
|
|
|
|
def test_every_configured_value_is_a_registry_key(self):
|
|
"""Guards against the drift that silently disabled a retriever."""
|
|
configured = settings.GUARDRAILS_CHECKS_ENABLED or []
|
|
GuardrailCreator._ensure_builtin()
|
|
unknown = set(configured) - set(GuardrailCreator.checks)
|
|
assert not unknown, f"not registry keys: {sorted(unknown)}"
|
|
|
|
def test_catalog_only_exposes_enabled_keys(self, narrowed):
|
|
assert [entry["name"] for entry in GuardrailCreator.catalog()] == ["pii"]
|
|
|
|
def test_a_typo_is_filtered_not_rejected(self, monkeypatch):
|
|
"""Documents the sharp edge the test above exists to catch."""
|
|
monkeypatch.setattr(settings, "GUARDRAILS_CHECKS_ENABLED", ["pii", "secrets_"])
|
|
monkeypatch.setattr(GuardrailCreator, "_bootstrapped", False)
|
|
assert GuardrailCreator.enabled_keys() == ["pii"]
|
|
|
|
|
|
class TestSalvage:
|
|
def test_disallowed_control_is_dropped_alone(self, narrowed):
|
|
parsed = GuardrailsConfig.parse(TWO_CONTROLS)
|
|
assert parsed.enabled is True
|
|
assert _checks(parsed) == ["pii"]
|
|
|
|
def test_agent_config_wrapper_salvages_too(self, narrowed):
|
|
parsed = AgentConfig.parse({"guardrails": TWO_CONTROLS}).guardrails
|
|
assert _checks(parsed) == ["pii"]
|
|
|
|
def test_resolve_config_end_to_end(self, narrowed):
|
|
assert _checks(resolve_config({"guardrails": TWO_CONTROLS})) == ["pii"]
|
|
|
|
def test_unknown_check_from_an_upgrade_is_dropped_alone(self):
|
|
raw = {
|
|
"enabled": True,
|
|
"mode": "scan_all",
|
|
"controls": [
|
|
{"check": "moderation", "stage": "output", "action": "flag"},
|
|
{"check": "secrets", "stage": "output", "action": "redact"},
|
|
],
|
|
}
|
|
assert _checks(GuardrailsConfig.parse(raw)) == ["secrets"]
|
|
|
|
def test_mode_preserved_while_salvaging(self, narrowed):
|
|
assert GuardrailsConfig.parse(TWO_CONTROLS).mode == "scan_all"
|
|
|
|
def test_drop_is_logged_with_the_reason(self, narrowed, caplog):
|
|
with caplog.at_level(logging.WARNING, logger="docsgpt.guardrails.config"):
|
|
GuardrailsConfig.parse(TWO_CONTROLS)
|
|
assert "secrets:output" in caplog.text
|
|
assert "not enabled on this instance" in caplog.text
|
|
|
|
def test_unusable_config_still_degrades_to_disabled(self, caplog):
|
|
with caplog.at_level(logging.WARNING, logger="docsgpt.guardrails.config"):
|
|
parsed = GuardrailsConfig.parse({"enabled": True, "mode": "not_a_mode"})
|
|
assert parsed.enabled is False
|
|
assert "unusable" in caplog.text
|
|
|
|
def test_valid_config_is_untouched(self):
|
|
assert _checks(GuardrailsConfig.parse(TWO_CONTROLS)) == ["secrets", "pii"]
|