- integrations.ts: grant data:<provider> on OAuth callback, revoke on disconnect - Backfill migration: INSERT OR IGNORE data:<provider> for all active tokens - Agent manifests: drop agent:<id> from required_consents (momentum, time-of-day, overdue-task, recent-patterns, health-vitals) — per-agent control is a preference - eligibility.ts: update comment to reflect data:-only consent model - test_manifest.py: assert no agent: consents remain in any manifest - migrations.test.ts: backfill idempotency tests for issue #127 - Dockerfile.api: drop --offline flag (fixes ERR_PNPM_NO_OFFLINE_META) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
69 lines
2.5 KiB
Python
69 lines
2.5 KiB
Python
"""Manifest registry tests (ADR-0014).
|
|
|
|
Each agent module exports a `MANIFEST: AgentManifest` whose id and version
|
|
must agree with the agent class. The registry exposes both, and `to_dict()`
|
|
must drop the `infer` callable so the wire payload is JSON-serialisable.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
|
|
|
import pytest # noqa: E402
|
|
|
|
from ml.agents.manifest import AgentManifest, InferredParam # noqa: E402
|
|
from ml.agents.registry import ( # noqa: E402
|
|
all_agents,
|
|
all_manifests,
|
|
get_agent,
|
|
get_manifest,
|
|
)
|
|
|
|
|
|
def test_every_agent_has_a_matching_manifest():
|
|
agents = {a.agent_id: a for a in all_agents()}
|
|
manifests = {m.id: m for m in all_manifests()}
|
|
assert agents.keys() == manifests.keys(), "agent / manifest registries diverged"
|
|
for aid in agents:
|
|
assert agents[aid].version == manifests[aid].version, (
|
|
f"version mismatch for {aid}: agent={agents[aid].version!r} "
|
|
f"manifest={manifests[aid].version!r}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("agent_id", [
|
|
"overdue-task", "momentum", "time-of-day", "recent-patterns", "focus-area",
|
|
])
|
|
def test_manifest_required_fields(agent_id: str):
|
|
m = get_manifest(agent_id)
|
|
assert m.id == agent_id
|
|
assert m.version
|
|
assert m.description
|
|
assert isinstance(m.pref_schema, dict) and m.pref_schema.get("type") == "object"
|
|
assert isinstance(m.required_consents, list) and m.required_consents
|
|
assert "data:core" in m.required_consents, "every agent should require data:core"
|
|
assert all(c.startswith("data:") for c in m.required_consents), "only data: consents allowed; agent: consents have been removed"
|
|
assert m.ttl_sec == get_agent(agent_id).ttl_seconds, "ttl divergence"
|
|
|
|
|
|
def test_to_dict_is_json_serialisable_and_drops_infer_callable():
|
|
m = AgentManifest(
|
|
id="x", version="1.0.0", description="d",
|
|
pref_schema={"type": "object"}, context_schema=[], required_consents=["data:core"],
|
|
output_contract={"type": "snippet"}, ttl_sec=60,
|
|
inferred_params=[InferredParam(key="k", ttl_sec=60, cold_start_default=0, min_history=10, infer=lambda h: 0)],
|
|
)
|
|
payload = m.to_dict()
|
|
# Round-trip through json to confirm no callables / non-JSON types leaked.
|
|
data = json.loads(json.dumps(payload))
|
|
assert data["inferred_params"][0]["key"] == "k"
|
|
assert "infer" not in data["inferred_params"][0]
|
|
|
|
|
|
def test_get_manifest_unknown_raises():
|
|
with pytest.raises(KeyError):
|
|
get_manifest("not-an-agent")
|