"""Prompt-isolation proof for the SDK call options (§11, S10 post-mortem). The S10 live run leaked the operator's Claude Code configuration into every spawned CLI session: ``ClaudeAgentOptions.setting_sources`` defaults to ``None``, which loads ALL filesystem settings (verified against SDK 0.2.110) — session-start hooks injected STATE.md into the model's context, every reply opened with a mandated confirmation line (so a reply was NEVER pure JSON), and each call paid ~10-15k uncached context tokens. ``[]`` is the SDK's documented isolation mode: no filesystem settings, no hooks, no CLAUDE.md. Importing the client here is offline-safe: constructing options touches no network and needs no API key; ``query()`` is replaced with a recording fake. """ from __future__ import annotations from typing import Any, AsyncIterator import pytest from portfolio_optimiser_claude import sdk_client from portfolio_optimiser_claude.contracts import ModelMapContract from portfolio_optimiser_claude.sdk_client import SdkModelClient, build_call_options class TestBuildCallOptions: def test_all_filesystem_settings_are_disabled(self) -> None: # LOAD-BEARING (§11): [] is SDK isolation mode. The default (None) # loads user+project+local settings — hooks and CLAUDE.md leak into # the model's context, exactly the observed S10 failure. options = build_call_options("model-x", max_budget_usd=0.25) assert options.setting_sources == [] def test_the_system_prompt_is_empty(self) -> None: # None serializes to --system-prompt "" (verified against 0.2.110): # no Claude Code preset, no appended operator instructions. options = build_call_options("model-x", max_budget_usd=0.25) assert options.system_prompt is None def test_the_call_stays_bounded_single_turn_no_tools(self) -> None: options = build_call_options("model-x", max_budget_usd=0.25) assert options.max_turns == 1 assert options.tools == [] assert options.max_budget_usd == 0.25 assert options.model == "model-x" class TestCompleteThreadsIsolatedOptions: def test_complete_passes_the_isolated_options_to_query( self, monkeypatch: pytest.MonkeyPatch ) -> None: # Detach-proof: if complete() ever builds its options inline again # (dropping the isolation), this goes RED — grønn-men-død guard. captured: dict[str, Any] = {} def fake_query(*, prompt: str, options: Any) -> AsyncIterator[Any]: captured["prompt"] = prompt captured["options"] = options async def _empty() -> AsyncIterator[Any]: return yield return _empty() monkeypatch.setattr(sdk_client, "query", fake_query) client = SdkModelClient( ModelMapContract(profiles={"anthropic": {"default": "model-default"}}), max_budget_usd_per_call=0.10, ) reply = client.complete("the prompt", role="proposer") assert captured["prompt"] == "the prompt" assert captured["options"].setting_sources == [] assert captured["options"].max_budget_usd == 0.10 assert captured["options"].model == "model-default" # No usage surfaced by the fake → the reply fails CLOSED (§8). assert reply.usage_tokens is None