Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59d5cdfcb8 | |||
| acb2fbe328 | |||
| f311797c13 | |||
| c62e2c3864 |
@@ -39,6 +39,15 @@ Three properties are covered, all against STABLE-on-main behavior:
|
||||
/ ``omnigent_credentials_env`` — a missing key is a clean SKIP so the e2e
|
||||
shards stay green, and it runs for real wherever a key is present.
|
||||
|
||||
**Why this test cannot use the mock LLM server:** The ``google-antigravity``
|
||||
SDK has no OpenAI-compatible ``base_url`` and no Databricks-gateway path.
|
||||
Setting ``OPENAI_BASE_URL`` to the mock server has no effect on this harness —
|
||||
the SDK always connects directly to Google's Gemini backend. Furthermore,
|
||||
assertions 2 and 3 verify the lifecycle of a real native ``localharness``
|
||||
binary process; a mock LLM could not exercise this at all. The ``pytest.skip``
|
||||
in the :func:`antigravity_runnable` fixture gates cleanly when the SDK or key
|
||||
is absent.
|
||||
|
||||
.. note::
|
||||
**glibc >= ~2.36 caveat.** The native ``localharness`` binary is linked
|
||||
against a recent glibc (needs ``GLIBC_ABI_DT_RELR``). On an older host (e.g.
|
||||
|
||||
@@ -75,7 +75,7 @@ def _connect_env(base_env: Mapping[str, str], home: Path) -> dict[str, str]:
|
||||
keeping the test from touching the developer's real local server.
|
||||
|
||||
:param base_env: Fixture-provided credentials environment, e.g.
|
||||
``omnigent_credentials_env``.
|
||||
``mock_credentials_env``.
|
||||
:param home: Isolated HOME for this test's runtime data.
|
||||
:returns: Environment dict for ``pexpect.spawn``.
|
||||
"""
|
||||
@@ -258,8 +258,7 @@ def _prespawn_persistent_server(
|
||||
def test_host_ctrl_c_yes_stops_local_server(
|
||||
omnigent_python: Path,
|
||||
omnigent_repo_root: Path,
|
||||
omnigent_credentials_env: dict[str, str],
|
||||
databricks_workspace: tuple[str, str],
|
||||
mock_credentials_env: dict[str, str],
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -267,13 +266,12 @@ def test_host_ctrl_c_yes_stops_local_server(
|
||||
|
||||
:param omnigent_python: Python interpreter fixture.
|
||||
:param omnigent_repo_root: Repo root fixture (subprocess cwd).
|
||||
:param omnigent_credentials_env: Real credential environment fixture.
|
||||
:param databricks_workspace: ``(profile, host)`` fixture.
|
||||
:param mock_credentials_env: Mock-LLM credential environment fixture.
|
||||
:param tmp_path: Per-test temp directory.
|
||||
:returns: None.
|
||||
"""
|
||||
home = tmp_path / "home"
|
||||
env = _connect_env(omnigent_credentials_env, home)
|
||||
env = _connect_env(mock_credentials_env, home)
|
||||
child = _spawn_connect(omnigent_python, omnigent_repo_root, env)
|
||||
server_pid = -1
|
||||
try:
|
||||
@@ -307,8 +305,7 @@ def test_host_ctrl_c_yes_stops_local_server(
|
||||
def test_host_ctrl_c_no_leaves_local_server_running(
|
||||
omnigent_python: Path,
|
||||
omnigent_repo_root: Path,
|
||||
omnigent_credentials_env: dict[str, str],
|
||||
databricks_workspace: tuple[str, str],
|
||||
mock_credentials_env: dict[str, str],
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -320,13 +317,12 @@ def test_host_ctrl_c_no_leaves_local_server_running(
|
||||
|
||||
:param omnigent_python: Python interpreter fixture.
|
||||
:param omnigent_repo_root: Repo root fixture (subprocess cwd).
|
||||
:param omnigent_credentials_env: Real credential environment fixture.
|
||||
:param databricks_workspace: ``(profile, host)`` fixture.
|
||||
:param mock_credentials_env: Mock-LLM credential environment fixture.
|
||||
:param tmp_path: Per-test temp directory.
|
||||
:returns: None.
|
||||
"""
|
||||
home = tmp_path / "home"
|
||||
env = _connect_env(omnigent_credentials_env, home)
|
||||
env = _connect_env(mock_credentials_env, home)
|
||||
child = _spawn_connect(omnigent_python, omnigent_repo_root, env)
|
||||
server_pid = -1
|
||||
try:
|
||||
@@ -359,8 +355,7 @@ def test_host_ctrl_c_no_leaves_local_server_running(
|
||||
def test_host_ctrl_c_reused_server_shows_no_prompt(
|
||||
omnigent_python: Path,
|
||||
omnigent_repo_root: Path,
|
||||
omnigent_credentials_env: dict[str, str],
|
||||
databricks_workspace: tuple[str, str],
|
||||
mock_credentials_env: dict[str, str],
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -374,13 +369,12 @@ def test_host_ctrl_c_reused_server_shows_no_prompt(
|
||||
|
||||
:param omnigent_python: Python interpreter fixture.
|
||||
:param omnigent_repo_root: Repo root fixture (subprocess cwd).
|
||||
:param omnigent_credentials_env: Real credential environment fixture.
|
||||
:param databricks_workspace: ``(profile, host)`` fixture.
|
||||
:param mock_credentials_env: Mock-LLM credential environment fixture.
|
||||
:param tmp_path: Per-test temp directory.
|
||||
:returns: None.
|
||||
"""
|
||||
home = tmp_path / "home"
|
||||
env = _connect_env(omnigent_credentials_env, home)
|
||||
env = _connect_env(mock_credentials_env, home)
|
||||
|
||||
# Bring the server up first, independently of connect, with a config
|
||||
# signature that matches what connect will compute — so connect reuses it.
|
||||
|
||||
@@ -22,6 +22,16 @@ backend-native SDK harness): because a Gemini key is not provisioned on CI, the
|
||||
test **skips** (rather than fails) when no key is present, so the e2e shards stay
|
||||
green; it runs for real wherever a key is configured.
|
||||
|
||||
**Why this test cannot use the mock LLM server:** The ``google-antigravity``
|
||||
SDK has no OpenAI-compatible ``base_url`` and no Databricks-gateway path.
|
||||
Setting ``OPENAI_BASE_URL`` to the mock server has no effect on this harness —
|
||||
the SDK always connects directly to Google's Gemini backend using the Gemini
|
||||
API key. There is no intercept point equivalent to ``OPENAI_BASE_URL`` in the
|
||||
Gemini SDK, so the mock-LLM approach used by other harness tests (e.g.
|
||||
``test_per_harness_openai_agents.py``) cannot be applied here. The
|
||||
``pytest.skip`` in :func:`_antigravity_skip_reason` gates each test cleanly
|
||||
when the SDK or key is absent.
|
||||
|
||||
**Prerequisites (skipped cleanly when absent):**
|
||||
- ``google.antigravity`` importable in the Omnigent venv (the ``antigravity``
|
||||
extra — ``pip install 'omnigent[antigravity]'``).
|
||||
|
||||
@@ -22,6 +22,14 @@ key is not provisioned on CI, the test **skips** (rather than fails) when
|
||||
``CURSOR_API_KEY`` is absent so the e2e shards stay green; it runs for real
|
||||
wherever a key is present.
|
||||
|
||||
**Why this test cannot use the mock LLM server:** The ``cursor-sdk`` connects
|
||||
directly to Cursor's proprietary backend using ``CURSOR_API_KEY`` — it does not
|
||||
honour ``OPENAI_BASE_URL`` the way the ``openai-agents`` harness does. There is
|
||||
no OpenAI-compatible shim path in the Cursor SDK, so pointing
|
||||
``OPENAI_BASE_URL`` at the mock server has no effect. This harness can only be
|
||||
exercised with a real Cursor API key; the ``pytest.skip`` below gates the test
|
||||
cleanly when the key is absent.
|
||||
|
||||
**What breaks if this fails (with prerequisites present):**
|
||||
- ``CursorExecutor`` regresses (the ``SDKMessage`` → ExecutorEvent translation,
|
||||
the ``custom_tools`` tool bridge, persistent-agent reuse, or the system-prompt
|
||||
|
||||
+110
-59
@@ -519,96 +519,147 @@ def test_no_guardrails_agent_unaffected(
|
||||
assert "[Denied by policy" not in text
|
||||
|
||||
|
||||
# ── Prompt policy (Phase 9): real LLM classifier end-to-end ─
|
||||
# ── Prompt policy (Phase 9): LLM classifier end-to-end ──────
|
||||
#
|
||||
# These tests exercise the production path of the
|
||||
# ``prompt_policy`` builtin — the real LLM gets called with
|
||||
# the framework-generated envelope + author prompt, and the
|
||||
# parsed JSON verdict drives the ALLOW / DENY branch.
|
||||
# The ``prompt_policy`` builtin calls ``event["llm_client"]``,
|
||||
# a ``PolicyLLMClient`` pre-bound to the server-level ``llm:``
|
||||
# config. In mock mode the live_server fixture writes a server
|
||||
# config with ``llm: {model: "mock-model", connection: {base_url:
|
||||
# <mock_url>/v1, api_key: mock-key}}``, so the classifier call
|
||||
# goes to the mock server's ``/v1/responses`` queue keyed by
|
||||
# ``"mock-model"``. Pre-seeding that queue with a JSON verdict
|
||||
# fully exercises the wiring without real credentials.
|
||||
|
||||
_DENY_CANADA_POLICY_CONFIG = {
|
||||
"policies": {
|
||||
"deny_canada": {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"path": "omnigent.policies.builtins.prompt.prompt_policy",
|
||||
"arguments": {
|
||||
"prompt": (
|
||||
"You are a strict content filter. Look at the user's "
|
||||
"message and decide:\n"
|
||||
"- If it mentions Canada, Canadian, Ontario, Quebec, "
|
||||
"Toronto, Montreal, or anything unambiguously Canadian, "
|
||||
"deny the request.\n"
|
||||
"- Otherwise allow."
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Server-level LLM model key — the PolicyLLMClient uses the
|
||||
# model from the server's ``llm:`` config, which in mock mode
|
||||
# is always ``"mock-model"``.
|
||||
_SERVER_LLM_MODEL = "mock-model"
|
||||
|
||||
|
||||
# The verdict comes from a real LLM-backed classifier (the deny_canada
|
||||
# PromptPolicy), so an occasional misclassification or a transient
|
||||
# classifier-call failure (fail-closed DENY) can flip the expected
|
||||
# allow/deny outcome. Bounded reruns absorb that non-determinism without
|
||||
# masking a genuine wiring regression: a real break fails all 3 attempts.
|
||||
@pytest.mark.flaky(reruns=2, reruns_delay=5)
|
||||
def test_prompt_policy_allow_path_reaches_llm(
|
||||
http_client: httpx.Client,
|
||||
prompt_policy_agent: str,
|
||||
live_runner_id: str,
|
||||
using_mock_llm: bool,
|
||||
mock_llm_server_url: str,
|
||||
) -> None:
|
||||
"""
|
||||
Non-Canadian input → classifier ALLOWs → agent LLM runs →
|
||||
assistant text comes back. Proves the real classifier
|
||||
works end-to-end through the real LLM, the policy engine
|
||||
composes ALLOW, and the full turn completes normally.
|
||||
assistant text comes back.
|
||||
|
||||
The mock server's ``"mock-model"`` queue is pre-seeded with an
|
||||
ALLOW verdict so the prompt_policy classifier returns ALLOW
|
||||
without any real LLM call. The agent model is a separate mock
|
||||
key so the two queues don't interfere.
|
||||
|
||||
:param http_client: HTTP client pointed at the live server.
|
||||
:param live_runner_id: Runner id for the session.
|
||||
:param mock_llm_server_url: Mock LLM server URL.
|
||||
"""
|
||||
if using_mock_llm:
|
||||
pytest.skip("requires real LLM (prompt policy classifier)")
|
||||
agent_model = f"mock-pp-allow-{uuid.uuid4().hex[:6]}"
|
||||
|
||||
reset_mock_llm(mock_llm_server_url)
|
||||
# Classifier verdict: ALLOW (non-Canadian input).
|
||||
configure_mock_llm(
|
||||
mock_llm_server_url,
|
||||
[{"text": '{"action": "allow", "reason": ""}'}],
|
||||
key=_SERVER_LLM_MODEL,
|
||||
)
|
||||
# Agent's own LLM response (reached only after ALLOW).
|
||||
configure_mock_llm(
|
||||
mock_llm_server_url,
|
||||
[{"text": "The Eiffel Tower is in Paris, France."}],
|
||||
key=agent_model,
|
||||
)
|
||||
agent_name = register_inline_agent(
|
||||
http_client,
|
||||
name=f"pp-allow-{uuid.uuid4().hex[:6]}",
|
||||
harness="openai-agents",
|
||||
model=agent_model,
|
||||
profile="",
|
||||
prompt="You are a minimal test agent. Reply briefly.",
|
||||
mock_llm_base_url=f"{mock_llm_server_url}/v1",
|
||||
extra_config=_DENY_CANADA_POLICY_CONFIG,
|
||||
)
|
||||
session_id = create_runner_bound_session(
|
||||
http_client, agent_name=prompt_policy_agent, runner_id=live_runner_id
|
||||
http_client, agent_name=agent_name, runner_id=live_runner_id
|
||||
)
|
||||
rid = send_user_message_to_session(
|
||||
http_client,
|
||||
session_id=session_id,
|
||||
content="What's 2+2? Answer with the number only.",
|
||||
content="Where is the Eiffel Tower?",
|
||||
)
|
||||
body = poll_session_until_terminal(
|
||||
http_client, session_id=session_id, response_id=rid, timeout=120
|
||||
)
|
||||
assert body["status"] == "completed", f"Unexpected status: {body.get('error')}"
|
||||
text = _extract_all_assistant_text(body)
|
||||
# Real LLM answered the question — "4" must appear.
|
||||
# Stronger than a non-empty check: proves the request
|
||||
# actually reached the LLM and the LLM's output
|
||||
# propagated through the ALLOW path.
|
||||
assert "4" in text, f"Expected the LLM's answer to 2+2 ('4') in the reply.\nGot: {text!r}"
|
||||
# Policy did NOT deny — the DENY sentinel must not appear.
|
||||
assert "[Denied by policy" not in text, (
|
||||
f"ALLOW path accidentally emitted a DENY sentinel: {text!r}"
|
||||
)
|
||||
assert len(text.strip()) > 0, "Expected LLM output after ALLOW; got empty response."
|
||||
assert "[Denied by policy" not in text
|
||||
|
||||
|
||||
# Same real-classifier non-determinism as the allow-path test: the DENY can
|
||||
# occasionally not fire. Bounded reruns; a real regression fails all 3.
|
||||
@pytest.mark.flaky(reruns=2, reruns_delay=5)
|
||||
def test_prompt_policy_deny_path_short_circuits(
|
||||
http_client: httpx.Client,
|
||||
prompt_policy_agent: str,
|
||||
live_runner_id: str,
|
||||
using_mock_llm: bool,
|
||||
mock_llm_server_url: str,
|
||||
) -> None:
|
||||
"""
|
||||
Canadian-topic input → classifier DENYs → the events endpoint
|
||||
short-circuits the turn synchronously with an inline deny
|
||||
verdict; the agent LLM never produces its normal output.
|
||||
Canadian-topic input → classifier DENYs → events endpoint
|
||||
short-circuits with an inline deny verdict; agent LLM never runs.
|
||||
|
||||
This is the canonical reason prompt policies exist: a
|
||||
topic-level content filter an author describes in prose
|
||||
rather than a Python predicate. If the real classifier
|
||||
isn't wired (or can't reach the gateway), the policy fails
|
||||
and the verdict carries an error reason rather than the
|
||||
author's ``"mentions Canada"`` — so this test is both a
|
||||
classifier-wiring proof and a gateway-routing regression
|
||||
guard.
|
||||
The mock server's ``"mock-model"`` queue is pre-seeded with a
|
||||
DENY verdict so the prompt_policy classifier fires DENY and the
|
||||
turn is resolved synchronously before reaching the runner.
|
||||
|
||||
:param http_client: HTTP client pointed at the live server.
|
||||
:param live_runner_id: Runner id for the session.
|
||||
:param mock_llm_server_url: Mock LLM server URL.
|
||||
"""
|
||||
if using_mock_llm:
|
||||
pytest.skip("requires real LLM (prompt policy classifier)")
|
||||
session_id = create_runner_bound_session(
|
||||
http_client, agent_name=prompt_policy_agent, runner_id=live_runner_id
|
||||
agent_model = f"mock-pp-deny-{uuid.uuid4().hex[:6]}"
|
||||
|
||||
reset_mock_llm(mock_llm_server_url)
|
||||
# Classifier verdict: DENY (Canadian input).
|
||||
configure_mock_llm(
|
||||
mock_llm_server_url,
|
||||
[{"text": '{"action": "deny", "reason": "Input mentions Canada."}'}],
|
||||
key=_SERVER_LLM_MODEL,
|
||||
)
|
||||
resp = _post_user_message(http_client, session_id, "What's the capital of Canada?")
|
||||
agent_name = register_inline_agent(
|
||||
http_client,
|
||||
name=f"pp-deny-{uuid.uuid4().hex[:6]}",
|
||||
harness="openai-agents",
|
||||
model=agent_model,
|
||||
profile="",
|
||||
prompt="You are a minimal test agent. Reply briefly.",
|
||||
mock_llm_base_url=f"{mock_llm_server_url}/v1",
|
||||
extra_config=_DENY_CANADA_POLICY_CONFIG,
|
||||
)
|
||||
session_id = create_runner_bound_session(
|
||||
http_client, agent_name=agent_name, runner_id=live_runner_id
|
||||
)
|
||||
resp = _post_user_message(http_client, session_id, "Tell me about Toronto, Canada.")
|
||||
assert resp.status_code == 202, f"unexpected status: {resp.status_code} {resp.text[:300]}"
|
||||
verdict = resp.json()
|
||||
# ``denied: true`` proves the classifier ran and the DENY
|
||||
# short-circuited the turn synchronously (no queued item).
|
||||
assert verdict.get("denied") is True, f"expected classifier DENY verdict; got {verdict}"
|
||||
# The author's prompt instructs the classifier to emit exactly
|
||||
# ``"mentions Canada"`` as the reason. Casefold-compare so model
|
||||
# capitalization variance doesn't break the test. A 401/gateway
|
||||
# error reason here means the classifier didn't reach the gateway.
|
||||
assert "canada" in verdict.get("reason", "").lower(), (
|
||||
f"DENY verdict didn't carry the expected reason ('Canada'); got {verdict}"
|
||||
assert verdict.get("denied") is True, (
|
||||
f"Expected synchronous DENY from prompt policy; got {verdict}"
|
||||
)
|
||||
assert verdict.get("reason"), f"Expected a deny reason; got {verdict}"
|
||||
|
||||
Reference in New Issue
Block a user