b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
124 lines
4.8 KiB
Python
124 lines
4.8 KiB
Python
"""Conformance kit for :class:`agent.secret_sources.base.SecretSource`.
|
|
|
|
Any secret-source backend — bundled or external plugin — can validate
|
|
itself against the contract by subclassing :class:`SecretSourceConformance`
|
|
and providing a ``source`` fixture (plus optional per-source config
|
|
fixtures). Example::
|
|
|
|
from tests.secret_sources.conformance import SecretSourceConformance
|
|
|
|
class TestMySourceConformance(SecretSourceConformance):
|
|
@pytest.fixture
|
|
def source(self):
|
|
return MySource()
|
|
|
|
The checks encode the parts of the contract that break OTHER people
|
|
when violated: never raising, never prompting (stdin closed), respecting
|
|
disabled config, valid identity attributes, and orchestrator
|
|
compatibility.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from agent.secret_sources.base import (
|
|
SECRET_SOURCE_API_VERSION,
|
|
FetchResult,
|
|
SecretSource,
|
|
)
|
|
from agent.secret_sources.registry import (
|
|
_reset_registry_for_tests,
|
|
apply_all,
|
|
register_source,
|
|
)
|
|
|
|
|
|
class SecretSourceConformance:
|
|
"""Base class of contract checks; subclass and provide ``source``."""
|
|
|
|
@pytest.fixture
|
|
def source(self) -> SecretSource: # pragma: no cover — must override
|
|
raise NotImplementedError("conformance subclasses must provide a source fixture")
|
|
|
|
@pytest.fixture
|
|
def minimal_cfg(self) -> dict:
|
|
"""An enabled-but-unconfigured section — the common misconfig case."""
|
|
return {"enabled": True}
|
|
|
|
# -- identity ----------------------------------------------------------
|
|
|
|
def test_name_is_lowercase_identifier(self, source):
|
|
assert source.name, "source.name must be non-empty"
|
|
assert source.name == source.name.lower()
|
|
assert source.name.replace("_", "").isalnum()
|
|
|
|
def test_label_present(self, source):
|
|
assert source.label, "source.label must be a human-readable name"
|
|
|
|
def test_shape_valid(self, source):
|
|
assert source.shape in ("mapped", "bulk")
|
|
|
|
def test_api_version_current(self, source):
|
|
assert source.api_version == SECRET_SOURCE_API_VERSION
|
|
|
|
# -- contract behavior --------------------------------------------------
|
|
|
|
def test_fetch_never_raises_on_malformed_config(self, source, tmp_path):
|
|
"""Every degenerate config shape must produce a FetchResult, not a raise."""
|
|
for cfg in ({}, {"enabled": True}, {"enabled": True, "env": "not-a-dict"},
|
|
{"enabled": True, "cache_ttl_seconds": "bogus"}, None):
|
|
result = source.fetch(cfg if isinstance(cfg, dict) else {}, tmp_path)
|
|
assert isinstance(result, FetchResult), (
|
|
f"fetch() returned {type(result).__name__} for cfg={cfg!r}"
|
|
)
|
|
|
|
def test_fetch_unconfigured_reports_error_not_secrets(self, source, tmp_path,
|
|
minimal_cfg, monkeypatch):
|
|
"""enabled=true with nothing else set must fail cleanly with a kind."""
|
|
result = source.fetch(minimal_cfg, tmp_path)
|
|
assert isinstance(result, FetchResult)
|
|
if not result.ok:
|
|
assert result.error_kind is not None, (
|
|
"errors must carry a machine-readable ErrorKind"
|
|
)
|
|
assert not result.secrets
|
|
|
|
def test_disabled_by_default(self, source):
|
|
assert source.is_enabled({}) is False
|
|
assert source.is_enabled({"enabled": False}) is False
|
|
|
|
def test_timeout_is_positive(self, source, minimal_cfg):
|
|
assert source.fetch_timeout_seconds(minimal_cfg) > 0
|
|
# Garbage config must not break the timeout accessor either.
|
|
assert source.fetch_timeout_seconds({"timeout_seconds": "junk"}) > 0
|
|
|
|
def test_protected_vars_are_valid_names(self, source, minimal_cfg):
|
|
from agent.secret_sources.base import is_valid_env_name
|
|
|
|
for var in source.protected_env_vars(minimal_cfg):
|
|
assert is_valid_env_name(var)
|
|
|
|
# -- orchestrator compatibility ------------------------------------------
|
|
|
|
def test_registers_and_applies_via_orchestrator(self, source, tmp_path,
|
|
monkeypatch):
|
|
"""The source must survive a full apply_all() pass without breaking it."""
|
|
_reset_registry_for_tests()
|
|
# Prevent the bundled sources from interfering.
|
|
monkeypatch.setattr(
|
|
"agent.secret_sources.registry._ensure_builtin_sources", lambda: None
|
|
)
|
|
try:
|
|
assert register_source(source), "register_source() rejected the source"
|
|
env: dict = {}
|
|
report = apply_all(
|
|
{source.name: {"enabled": True}}, tmp_path, environ=env
|
|
)
|
|
names = [sr.name for sr in report.sources]
|
|
assert source.name in names
|
|
finally:
|
|
_reset_registry_for_tests()
|