personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4"""OpenHands dependency boundary tests for context-window classification."""
5
6from __future__ import annotations
7
8from types import ModuleType
9from typing import Any
10
11import pytest
12
13from solstone.think.providers.shared import (
14 _CONTEXT_WINDOW_PATTERNS,
15 classify_provider_error,
16)
17
18# Documents the OpenHands SDK 1.27.1 wrapper split for Solstone's recognised
19# context-window messages. The behavioral invariant is classification below;
20# this mapping makes dependency drift explicit and reviewable.
21_OPENHANDS_SDK_1_27_1_CONTEXT_WRAPPERS = {
22 "exceeds the available context size": "LLMBadRequestError",
23 "context size has been exceeded": "LLMBadRequestError",
24 "exceeds the context window": "LLMBadRequestError",
25 "maximum context length": "LLMBadRequestError",
26 "longer than the model's context length": "LLMBadRequestError",
27 "context length exceeded": "LLMContextWindowExceedError",
28}
29
30
31def _require_attrs(module: ModuleType, *names: str) -> list[Any]:
32 missing = [name for name in names if not hasattr(module, name)]
33 if missing:
34 pytest.skip(f"{module.__name__} does not expose {', '.join(missing)}")
35 return [getattr(module, name) for name in names]
36
37
38def _openhands_sdk_version() -> str:
39 openhands_sdk = pytest.importorskip("openhands.sdk")
40 return str(getattr(openhands_sdk, "__version__", "unknown"))
41
42
43def _mapped_openhands_exception(message: str) -> BaseException:
44 litellm_exceptions = pytest.importorskip("litellm.exceptions")
45 openhands_mapping = pytest.importorskip("openhands.sdk.llm.exceptions.mapping")
46 (bad_request_error,) = _require_attrs(litellm_exceptions, "BadRequestError")
47 (map_provider_exception,) = _require_attrs(
48 openhands_mapping, "map_provider_exception"
49 )
50
51 exc = bad_request_error(
52 message,
53 model="gemini-test",
54 llm_provider="google",
55 )
56 return map_provider_exception(exc)
57
58
59def test_expected_wrapper_mapping_covers_every_context_pattern() -> None:
60 assert set(_OPENHANDS_SDK_1_27_1_CONTEXT_WRAPPERS) == set(
61 _CONTEXT_WINDOW_PATTERNS
62 ), (
63 "OpenHands context-wrapper expectation must cover every Solstone "
64 "context-window pattern before the dependency contract can be reviewed."
65 )
66
67
68@pytest.mark.parametrize("message", _CONTEXT_WINDOW_PATTERNS)
69def test_context_window_patterns_classify_after_real_openhands_mapping(
70 message: str,
71) -> None:
72 mapped = _mapped_openhands_exception(message)
73
74 assert classify_provider_error(mapped, "google") == "context_window_exceeded"
75
76
77def test_openhands_context_wrapper_split_matches_pinned_contract() -> None:
78 expected = _OPENHANDS_SDK_1_27_1_CONTEXT_WRAPPERS
79 actual = {
80 message: type(_mapped_openhands_exception(message)).__name__
81 for message in _CONTEXT_WINDOW_PATTERNS
82 }
83
84 assert actual == expected, (
85 f"OpenHands SDK {_openhands_sdk_version()} changed the context-window "
86 f"wrapper mapping: {actual!r}. This is a Solstone/OpenHands dependency "
87 "contract; review classify_provider_error's rule ordering before "
88 "changing _OPENHANDS_SDK_1_27_1_CONTEXT_WRAPPERS, don't just update the "
89 "constant."
90 )