diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index 4e716638..7170e530 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -450,86 +450,6 @@ class AgentManager: logger.info(f"[MCP-DEBUG] Final mcp_servers: {list(mcp_servers.keys())}") return mcp_servers - def _build_connected_tools_context(self, allowed_tools: list[str]) -> str | None: - """Build a context block describing connected MCP tools and their accounts. - - Tools set to 'deny' and fully-denied servers are excluded. - """ - all_tools = load_all_tools() - mcp_tools = [t for t in all_tools if t.mcp_config and t.enabled and t.auth_status in ("configured", "connected")] - - sections = [] - for tool in mcp_tools: - tool_ref = f"mcp:{tool.name}" - if tool_ref not in allowed_tools and allowed_tools != get_all_tool_names(): - continue - - if _is_fully_denied(tool): - continue - - server_name = _sanitize_server_name(tool.name) - denied = _get_denied_tool_names(tool) - tool_descs = { - k: v for k, v in tool.tool_permissions.get("_tool_descriptions", {}).items() - if k not in denied - } - if not tool_descs: - continue - - lines = [f"MCP Server: {server_name}"] - lines.append(f" Status: {tool.auth_status}") - - if tool.connected_account_email: - lines.append(f" Connected account: {tool.connected_account_email}") - lines.append( - f" IMPORTANT: When calling tools from this server that require an email " - f"parameter (e.g. user_google_email, user_email), always use " - f"\"{tool.connected_account_email}\" automatically — do NOT ask the user." - ) - - # Discord guild scoping — hard restriction. The bot may technically - # be in other servers (across other OpenSwarm users), but this - # specific user only authorized these guild IDs. - if tool.name.lower() == "discord": - guilds = tool.oauth_tokens.get("guilds") or [] - if guilds: - guild_descriptions = ", ".join( - f"{g.get('name', 'Unknown')} ({g.get('id', '')})" for g in guilds - ) - allowed_ids = [g.get("id", "") for g in guilds if g.get("id")] - lines.append( - f" AUTHORIZED DISCORD SERVERS (guild_ids): {guild_descriptions}" - ) - lines.append( - f" HARD RESTRICTION: You MUST only call Discord tools that operate on " - f"these guild_ids: {allowed_ids}. NEVER call Discord tools on any other " - f"guild_id even if the bot has access to it. NEVER list, search, or " - f"enumerate servers outside this list. If a user asks about a server " - f"not in this list, refuse and tell them to authorize it via the Connect " - f"Discord button. This is a security boundary, not a preference." - ) - else: - lines.append( - f" No Discord servers authorized yet. Tell the user to click " - f"'Connect Discord' to add a server before attempting any Discord actions." - ) - - tool_names = list(tool_descs.keys()) - if tool_names: - lines.append(f" Available tools ({len(tool_names)}): {', '.join(tool_names)}") - - sections.append("\n".join(lines)) - - if not sections: - return None - return ( - "\n" - "The following MCP tool servers are connected and available. " - "Use them directly when relevant to the user's request.\n\n" - + "\n\n".join(sections) - + "\n" - ) - def _build_outputs_context(self, active_outputs: list[str] | None = None) -> str | None: """Outputs context for the system prompt. @@ -962,86 +882,6 @@ class AgentManager: return "" return "\n" + "\n".join(lines) + "\n" - # ------------------------------------------------------------------ - # Compaction & token guard (Phase 2) - # - # Triggered by *live* context-usage ratio, not turn count. The signal - # is the same `ctx_used_pct` we already broadcast to the UI on every - # turn: input_tokens / context_window. Three escalating thresholds: - # - compact_threshold_pct (default 0.65): summarize stale tool_results - # and old user/assistant pairs before the next query() call - # - context_soft_cap_pct (default 0.90): pre-send hard guard. After - # compaction, if still over, LRU-trim active_outputs/active_mcps - # - >= 1.0 hits the proxy/Anthropic 200K ceiling — friendly card - # surfaces from the catch-all - # ------------------------------------------------------------------ - - @staticmethod - def _approx_tokens(text: str) -> int: - """Conservative chars/4 estimate. Used for the pre-send guard - and the compaction trigger when a precise count_tokens isn't - cheap (or the route isn't Anthropic). Errs slightly high so we - compact a touch earlier than strictly necessary.""" - return max(1, len(text or "") // 4) - - @staticmethod - def _summarize_message_block(messages: list) -> str: - """Programmatic, no-LLM summary of a message slice. Mirrors the - shape of browser_agent._summarize_messages: extracts the original - user task, counts tool calls, captures the last assistant text. - Cheap, deterministic, and never makes a network call — so - compaction itself adds zero latency to the user's turn. - """ - if not messages: - return "" - - initial_task = "" - for m in messages: - if getattr(m, "role", "") == "user": - content = getattr(m, "content", "") - txt = content if isinstance(content, str) else str(content) - if txt.strip(): - initial_task = txt.strip()[:400] - break - - tool_calls_by_name: dict[str, int] = {} - last_tool_results = 0 - last_assistant_text = "" - for m in messages: - role = getattr(m, "role", "") - if role == "tool_call": - content = getattr(m, "content", {}) or {} - name = (content.get("tool") if isinstance(content, dict) else None) or "unknown" - tool_calls_by_name[name] = tool_calls_by_name.get(name, 0) + 1 - elif role == "tool_result": - last_tool_results += 1 - elif role == "assistant": - content = getattr(m, "content", "") - if isinstance(content, str) and content.strip(): - last_assistant_text = content.strip() - elif isinstance(content, list): - for block in content: - if isinstance(block, dict) and block.get("type") == "text": - txt = (block.get("text") or "").strip() - if txt: - last_assistant_text = txt - - parts = [""] - parts.append("[The following is a programmatic summary of earlier turns in this session. Originals are preserved on disk and viewable via the chat UI's compaction drawer.]") - if initial_task: - parts.append(f'Initial user request: "{initial_task}"') - if tool_calls_by_name: - total = sum(tool_calls_by_name.values()) - top = sorted(tool_calls_by_name.items(), key=lambda kv: -kv[1])[:8] - parts.append(f"Tool calls so far ({total} total): " + ", ".join(f"{n}×{c}" for n, c in top)) - if last_tool_results: - parts.append(f"Tool results received: {last_tool_results}") - if last_assistant_text: - parts.append("Last assistant message:") - parts.append(last_assistant_text[:1200]) - parts.append("") - return "\n".join(parts) - def _maybe_compact(self, session: AgentSession, force: bool = False) -> bool: """Run summarizer when ctx_used_pct >= compact_threshold_pct (or force). diff --git a/backend/apps/agents/browser_agent.py b/backend/apps/agents/browser_agent.py index 97591d39..7bd8fce5 100644 --- a/backend/apps/agents/browser_agent.py +++ b/backend/apps/agents/browser_agent.py @@ -20,12 +20,6 @@ from backend.apps.tools_lib.tools_lib import load_builtin_permissions logger = logging.getLogger(__name__) -MODEL_MAP = { - "sonnet": "claude-sonnet-4-6", - "opus": "claude-opus-4-6", - "haiku": "claude-haiku-4-5-20251001", -} - # Cache of conversation history per browser_id so successive BrowserAgent # calls on the same browser can resume rather than restart from scratch. # Without this every "swipe right" / "swipe left" call has to take a new @@ -35,11 +29,6 @@ _browser_history: dict[str, list[dict]] = {} _MAX_HISTORY_MESSAGES = 30 -def clear_browser_history(browser_id: str) -> None: - """Drop cached conversation history for a browser (e.g. when it's closed).""" - _browser_history.pop(browser_id, None) - - # --------------------------------------------------------------------------- # Loop detection # diff --git a/backend/apps/agents/mcp_preflight.py b/backend/apps/agents/mcp_preflight.py index 3492fee3..9a4cd0c6 100644 --- a/backend/apps/agents/mcp_preflight.py +++ b/backend/apps/agents/mcp_preflight.py @@ -32,35 +32,6 @@ from backend.apps.tools_lib.tools_lib import _load_all as load_all_tools logger = logging.getLogger(__name__) -# --------------------------------------------------------------------------- -# Tool-agnostic discovery scaffolding — appended to the agent's system prompt -# only when preflight flags the prompt as vague/information-gathering. -# --------------------------------------------------------------------------- -DISCOVERY_SCAFFOLDING = ( - "# Discovery before action\n" - "When a request is vague or could be grounded in user context, do not " - "guess generic defaults. First silently enumerate what would change the " - "output — voice, tone, audience, prior context, recent precedent, facts " - "that only live in the user's data. Then look at your available tools and " - "pick the ones that could answer those unknowns. Read a few examples " - "(usually 3–10 is enough), summarize what you found into a few bullets, " - "then act confidently.\n\n" - "Tool-selection hierarchy for information gathering:\n" - " 1. Direct local access (filesystem reads, code search, shell) — " - "cheapest and fastest.\n" - " 2. Connected services / MCP tools — for user data that lives in a " - "linked account (email, calendar, notes, tickets, etc.).\n" - " 3. Web search / fetch — for public information that isn't in your " - "training cutoff.\n" - " 4. Browser automation — only when a real interactive session or " - "login is required.\n" - " 5. Sub-agents — only for parallelizable subtasks or to isolate heavy " - "context. Not for serial steps.\n\n" - "Asking the user is a fallback, not a first move. Never fabricate. If " - "no tool can ground a critical unknown, ask one concise question." -) - - # --------------------------------------------------------------------------- # Curated MCP shortlist. These `id` values MUST match the exact `name` field # on ToolDefinition entries that OpenSwarm ships as defaults (see diff --git a/backend/apps/agents/providers/registry.py b/backend/apps/agents/providers/registry.py index 5b007845..89bb3fb1 100644 --- a/backend/apps/agents/providers/registry.py +++ b/backend/apps/agents/providers/registry.py @@ -184,92 +184,6 @@ BUILTIN_MODELS: dict[str, list[dict[str, Any]]] = { ], } -# --------------------------------------------------------------------------- -# Thinking level translation -# --------------------------------------------------------------------------- -# Each provider has a different API shape for "how hard should the model -# think." We expose a single provider-agnostic level (off/low/medium/high/ -# auto) on the session and translate here. -# -# Returns the provider-specific payload to merge into request params, or -# None if no special thinking params should be sent (use defaults). - -def thinking_params_for(api: str, level: str, model_id: str = "") -> dict | None: - """Translate a provider-agnostic thinking level to per-provider API params. - - Args: - api: "anthropic" | "codex" | "gemini-cli" - level: "off" | "low" | "medium" | "high" | "auto" - model_id: optional, used to pick adaptive vs legacy for Claude - - Returns a dict to merge into request params, or None for "use defaults". - """ - if level == "auto": - # Let provider use its own default. For Claude 4.6 we still want - # adaptive thinking on by default so users see reasoning. - if api == "anthropic": - return {"thinking": {"type": "adaptive"}} - return None - - if level == "off": - if api == "anthropic": - return {"thinking": {"type": "disabled"}} - if api == "codex": - return {"reasoning": {"effort": "none"}} - # Gemini: thinkingBudget=0 truly disables reasoning (no - # thoughtSignature emitted). Critical for multi-step tool turns - # — without this Gemini 2.5/3.x still emits signatures even at - # the lowest "level," which then break the next request with - # "Thought signature is not valid" 400 because the SDK has no - # way to round-trip them. The translator at 9Router 0.3.60 - # explicitly checks `thinkingBudget == 0` to skip emitting - # thinking config, which is what we want. - if api == "gemini-cli": - return {"thinkingConfig": {"thinkingBudget": 0}} - return None - - # Claude 4.6 models use adaptive thinking (no manual budget). For older - # Claude models we'd use budget_tokens; we don't ship those today. - if api == "anthropic": - return {"thinking": {"type": "adaptive"}} - - if api == "codex": - effort_map = {"low": "low", "medium": "medium", "high": "high"} - return {"reasoning": {"effort": effort_map[level]}} - - if api == "gemini-cli": - level_map = {"low": "LOW", "medium": "MEDIUM", "high": "HIGH"} - return {"thinkingConfig": {"thinkingLevel": level_map[level]}} - - return None - - -# --------------------------------------------------------------------------- -# OpenRouter: built-in integration for 300+ models -# --------------------------------------------------------------------------- - -OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" - -_9router_cache: dict = {"available": None, "checked_at": 0} - - -def _is_9router_available() -> bool: - """Check if 9Router is running on localhost:20128. Caches for 30 seconds.""" - import time as _time - now = _time.time() - if _9router_cache["available"] is not None and now - _9router_cache["checked_at"] < 30: - return _9router_cache["available"] - try: - import httpx - r = httpx.get("http://localhost:20128/v1/models", timeout=2.0) - available = r.status_code == 200 - except Exception: - available = False - _9router_cache["available"] = available - _9router_cache["checked_at"] = now - return available - - # --------------------------------------------------------------------------- # Model resolution (used by the live claude_agent_sdk path) # --------------------------------------------------------------------------- @@ -485,24 +399,6 @@ async def resolve_aux_model( ) -def get_context_window(provider: str, model: str, settings: AppSettings | None = None) -> int: - """Look up context window for any model.""" - # Check built-in models first - for models in BUILTIN_MODELS.values(): - for m in models: - if m["value"] == model: - return m.get("context_window", 128_000) - - # Check custom providers - if settings: - for cp in getattr(settings, "custom_providers", []): - for m in cp.models: - if m.get("value") == model or m.get("id") == model: - return m.get("context_window", 128_000) - - return 128_000 # safe default - - # --------------------------------------------------------------------------- # Cost tracking # --------------------------------------------------------------------------- diff --git a/backend/apps/agents/tools/base.py b/backend/apps/agents/tools/base.py index cfca6418..1cf00fb0 100644 --- a/backend/apps/agents/tools/base.py +++ b/backend/apps/agents/tools/base.py @@ -14,10 +14,6 @@ class BaseTool(ABC): name: str description: str - @abstractmethod - def get_schema(self) -> dict: - ... - @abstractmethod async def execute(self, input_data: dict, context: ToolContext) -> list[dict]: ... diff --git a/backend/apps/agents/tools/web.py b/backend/apps/agents/tools/web.py index 0609e3b2..e575775c 100644 --- a/backend/apps/agents/tools/web.py +++ b/backend/apps/agents/tools/web.py @@ -49,24 +49,6 @@ class WebSearchTool(BaseTool): "snippets for the top results." ) - def get_schema(self) -> dict: - return { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "The search query.", - }, - "num_results": { - "type": "integer", - "description": "Maximum number of results to return (default 5).", - "default": 5, - }, - }, - "required": ["query"], - "additionalProperties": False, - } - async def execute(self, input_data: dict, context: ToolContext) -> list[dict]: query: str = input_data["query"] num_results: int = input_data.get("num_results", 5) @@ -163,23 +145,6 @@ class WebFetchTool(BaseTool): "HTML is stripped to plain text. Output capped at ~250 KB." ) - def get_schema(self) -> dict: - return { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "The URL to fetch.", - }, - "prompt": { - "type": "string", - "description": "Optional prompt/context describing what information to look for.", - }, - }, - "required": ["url"], - "additionalProperties": False, - } - async def execute(self, input_data: dict, context: ToolContext) -> list[dict]: url: str = input_data["url"] prompt: str | None = input_data.get("prompt") diff --git a/backend/apps/outputs/outputs.py b/backend/apps/outputs/outputs.py index 1c8ef4f9..0a63fd7b 100644 --- a/backend/apps/outputs/outputs.py +++ b/backend/apps/outputs/outputs.py @@ -22,17 +22,6 @@ from backend.apps.settings.settings import load_settings logger = logging.getLogger(__name__) -MODEL_MAP = { - "sonnet": "claude-sonnet-4-20250514", - "opus": "claude-opus-4-20250514", - "haiku": "claude-haiku-4-5-20251001", -} - - -def _resolve_model(short_name: str) -> str: - return MODEL_MAP.get(short_name, short_name) - - def _get_anthropic_client(api_model: str | None = None): """Create an AsyncAnthropic client using the API key from app settings. @@ -184,15 +173,6 @@ def _load(output_id: str) -> Output: return Output(**json.load(f)) -def load_output(output_id: str) -> Output | None: - """Public helper for other modules to resolve an output by ID.""" - path = os.path.join(DATA_DIR, f"{output_id}.json") - if not os.path.exists(path): - return None - with open(path) as f: - return Output(**json.load(f)) - - def _walk_directory(folder: str) -> dict[str, str]: """Walk a directory tree and return {relative_path: content} for all text files.""" files: dict[str, str] = {} diff --git a/backend/apps/service/client.py b/backend/apps/service/client.py index d9ee2720..92bfc9ab 100644 --- a/backend/apps/service/client.py +++ b/backend/apps/service/client.py @@ -46,7 +46,6 @@ _MAX_INFLIGHT = 16 _test_sink: Optional[Any] = None _install_id: Optional[str] = None -_user_id: Optional[str] = None _inflight = 0 _inflight_lock = asyncio.Lock() _drain_lock = asyncio.Lock() @@ -85,9 +84,6 @@ def _get_install_id() -> str: def _get_user_id() -> Optional[str]: - global _user_id - if _user_id: - return _user_id try: from backend.apps.settings.settings import load_settings s = load_settings() @@ -96,11 +92,6 @@ def _get_user_id() -> Optional[str]: return None -def set_user_id(uid: Optional[str]) -> None: - global _user_id - _user_id = uid or None - - def _is_enabled(kind: str) -> bool: """Honour user opt-out. Diagnostic always flows (errors block usability); state + session honour the toggle.""" diff --git a/backend/apps/settings/credentials.py b/backend/apps/settings/credentials.py index 9883d134..5a321b95 100644 --- a/backend/apps/settings/credentials.py +++ b/backend/apps/settings/credentials.py @@ -75,36 +75,6 @@ def validate_credentials(settings: AppSettings, provider: str = "anthropic") -> return -def get_provider_credentials(settings: AppSettings, provider: str) -> dict[str, str]: - """Return credential dict for a specific provider.""" - p = provider.lower().strip() - validate_credentials(settings, provider) - - if p in ("anthropic", "claude"): - if getattr(settings, "connection_mode", "own_key") == "openswarm-pro": - return { - "auth_token": getattr(settings, "openswarm_bearer_token", "") or "", - "base_url": getattr(settings, "openswarm_proxy_url", None) or OPENSWARM_DEFAULT_PROXY_URL, - } - return {"api_key": settings.anthropic_api_key or ""} - - if p in ("openai", "codex"): - return {"api_key": settings.openai_api_key or ""} - - if p in ("gemini", "google", "gemini-cli"): - return {"api_key": getattr(settings, "google_api_key", "") or ""} - - if p == "openrouter": - return {"api_key": getattr(settings, "openrouter_api_key", "") or ""} - - # Custom provider - for cp in getattr(settings, "custom_providers", []): - if cp.name.lower() == p: - return {"api_key": cp.api_key, "base_url": cp.base_url} - - raise ValueError(f"No credentials for provider: {provider}") - - # --------------------------------------------------------------------------- # Legacy helpers (kept for backward compat during migration) # --------------------------------------------------------------------------- diff --git a/backend/main.py b/backend/main.py index 38b1097e..7a62b590 100644 --- a/backend/main.py +++ b/backend/main.py @@ -207,13 +207,6 @@ async def websocket_session(websocket: WebSocket, session_id: str): "message": payload.get("message"), "updated_input": payload.get("updated_input"), }) - elif event == "agent:edit_message": - from backend.apps.agents.agent_manager import agent_manager - await agent_manager.edit_message( - session_id, - payload.get("message_id", ""), - payload.get("content", ""), - ) elif event == "agent:stop": from backend.apps.agents.agent_manager import agent_manager await agent_manager.stop_agent(session_id) diff --git a/backend/tests/test_agent_manager_unit.py b/backend/tests/test_agent_manager_unit.py index c3e63b87..241a23e8 100644 --- a/backend/tests/test_agent_manager_unit.py +++ b/backend/tests/test_agent_manager_unit.py @@ -12,8 +12,7 @@ Test groups: - pure instance methods (`_resolve_mode`, `_compose_system_prompt`, `_resolve_context_paths`, `_build_dir_tree`, `_resolve_forced_tools`, `_resolve_attached_skills`, `_get_branch_messages`, - `_build_history_prefix`, `_approx_tokens`, - `_summarize_message_block`, `_truncate_large_tool_result`, + `_build_history_prefix`, `_truncate_large_tool_result`, `_build_search_text`, `_maybe_compact`) - lifecycle (launch/update/edit/switch_branch/duplicate/close/ delete/resume/stop, history, browser-agent children, approval, @@ -564,7 +563,7 @@ def test_get_branch_messages_walks_fork_lineage(): # --------------------------------------------------------------------------- -# _build_history_prefix / _approx_tokens / _summarize_message_block +# _build_history_prefix # --------------------------------------------------------------------------- @@ -587,59 +586,6 @@ def test_build_history_prefix_empty_returns_empty(): assert AgentManager._build_history_prefix([]) == "" -@pytest.mark.parametrize("text,expected", [ - ("", 1), - ("a" * 4, 1), - ("a" * 16, 4), - ("a" * 100, 25), -]) -def test_approx_tokens_chars_over_four(text, expected): - assert AgentManager._approx_tokens(text) == expected - - -def test_approx_tokens_handles_none(): - assert AgentManager._approx_tokens(None) == 1 - - -def test_summarize_message_block_empty_returns_empty(): - assert AgentManager._summarize_message_block([]) == "" - - -def test_summarize_message_block_extracts_initial_task_and_counts(): - msgs = [ - Message(role="user", content="please do the thing"), - Message(role="tool_call", content={"tool": "Read", "input": {}}), - Message(role="tool_call", content={"tool": "Bash", "input": {}}), - Message(role="tool_call", content={"tool": "Read", "input": {}}), - Message(role="tool_result", content="ok"), - Message(role="assistant", content="done"), - ] - out = AgentManager._summarize_message_block(msgs) - assert "" in out - assert "please do the thing" in out - # Tool counts - assert "Read×2" in out - assert "Bash×1" in out - assert "Tool calls so far (3 total)" in out - assert "Tool results received: 1" in out - assert "Last assistant message:" in out - assert "done" in out - - -def test_summarize_message_block_assistant_list_content(): - """Assistant messages with list content (Anthropic block shape) - should still surface their text.""" - msgs = [ - Message(role="user", content="task"), - Message(role="assistant", content=[ - {"type": "text", "text": "the answer"}, - {"type": "tool_use", "id": "t1"}, - ]), - ] - out = AgentManager._summarize_message_block(msgs) - assert "the answer" in out - - # --------------------------------------------------------------------------- # _truncate_large_tool_result # --------------------------------------------------------------------------- diff --git a/backend/tests/test_browser_agent_unit.py b/backend/tests/test_browser_agent_unit.py index 2f0e7ef2..82fd7946 100644 --- a/backend/tests/test_browser_agent_unit.py +++ b/backend/tests/test_browser_agent_unit.py @@ -11,8 +11,7 @@ in small helpers that can be exercised in isolation: conversations. - `_format_tool_result` — translate `ws_manager` result dicts into Anthropic content blocks. - - `clear_browser_history` / `execute_browser_tool` — small async + state - helpers. + - `execute_browser_tool` — small async + state helper. The integration-level tests for `run_browser_agent` / `run_browser_agents` / `_create_browser_card` live in the sister file @@ -40,7 +39,6 @@ from backend.apps.agents.browser_agent import ( _summarize_messages, _trim_history_by_turns, _validate_message_pairing, - clear_browser_history, execute_browser_tool, ) @@ -59,28 +57,6 @@ def _clear_browser_history_module_state(): ba._browser_history.clear() -# --------------------------------------------------------------------------- -# clear_browser_history -# --------------------------------------------------------------------------- - - -def test_clear_browser_history_drops_only_target_entry(): - ba._browser_history["b-1"] = [{"role": "user", "content": "hi"}] - ba._browser_history["b-2"] = [{"role": "user", "content": "yo"}] - - clear_browser_history("b-1") - - assert "b-1" not in ba._browser_history - assert "b-2" in ba._browser_history - - -def test_clear_browser_history_missing_id_is_noop(): - """Calling clear on an unknown id must not raise (it's used in - cleanup paths where the cache may already be empty).""" - clear_browser_history("never-existed") - assert ba._browser_history == {} - - # --------------------------------------------------------------------------- # _hash_tool_call # --------------------------------------------------------------------------- diff --git a/backend/tests/test_outputs_unit.py b/backend/tests/test_outputs_unit.py index 753c7e48..827d722c 100644 --- a/backend/tests/test_outputs_unit.py +++ b/backend/tests/test_outputs_unit.py @@ -4,12 +4,12 @@ These exercise pure logic and pydantic models without booting FastAPI. The integration surface (routes) lives in `test_api_outputs.py`. Covers: - - outputs.py helpers: _resolve_model, _validate_against_schema, + - outputs.py helpers: _validate_against_schema, _build_data_injection, _inject_data_into_html, _inject_token_into_relative_urls (every branch in _ABSOLUTE_URL_PREFIXES + token-already-present + fragment), _decode_data_param, _walk_directory. - - On-disk store helpers: _save / _load / load_output / _load_all. + - On-disk store helpers: _save / _load / _load_all. - Models: legacy `frontend_code` / `backend_code` / `schema_json` migration into `files`, plus the property accessors. - executor.execute_backend_code: happy path, stdout capture, @@ -35,12 +35,9 @@ from backend.apps.outputs.outputs import ( _inject_token_into_relative_urls, _load, _load_all, - _resolve_model, _save, _validate_against_schema, _walk_directory, - load_output, - MODEL_MAP, ) from backend.apps.outputs.models import ( AutoRunConfig, @@ -55,22 +52,6 @@ from backend.apps.outputs.executor import ( ) -# --------------------------------------------------------------------------- -# _resolve_model -# --------------------------------------------------------------------------- - - -def test_resolve_model_known_short_name(): - assert _resolve_model("sonnet") == MODEL_MAP["sonnet"] - assert _resolve_model("opus") == MODEL_MAP["opus"] - assert _resolve_model("haiku") == MODEL_MAP["haiku"] - - -def test_resolve_model_unknown_passthrough(): - assert _resolve_model("claude-3-5-haiku") == "claude-3-5-haiku" - assert _resolve_model("") == "" - - # --------------------------------------------------------------------------- # _validate_against_schema # --------------------------------------------------------------------------- @@ -272,7 +253,7 @@ def test_walk_directory_skips_unreadable(tmp_path): # --------------------------------------------------------------------------- -# _load_all / _save / _load / load_output +# _load_all / _save / _load # --------------------------------------------------------------------------- @@ -291,18 +272,6 @@ def test_load_missing_raises_404(tmp_data_dirs): assert exc.value.status_code == 404 -def test_load_output_returns_none_for_missing(tmp_data_dirs): - assert load_output("does-not-exist") is None - - -def test_load_output_returns_resolved(tmp_data_dirs): - out = Output(name="x") - _save(out) - fetched = load_output(out.id) - assert fetched is not None - assert fetched.name == "x" - - def test_load_all_picks_up_saved(tmp_data_dirs): a = Output(name="a") b = Output(name="b") diff --git a/backend/tests/test_service.py b/backend/tests/test_service.py index acc0f2c7..2d5f57ba 100644 --- a/backend/tests/test_service.py +++ b/backend/tests/test_service.py @@ -45,7 +45,6 @@ def patch_settings(tmp_path): def fresh_client(tmp_path): import backend.apps.service.client as client client._install_id = None - client._user_id = None client._test_sink = None spool = tmp_path / "spool.db" with patch.object(client, "_spool_path", lambda: str(spool)): @@ -78,35 +77,42 @@ def test_sync_carries_install_id(sink): assert body["client_state"]["install_id"] == "test-install-abc" -def test_sync_carries_user_id_when_set(sink): - from backend.apps.service.client import sync, set_user_id - set_user_id("alice@example.com") - sync({}) +def test_sync_carries_user_id_when_set_in_settings(tmp_path, sink): + """User id is read from settings.user_email at envelope-build time.""" + sf = tmp_path / "settings_with_email.json" + sf.write_text(json.dumps({ + "installation_id": "test-install-abc", + "analytics_opt_in": True, + "user_email": "alice@example.com", + })) + import backend.apps.settings.settings as settings_mod + with patch.object(settings_mod, "SETTINGS_FILE", str(sf)): + from backend.apps.service.client import sync + sync({}) _, body = sink[0] assert body["client_state"]["user_id"] == "alice@example.com" -def test_sync_no_user_id_when_not_set(sink): +def test_sync_no_user_id_when_email_not_set(sink): + """No user_email in settings → user_id absent from envelope.""" from backend.apps.service.client import sync sync({}) _, body = sink[0] assert "user_id" not in body["client_state"] -def test_sync_user_id_cleared_with_none(sink): - from backend.apps.service.client import sync, set_user_id - set_user_id("alice") - set_user_id(None) - sync({}) - _, body = sink[0] - assert "user_id" not in body["client_state"] - - -def test_sync_user_id_cleared_with_empty(sink): - from backend.apps.service.client import sync, set_user_id - set_user_id("alice") - set_user_id("") - sync({}) +def test_sync_no_user_id_when_email_empty(tmp_path, sink): + """Empty-string user_email is treated like missing.""" + sf = tmp_path / "settings_empty_email.json" + sf.write_text(json.dumps({ + "installation_id": "test-install-abc", + "analytics_opt_in": True, + "user_email": "", + })) + import backend.apps.settings.settings as settings_mod + with patch.object(settings_mod, "SETTINGS_FILE", str(sf)): + from backend.apps.service.client import sync + sync({}) _, body = sink[0] assert "user_id" not in body["client_state"] diff --git a/backend/tests/test_v2_invariants.py b/backend/tests/test_v2_invariants.py index 304ff1d0..5ea2af25 100644 --- a/backend/tests/test_v2_invariants.py +++ b/backend/tests/test_v2_invariants.py @@ -926,23 +926,6 @@ def test_find_builtin_model_returns_dict_for_known(): assert sonnet.get("api") == "anthropic" -# =========================================================================== -# Group R — context window -# =========================================================================== - - -def test_get_context_window_known_model(): - from backend.apps.agents.providers.registry import get_context_window - cw = get_context_window("Anthropic", "sonnet") - assert cw >= 200_000 - - -def test_get_context_window_unknown_returns_default(): - from backend.apps.agents.providers.registry import get_context_window - cw = get_context_window("Unknown", "fake-model") - assert cw == 128_000 - - # =========================================================================== # Group S — calculate_cost regression tests # =========================================================================== @@ -1051,18 +1034,16 @@ def test_web_tools_classes_inherit_basetool(): assert issubclass(WebFetchTool, BaseTool) -def test_web_search_tool_has_name_and_schema(): +def test_web_search_tool_has_name(): from backend.apps.agents.tools.web import WebSearchTool tool = WebSearchTool() assert tool.name - assert isinstance(tool.get_schema(), dict) -def test_web_fetch_tool_has_name_and_schema(): +def test_web_fetch_tool_has_name(): from backend.apps.agents.tools.web import WebFetchTool tool = WebFetchTool() assert tool.name - assert isinstance(tool.get_schema(), dict) # ===========================================================================