chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:12:33 +08:00
commit aacb60a4af
3387 changed files with 981307 additions and 0 deletions
@@ -0,0 +1,168 @@
from __future__ import annotations
from collections.abc import AsyncIterator
from typing import Any
import pytest
from opensquilla.engine import Agent, AgentConfig, ToolResult
from opensquilla.engine.types import ToolCall
from opensquilla.execution_status import runtime_execution_status
from opensquilla.provider import (
ChatConfig,
ContentBlockToolResult,
ContentBlockToolUse,
Message,
ToolDefinition,
ToolInputSchema,
)
from opensquilla.provider import DoneEvent as ProviderDone
from opensquilla.provider import TextDeltaEvent as ProviderText
from opensquilla.provider import ToolUseEndEvent as ProviderToolUseEnd
from opensquilla.provider import ToolUseStartEvent as ProviderToolUseStart
class _BoundaryProvider:
provider_name = "synthetic"
def __init__(self, large_local_argument: str) -> None:
self.large_local_argument = large_local_argument
self.calls: list[list[Message]] = []
def chat(
self,
messages: list[Message],
tools: list[Any] | None = None,
config: ChatConfig | None = None,
) -> AsyncIterator[Any]:
self.calls.append(messages)
return self._stream(len(self.calls))
async def _stream(self, call_number: int) -> AsyncIterator[Any]:
if call_number == 1:
calls = [
(
"local-large-1",
"local_context_builder",
{"content": self.large_local_argument, "label": "local"},
),
("fetch-large-0", "web_fetch", {"url": "https://example.com/0"}),
("fetch-large-1", "web_fetch", {"url": "https://example.com/1"}),
("fetch-error-2", "web_fetch", {"url": "https://example.com/2"}),
]
for tool_use_id, tool_name, arguments in calls:
yield ProviderToolUseStart(
tool_use_id=tool_use_id,
tool_name=tool_name,
)
yield ProviderToolUseEnd(
tool_use_id=tool_use_id,
tool_name=tool_name,
arguments=arguments,
)
yield ProviderDone(stop_reason="tool_use", input_tokens=500, output_tokens=50)
return
yield ProviderText(text="BOUNDARY_E2E_OK")
yield ProviderDone(stop_reason="stop", input_tokens=500, output_tokens=10)
async def list_models(self) -> list[Any]:
return []
def _tool_def(name: str) -> ToolDefinition:
return ToolDefinition(
name=name,
description=f"Synthetic {name}.",
input_schema=ToolInputSchema(properties={"content": {}, "url": {}, "label": {}}),
)
def _tool_blocks(messages: list[Message]) -> list[ContentBlockToolUse]:
blocks: list[ContentBlockToolUse] = []
for message in messages:
if not isinstance(message.content, list):
continue
blocks.extend(
block for block in message.content if isinstance(block, ContentBlockToolUse)
)
return blocks
def _tool_result_blocks(messages: list[Message]) -> list[ContentBlockToolResult]:
blocks: list[ContentBlockToolResult] = []
for message in messages:
if not isinstance(message.content, list):
continue
blocks.extend(
block for block in message.content if isinstance(block, ContentBlockToolResult)
)
return blocks
@pytest.mark.asyncio
async def test_agent_multi_turn_boundary_e2e_keeps_critical_context() -> None:
large_local_argument = "LOCAL_ARGUMENT_START\n" + ("x" * 24_000)
provider = _BoundaryProvider(large_local_argument)
async def tool_handler(call: ToolCall) -> ToolResult:
if call.tool_use_id == "local-large-1":
return ToolResult(
tool_use_id=call.tool_use_id,
tool_name=call.tool_name,
content="local context stored",
is_error=False,
)
if call.tool_use_id == "fetch-error-2":
return ToolResult(
tool_use_id=call.tool_use_id,
tool_name=call.tool_name,
content="FETCH_FAILURE_MARKER " + ("e" * 8_000),
is_error=True,
execution_status=runtime_execution_status(
"error",
reason="runtime_error",
),
)
suffix = call.tool_use_id.rsplit("-", 1)[-1]
return ToolResult(
tool_use_id=call.tool_use_id,
tool_name=call.tool_name,
content=f"FETCH_RESULT_{suffix}_START\n" + ("r" * 48_000),
is_error=False,
)
agent = Agent(
provider=provider,
config=AgentConfig(
context_window_tokens=200_000,
max_tokens=8192,
flush_enabled=False,
max_iterations=4,
),
tool_definitions=[_tool_def("local_context_builder"), _tool_def("web_fetch")],
tool_handler=tool_handler,
)
events = [event async for event in agent.run_turn("exercise context boundaries")]
assert any(event.kind == "done" and event.text == "BOUNDARY_E2E_OK" for event in events)
assert len(provider.calls) == 2
replay = provider.calls[1]
tool_uses = _tool_blocks(replay)
local_tool_use = next(block for block in tool_uses if block.id == "local-large-1")
assert local_tool_use.input["content"] == large_local_argument
result_content_by_id = {
block.tool_use_id: block.content for block in _tool_result_blocks(replay)
}
assert str(result_content_by_id["fetch-large-0"]).startswith("FETCH_RESULT_0_START")
assert "external_tool_result_compacted" not in str(
result_content_by_id["fetch-large-0"]
)
assert "FETCH_FAILURE_MARKER" in str(result_content_by_id["fetch-error-2"])
assert all(
"opensquilla_compacted" not in str(block.input)
and "tool_use_argument_projection" not in str(block.input)
for block in tool_uses
)
@@ -0,0 +1,176 @@
from __future__ import annotations
import json
import os
from collections.abc import AsyncIterator
from pathlib import Path
from typing import Any
import pytest
from opensquilla.engine import Agent, AgentConfig, ToolResult
from opensquilla.engine.types import ToolCall
from opensquilla.provider import (
ChatConfig,
Message,
ToolDefinition,
ToolInputSchema,
)
from opensquilla.provider import (
DoneEvent as ProviderDone,
)
from opensquilla.provider import (
TextDeltaEvent as ProviderText,
)
from opensquilla.provider import (
ToolUseEndEvent as ProviderToolUseEnd,
)
from opensquilla.provider import (
ToolUseStartEvent as ProviderToolUseStart,
)
pytestmark = pytest.mark.local_golden
CASES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "agent_chains" / "synthetic_cases"
class _SyntheticCaseProvider:
provider_name = "synthetic"
def __init__(self, turns: list[dict[str, Any]]) -> None:
self._turns = turns
self.calls: list[list[Message]] = []
def chat(
self,
messages: list[Message],
tools: list[Any] | None = None,
config: ChatConfig | None = None,
) -> AsyncIterator[Any]:
self.calls.append(messages)
return self._stream(len(self.calls))
async def _stream(self, call_number: int) -> AsyncIterator[Any]:
turn = self._turns[call_number - 1]
tool_calls = turn.get("tool_calls") or []
for tool_call in tool_calls:
yield ProviderToolUseStart(
tool_use_id=tool_call["id"],
tool_name=tool_call["name"],
)
yield ProviderToolUseEnd(
tool_use_id=tool_call["id"],
tool_name=tool_call["name"],
arguments=tool_call["arguments"],
)
if tool_calls:
yield ProviderDone(stop_reason="tool_use", input_tokens=10, output_tokens=2)
return
yield ProviderText(text=turn["final_text"])
yield ProviderDone(stop_reason="stop", input_tokens=12, output_tokens=3)
async def list_models(self) -> list[Any]:
return []
def _tool_def(name: str, properties: dict[str, Any]) -> ToolDefinition:
return ToolDefinition(
name=name,
description=f"Synthetic {name}.",
input_schema=ToolInputSchema(
properties=properties,
required=list(properties),
),
)
def _case_paths() -> list[Path]:
return sorted(CASES_DIR.glob("*.json"))
def _load_case(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def _result_content(raw: Any) -> str:
if isinstance(raw, str):
return raw
return json.dumps(raw, sort_keys=True)
def _message_contains_tool_use(messages: list[Message], tool_use_id: str) -> bool:
return any(
message.role == "assistant"
and any(getattr(block, "id", "") == tool_use_id for block in message.content)
for message in messages
)
def _message_contains_tool_result(messages: list[Message], tool_use_id: str) -> bool:
return any(
message.role == "user"
and any(getattr(block, "tool_use_id", "") == tool_use_id for block in message.content)
for message in messages
)
@pytest.mark.asyncio
@pytest.mark.parametrize("case_path", _case_paths(), ids=lambda path: path.stem)
async def test_synthetic_complex_agent_golden(case_path: Path) -> None:
if os.environ.get("OPENSQUILLA_RUN_LOCAL_GOLDENS") != "1":
pytest.skip("set OPENSQUILLA_RUN_LOCAL_GOLDENS=1 to run synthetic local goldens")
case = _load_case(case_path)
provider = _SyntheticCaseProvider(case["turns"])
handled: list[tuple[str, str, dict[str, Any], bool]] = []
async def tool_handler(call: ToolCall) -> ToolResult:
payload = case["tool_results"][call.tool_use_id]
is_error = not bool(payload["ok"])
handled.append((call.tool_use_id, call.tool_name, dict(call.arguments), is_error))
return ToolResult(
tool_use_id=call.tool_use_id,
tool_name=call.tool_name,
content=_result_content(payload["content"]),
is_error=is_error,
)
agent = Agent(
provider=provider,
config=AgentConfig(max_iterations=int(case["max_iterations"])),
tool_definitions=[
_tool_def(tool["name"], tool["properties"])
for tool in case["tools"]
],
tool_handler=tool_handler,
)
events = [event async for event in agent.run_turn(case["prompt"])]
expect = case["expect"]
assert [tool_name for _tool_id, tool_name, _args, _is_error in handled] == expect[
"tool_order"
]
assert [tool_id for tool_id, _tool_name, _args, _is_error in handled] == expect["tool_ids"]
assert [
tool_id for tool_id, _tool_name, _args, is_error in handled if is_error
] == expect["error_tool_ids"]
assert len(provider.calls) == int(expect["iterations"])
assert not any(event.kind == "error" for event in events)
assert any(
event.kind == "done" and event.text == expect["final_text"]
for event in events
)
tool_ids_by_turn = [
[tool_call["id"] for tool_call in turn.get("tool_calls") or []]
for turn in case["turns"]
]
for turn_index, tool_ids in enumerate(tool_ids_by_turn):
if not tool_ids:
continue
replay_call = provider.calls[turn_index + 1]
for tool_id in tool_ids:
assert _message_contains_tool_use(replay_call, tool_id)
assert _message_contains_tool_result(replay_call, tool_id)
@@ -0,0 +1,600 @@
"""Gateway attachment history replay e2e tests.
These tests exercise the production upload -> sessions.send -> transcript
material -> SquillaRouter -> TurnRunner history path with deterministic fake
providers. They intentionally avoid live LLM credentials.
"""
from __future__ import annotations
import asyncio
import base64
import json
import uuid
from collections.abc import AsyncIterator
from pathlib import Path
from typing import Any
import httpx
import pytest
import opensquilla.engine.steps.squilla_router as squilla_router_step
from opensquilla.attachment_refs import transcript_material_path
from opensquilla.engine import Agent, AgentConfig
from opensquilla.engine.runtime import TurnRunner
from opensquilla.gateway import rpc_sessions as _rpc_sessions # noqa: F401
from opensquilla.gateway.agent_tasks import get_agent_task_registry
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.auth import Principal
from opensquilla.gateway.config import GatewayConfig
from opensquilla.gateway.rpc import RpcContext, get_dispatcher
from opensquilla.gateway.uploads import (
AttachmentNotFoundError,
UploadStore,
set_upload_store,
)
from opensquilla.gateway.websocket import SubscriptionManager, get_registry
from opensquilla.provider import ChatConfig, DoneEvent, Message, ModelCapabilities
from opensquilla.provider.types import ContentBlockImage, ModelInfo, TextDeltaEvent
from opensquilla.session.manager import SessionManager
from opensquilla.session.storage import SessionStorage
_PNG_BYTES = (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01"
b"\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4"
b"\x89\x00\x00\x00\nIDATx\x9cc\xf8\x0f\x00\x01\x01"
b"\x01\x00\x18\xdd\x8d\xb0\x00\x00\x00\x00IEND\xaeB`\x82"
)
_TEXT_MODEL = "test/text"
_GATE_MODEL = "test/gate"
_VISION_MODEL = "test/vision"
_TURN_TERMINAL_EVENT_TIMEOUT_SECONDS = 30.0
_TURN_TASK_DRAIN_TIMEOUT_SECONDS = 10.0
class _RecordingProvider:
provider_name = "fake"
def __init__(self, text: str = "ok") -> None:
self.text = text
self.calls: list[dict[str, Any]] = []
async def chat(
self,
messages: list[Message],
tools: list[Any] | None = None,
config: ChatConfig | None = None,
) -> AsyncIterator[Any]:
self.calls.append({"messages": messages, "tools": tools, "config": config})
yield TextDeltaEvent(text=self.text)
yield DoneEvent(stop_reason="end_turn", input_tokens=3, output_tokens=1)
async def list_models(self) -> list[ModelInfo]:
return []
class _RecordingSelector:
active_provider_id = "openrouter"
def __init__(
self,
providers: dict[str, _RecordingProvider],
model: str = _TEXT_MODEL,
) -> None:
self.providers = providers
self.model = model
def clone(self) -> _RecordingSelector:
return _RecordingSelector(self.providers, self.model)
def override_model(self, model: str) -> None:
self.model = model
def override_model_with_fallback_chain(
self,
model: str,
fallback_chain: list[object], # noqa: ARG002
) -> None:
self.override_model(model)
def resolve(self) -> _RecordingProvider:
return self.providers.get(self.model, self.providers[_TEXT_MODEL])
async def list_models(self) -> list[dict[str, Any]]:
return []
class _FakeModelCatalog:
def resolve_max_tokens(
self,
model_id: str, # noqa: ARG002
*,
user_override: int = 0,
provider: str = "openrouter", # noqa: ARG002
) -> int:
return user_override if user_override > 0 else 1024
def resolve_context_window(
self,
model_id: str, # noqa: ARG002
*,
provider: str = "openrouter", # noqa: ARG002
) -> int:
return 8192
def get_capabilities(
self,
model_id: str,
provider_name: str = "openrouter", # noqa: ARG002
base_url: str = "", # noqa: ARG002
) -> ModelCapabilities:
return ModelCapabilities(supports_vision=model_id == _VISION_MODEL)
class _EventSink:
authenticated = True
def __init__(self, conn_id: str) -> None:
self.conn_id = conn_id
self.events: list[tuple[str, dict[str, Any]]] = []
async def send_event(
self,
event: str,
payload: Any = None,
meta: dict[str, Any] | None = None, # noqa: ARG002
) -> None:
self.events.append((event, dict(payload or {})))
class _TextTierStrategy:
async def classify(
self,
message: str, # noqa: ARG002
valid_tiers: list[str],
routing_history: list[dict] | None = None, # noqa: ARG002
**kwargs: object, # noqa: ARG002
) -> tuple[str, float, str, dict[str, Any]]:
tier = "c1" if "c1" in valid_tiers else valid_tiers[0]
return (
tier,
0.87,
"test_text_route",
{
"route_class": "R1",
"thinking_mode": "T1",
"prompt_policy": "P0",
},
)
def _configure_gateway(tmp_path: Path) -> GatewayConfig:
config = GatewayConfig()
config.state_dir = str(tmp_path / "state")
config.workspace_dir = str(tmp_path / "workspace")
config.attachments.media_root = str(tmp_path / "media")
config.squilla_router.enabled = True
config.squilla_router.rollout_phase = "full"
config.squilla_router.require_router_runtime = False
config.squilla_router.vision_history_lookback_turns = 8
config.squilla_router.vision_history_candidate_turns = 8
config.squilla_router.vision_sticky_followup_turns = 3
config.squilla_router.vision_followup_gate_tier = "c0"
config.squilla_router.tiers = {
"c0": {
"provider": "openrouter",
"model": _GATE_MODEL,
"supports_image": False,
},
"c1": {
"provider": "openrouter",
"model": _TEXT_MODEL,
"supports_image": False,
},
"image_model": {
"provider": "openrouter",
"model": _VISION_MODEL,
"supports_image": True,
"image_only": True,
},
}
config.squilla_router.default_tier = "c1"
config.llm.provider = "openrouter"
config.llm.model = _TEXT_MODEL
return config
async def _upload_png(app: Any) -> str:
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(
transport=transport,
base_url="http://testserver",
) as client:
response = await client.post(
"/api/v1/files/upload",
files={"file": ("first.png", _PNG_BYTES, "image/png")},
)
assert response.status_code == 200, response.text
payload = response.json()
file_uuid = payload.get("file_uuid")
assert isinstance(file_uuid, str) and file_uuid.startswith("u-")
return file_uuid
async def _send_session_turn(
*,
ctx: RpcContext,
key: str,
sink: _EventSink,
message: str,
attachments: list[dict[str, Any]] | None = None,
) -> None:
done_before = sum(1 for event, _payload in sink.events if event == "session.event.done")
event_count_before = len(sink.events)
result = await get_dispatcher().dispatch(
"test",
"sessions.send",
{"key": key, "message": message, "attachments": attachments or []},
ctx,
)
assert result.ok, result.error
task = get_agent_task_registry().get(key)
loop = asyncio.get_running_loop()
deadline = loop.time() + _TURN_TERMINAL_EVENT_TIMEOUT_SECONDS
while loop.time() < deadline:
done_count = sum(
1 for event, _payload in sink.events if event == "session.event.done"
)
if done_count > done_before:
if task is not None:
try:
await asyncio.wait_for(
asyncio.shield(task),
timeout=_TURN_TASK_DRAIN_TIMEOUT_SECONDS,
)
except TimeoutError as exc:
raise AssertionError(
"timed out waiting for agent task to finish after done event; "
f"events={sink.events!r}"
) from exc
return
new_errors = [
payload
for event, payload in sink.events[event_count_before:]
if event == "session.event.error"
]
if new_errors:
raise AssertionError(f"turn emitted error events: {sink.events!r}")
if task is not None and task.done():
if task.cancelled():
raise AssertionError(f"agent task was cancelled; events={sink.events!r}")
exc = task.exception()
if exc is not None:
raise AssertionError(f"agent task failed; events={sink.events!r}") from exc
raise AssertionError(f"agent task ended without done event; events={sink.events!r}")
await asyncio.sleep(0.01)
raise AssertionError(f"timed out waiting for done event; events={sink.events!r}")
def _message_has_image(message: Message) -> bool:
return isinstance(message.content, list) and any(
isinstance(block, ContentBlockImage) for block in message.content
)
def _message_image_blocks(message: Message) -> list[ContentBlockImage]:
if not isinstance(message.content, list):
return []
return [
block for block in message.content if isinstance(block, ContentBlockImage)
]
def _event_payloads(sink: _EventSink, event_name: str) -> list[dict[str, Any]]:
return [payload for event, payload in sink.events if event == event_name]
def _file_uuid_attachment(file_uuid: str) -> dict[str, str]:
return {"file_uuid": file_uuid, "mime": "image/png", "name": "first.png"}
@pytest.fixture
async def _e2e_stack(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("OPENSQUILLA_OPENROUTER_LIVE_PRICING", "0")
config = _configure_gateway(tmp_path)
store = UploadStore(marker_dir=tmp_path / "upload-markers")
set_upload_store(store)
storage = SessionStorage(str(tmp_path / "sessions.sqlite"))
await storage.connect()
manager = SessionManager(
storage,
inject_time_prefix=False,
media_root=config.attachments.media_root,
)
text_provider = _RecordingProvider("text ok")
gate_provider = _RecordingProvider(
'{"decision":"needs_image","confidence":0.94,"reason":"visual detail"}'
)
vision_provider = _RecordingProvider("vision ok")
selector = _RecordingSelector(
{
_TEXT_MODEL: text_provider,
_GATE_MODEL: gate_provider,
_VISION_MODEL: vision_provider,
}
)
runner = TurnRunner(
provider_selector=selector,
session_manager=manager,
config=config,
model_catalog=_FakeModelCatalog(),
)
bootstrap_configs: list[AgentConfig] = []
original_bootstrap_run = runner._agent_bootstrap_stage.run
async def _record_bootstrap_config(inp: Any) -> Any:
outcome = await original_bootstrap_run(inp)
if not outcome.terminate and outcome.output is not None:
bootstrap_configs.append(outcome.output.agent_config)
return outcome
runner._agent_bootstrap_stage.run = _record_bootstrap_config # type: ignore[method-assign]
subscription_manager = SubscriptionManager()
sink = _EventSink(f"attachment-history-e2e-{uuid.uuid4().hex}")
get_registry().register(sink) # type: ignore[arg-type]
ctx = RpcContext(
conn_id=sink.conn_id,
principal=Principal(
role="operator",
scopes=frozenset(["operator.admin"]),
is_owner=True,
authenticated=True,
),
session_manager=manager,
config=config,
provider_selector=selector,
subscription_manager=subscription_manager,
turn_runner=runner,
)
app = create_gateway_app(
config,
session_manager=manager,
provider_selector=selector,
subscription_manager=subscription_manager,
turn_runner=runner,
)
try:
yield {
"app": app,
"bootstrap_configs": bootstrap_configs,
"config": config,
"ctx": ctx,
"gate_provider": gate_provider,
"manager": manager,
"runner": runner,
"sink": sink,
"storage": storage,
"store": store,
"subscription_manager": subscription_manager,
"text_provider": text_provider,
"vision_provider": vision_provider,
}
finally:
get_registry().unregister(sink.conn_id)
set_upload_store(None)
await storage.close()
@pytest.mark.asyncio
async def test_gateway_upload_history_image_replays_through_squilla_router_gate_history(
_e2e_stack: dict[str, Any],
) -> None:
manager: SessionManager = _e2e_stack["manager"]
subscription_manager: SubscriptionManager = _e2e_stack["subscription_manager"]
sink: _EventSink = _e2e_stack["sink"]
store: UploadStore = _e2e_stack["store"]
vision_provider: _RecordingProvider = _e2e_stack["vision_provider"]
gate_provider: _RecordingProvider = _e2e_stack["gate_provider"]
config: GatewayConfig = _e2e_stack["config"]
bootstrap_configs: list[AgentConfig] = _e2e_stack["bootstrap_configs"]
key = "agent:main:attachment-history-e2e"
session = await manager.create(
session_key=key,
agent_id="main",
display_name="attachment history e2e",
)
subscription_manager.subscribe_messages(sink.conn_id, key)
file_uuid = await _upload_png(_e2e_stack["app"])
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="Describe this image.",
attachments=[_file_uuid_attachment(file_uuid)],
)
with pytest.raises(AttachmentNotFoundError):
await store.get(file_uuid)
transcript = await manager.get_transcript(key)
first_user = transcript[0]
persisted = json.loads(first_user.content)
attachment = persisted["attachments"][0]
assert "file_uuid" not in json.dumps(persisted)
assert attachment["mime"] == "image/png"
assert attachment["name"] == "first.png"
sha = attachment["sha256_ref"]
assert isinstance(sha, str) and len(sha) == 64
material_path = transcript_material_path(
Path(config.attachments.media_root or ""),
session.session_id,
sha,
)
assert material_path.is_file()
assert material_path.read_bytes() == _PNG_BYTES
await manager.append_message(key, "user", "A text-only turn in between.")
await manager.append_message(key, "assistant", "Text answer in between.")
vision_calls_before = len(vision_provider.calls)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="What color is the small corner?",
)
assert len(gate_provider.calls) == 1
assert len(vision_provider.calls) == vision_calls_before + 1
final_call = vision_provider.calls[-1]
sent_messages = final_call["messages"]
image_blocks = [
block
for message in sent_messages[:-1]
for block in _message_image_blocks(message)
]
assert image_blocks
assert base64.b64decode(image_blocks[0].data, validate=True) == _PNG_BYTES
assert isinstance(sent_messages[-1].content, str)
assert sent_messages[-1].content.startswith("What color is the small corner?")
router_events = _event_payloads(sink, "session.event.router_decision")
assert router_events[-1]["source"] == "image_route"
assert router_events[-1]["model"] == _VISION_MODEL
done_events = _event_payloads(sink, "session.event.done")
assert done_events[-1]["image_route_reason"] == "gate_history"
assert done_events[-1]["vision_followup_needs_image"] is True
assert done_events[-1]["vision_followup_gate_decision"] == "needs_image"
assert bootstrap_configs[-1].preserve_historical_images is True
assert (
bootstrap_configs[-1].max_history_turns
== config.squilla_router.vision_history_lookback_turns
)
@pytest.mark.asyncio
async def test_historical_image_material_is_not_replayed_without_vision_support(
_e2e_stack: dict[str, Any],
) -> None:
manager: SessionManager = _e2e_stack["manager"]
runner: TurnRunner = _e2e_stack["runner"]
subscription_manager: SubscriptionManager = _e2e_stack["subscription_manager"]
sink: _EventSink = _e2e_stack["sink"]
key = "agent:main:attachment-history-no-vision"
await manager.create(session_key=key, agent_id="main")
subscription_manager.subscribe_messages(sink.conn_id, key)
file_uuid = await _upload_png(_e2e_stack["app"])
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="Describe this image.",
attachments=[_file_uuid_attachment(file_uuid)],
)
provider = _RecordingProvider()
agent = Agent(
provider=provider,
config=AgentConfig(
model_capabilities=ModelCapabilities(supports_vision=False),
preserve_historical_images=True,
),
)
await runner._load_history(agent, key)
events = [event async for event in agent.run_turn("Follow up.")]
assert any(getattr(event, "kind", None) == "done" for event in events)
assert not any(_message_has_image(message) for message in provider.calls[0]["messages"])
@pytest.mark.asyncio
async def test_historical_image_material_outside_lookback_is_not_replayed(
_e2e_stack: dict[str, Any],
) -> None:
manager: SessionManager = _e2e_stack["manager"]
runner: TurnRunner = _e2e_stack["runner"]
config: GatewayConfig = _e2e_stack["config"]
subscription_manager: SubscriptionManager = _e2e_stack["subscription_manager"]
sink: _EventSink = _e2e_stack["sink"]
key = "agent:main:attachment-history-lookback"
await manager.create(session_key=key, agent_id="main")
subscription_manager.subscribe_messages(sink.conn_id, key)
file_uuid = await _upload_png(_e2e_stack["app"])
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="Describe this image.",
attachments=[_file_uuid_attachment(file_uuid)],
)
await manager.append_message(key, "user", "A later text-only user turn.")
await manager.append_message(key, "assistant", "A later text-only answer.")
config.squilla_router.vision_history_lookback_turns = 1
provider = _RecordingProvider()
agent = Agent(
provider=provider,
config=AgentConfig(
model_capabilities=ModelCapabilities(supports_vision=True),
preserve_historical_images=True,
),
)
await runner._load_history(agent, key, trim_last_user=False)
events = [event async for event in agent.run_turn("Follow up.")]
assert any(getattr(event, "kind", None) == "done" for event in events)
assert not any(_message_has_image(message) for message in provider.calls[0]["messages"])
@pytest.mark.asyncio
async def test_gate_text_only_followup_stays_text_and_does_not_replay_history_image(
_e2e_stack: dict[str, Any],
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(squilla_router_step, "_get_strategy", lambda _cfg: _TextTierStrategy())
manager: SessionManager = _e2e_stack["manager"]
subscription_manager: SubscriptionManager = _e2e_stack["subscription_manager"]
sink: _EventSink = _e2e_stack["sink"]
gate_provider: _RecordingProvider = _e2e_stack["gate_provider"]
text_provider: _RecordingProvider = _e2e_stack["text_provider"]
vision_provider: _RecordingProvider = _e2e_stack["vision_provider"]
key = "agent:main:attachment-history-text-only"
await manager.create(session_key=key, agent_id="main")
subscription_manager.subscribe_messages(sink.conn_id, key)
file_uuid = await _upload_png(_e2e_stack["app"])
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="Describe this image.",
attachments=[_file_uuid_attachment(file_uuid)],
)
await manager.append_message(key, "user", "A text-only turn in between.")
await manager.append_message(key, "assistant", "Text answer in between.")
gate_provider.text = (
'{"decision":"text_only","confidence":0.91,"reason":"new coding task"}'
)
gate_calls_before = len(gate_provider.calls)
text_calls_before = len(text_provider.calls)
vision_calls_before = len(vision_provider.calls)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="Write a small Python script.",
)
assert len(gate_provider.calls) == gate_calls_before + 1
assert len(text_provider.calls) == text_calls_before + 1
assert len(vision_provider.calls) == vision_calls_before
sent_messages = text_provider.calls[-1]["messages"]
assert not any(_message_has_image(message) for message in sent_messages)
done_events = _event_payloads(sink, "session.event.done")
assert done_events[-1]["vision_followup_gate_decision"] == "text_only"
assert done_events[-1]["vision_followup_needs_image"] is False
assert done_events[-1].get("image_route_reason") is None
@@ -0,0 +1,372 @@
"""Opt-in gateway/WebSocket compaction regression tests."""
from __future__ import annotations
import asyncio
import os
import socket
import sqlite3
import subprocess
import sys
import textwrap
import time
from pathlib import Path
from typing import Any
import httpx
import pytest
from opensquilla.cli.gateway_client import GatewayClient
def _free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("127.0.0.1", 0))
return int(sock.getsockname()[1])
def _wait_for_health(port: int, server: subprocess.Popen[str]) -> None:
url = f"http://127.0.0.1:{port}/health"
deadline = time.monotonic() + 20.0
last_error = ""
while time.monotonic() < deadline:
if server.poll() is not None:
stdout = server.stdout.read() if server.stdout else ""
stderr = server.stderr.read() if server.stderr else ""
raise AssertionError(
f"gateway exited early code={server.returncode}\nstdout={stdout}\nstderr={stderr}"
)
try:
response = httpx.get(url, timeout=1.0)
if response.status_code == 200 and response.json().get("ok") is True:
return
except Exception as exc: # noqa: BLE001 - surfaced on timeout.
last_error = str(exc)
time.sleep(0.1)
raise AssertionError(f"gateway did not become healthy: {last_error}")
def _stop_process(server: subprocess.Popen[str]) -> None:
if server.poll() is not None:
return
server.terminate()
try:
server.wait(timeout=10)
except subprocess.TimeoutExpired:
server.kill()
server.wait(timeout=10)
def _kill_process(server: subprocess.Popen[str]) -> None:
if server.poll() is not None:
return
server.kill()
server.wait(timeout=10)
def _write_slow_compaction_server(
path: Path,
*,
port: int,
db_path: Path,
session_key: str,
) -> None:
path.write_text(
textwrap.dedent(
f"""
import asyncio
import os
from opensquilla.gateway.boot import start_gateway_server
from opensquilla.gateway.config import AuthConfig, GatewayConfig
from opensquilla.gateway.websocket import SubscriptionManager
from opensquilla.session.manager import SessionManager
from opensquilla.session.storage import SessionStorage
SESSION_KEY = {session_key!r}
class SlowCompactionSessionManager(SessionManager):
async def compact_with_result(self, *args, **kwargs):
await asyncio.sleep(1.2)
return await super().compact_with_result(*args, **kwargs)
async def seed(manager):
existing = await manager.get_session(SESSION_KEY)
if existing is not None:
return
node = await manager.create(
session_key=SESSION_KEY,
agent_id="main",
display_name="slow compaction e2e",
)
for idx in range(11):
await manager.append_message(
node.session_key,
role="user" if idx % 2 == 0 else "assistant",
content=("seed transcript block %02d " % idx) + ("alpha beta gamma " * 80),
token_count=320,
)
async def main():
os.makedirs(os.environ["OPENSQUILLA_STATE_DIR"], exist_ok=True)
storage = SessionStorage({str(db_path)!r})
await storage.connect()
manager = SlowCompactionSessionManager(storage, inject_time_prefix=False)
await seed(manager)
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
config.state_dir = os.environ["OPENSQUILLA_STATE_DIR"]
await start_gateway_server(
config=config,
session_manager=manager,
subscription_manager=SubscriptionManager(),
run=True,
)
await asyncio.Event().wait()
asyncio.run(main())
"""
),
encoding="utf-8",
)
async def _manual_compact_over_websocket(port: int, session_key: str) -> dict[str, Any]:
client = GatewayClient()
await client.connect(f"ws://127.0.0.1:{port}/ws")
try:
await client.call("sessions.subscribe")
await client.call("sessions.messages.subscribe", {"key": session_key})
compact_task = asyncio.create_task(
client.call(
"sessions.contextCompact",
{"key": session_key, "contextWindowTokens": 1000},
)
)
statuses: list[str] = []
all_statuses: list[str] = []
while True:
if compact_task.done() and "completed" in all_statuses:
break
frame = await asyncio.wait_for(client._recv_queue.get(), timeout=45.0) # noqa: SLF001
if frame.get("event") != "session.event.compaction":
continue
payload = frame.get("payload") or {}
status = str(payload.get("status") or "")
all_statuses.append(status)
if status:
statuses.append(status)
if status == "started":
assert compact_task.done() is False
if status in {"completed", "failed", "skipped", "cancelled"}:
break
result = await asyncio.wait_for(compact_task, timeout=15.0)
return {
"statuses": statuses,
"all_statuses": all_statuses,
"result": result,
}
finally:
await client.close()
async def _start_manual_compact_and_wait_for_started(
port: int, session_key: str
) -> tuple[GatewayClient, asyncio.Task[Any], dict[str, Any]]:
client = GatewayClient()
await client.connect(f"ws://127.0.0.1:{port}/ws")
await client.call("sessions.subscribe")
await client.call("sessions.messages.subscribe", {"key": session_key})
compact_task = asyncio.create_task(
client.call(
"sessions.contextCompact",
{"key": session_key, "contextWindowTokens": 1000},
)
)
while True:
frame = await asyncio.wait_for(client._recv_queue.get(), timeout=15.0) # noqa: SLF001
if frame.get("event") != "session.event.compaction":
continue
payload = frame.get("payload") or {}
if payload.get("status") == "started":
assert compact_task.done() is False
return client, compact_task, payload
def _read_compaction_db_state(db_path: Path) -> dict[str, int]:
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
session = conn.execute(
"select session_id, compaction_count from sessions limit 1"
).fetchone()
assert session is not None
session_id = session["session_id"]
summary_count = conn.execute(
"select count(*) from session_summaries where session_id = ?",
(session_id,),
).fetchone()[0]
entry_count = conn.execute(
"select count(*) from transcript_entries where session_id = ?",
(session_id,),
).fetchone()[0]
running_tasks = conn.execute(
"select count(*) from agent_tasks where status in ('queued', 'running')"
).fetchone()[0]
return {
"compaction_count": int(session["compaction_count"] or 0),
"summary_count": int(summary_count),
"entry_count": int(entry_count),
"running_tasks": int(running_tasks),
}
def _start_slow_compaction_gateway(
tmp_path: Path,
*,
port: int,
db_path: Path,
session_key: str,
env: dict[str, str],
) -> subprocess.Popen[str]:
server_script = tmp_path / "slow_compaction_gateway.py"
_write_slow_compaction_server(
server_script,
port=port,
db_path=db_path,
session_key=session_key,
)
return subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
@pytest.mark.asyncio
async def test_gateway_websocket_slow_manual_compaction_rewrites_db(
tmp_path: Path,
) -> None:
if os.environ.get("OPENSQUILLA_GATEWAY_COMPACTION_E2E") != "1":
pytest.skip("set OPENSQUILLA_GATEWAY_COMPACTION_E2E=1 to run compaction e2e")
port = _free_port()
session_key = "agent:main:webchat:slowcompact"
state_dir = tmp_path / "state"
log_dir = tmp_path / "logs"
db_path = state_dir / "sessions.db"
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(state_dir)
env["OPENSQUILLA_LOG_DIR"] = str(log_dir)
server = _start_slow_compaction_gateway(
tmp_path,
port=port,
db_path=db_path,
session_key=session_key,
env=env,
)
try:
_wait_for_health(port, server)
observed = await asyncio.wait_for(
_manual_compact_over_websocket(port, session_key),
timeout=90.0,
)
finally:
_stop_process(server)
result = observed["result"]
db_state = _read_compaction_db_state(db_path)
assert observed["statuses"] == ["started", "observed", "observed", "completed"]
assert result["compacted"] is True
assert result["tokens_before"] == 3520
assert result["tokens_after"] < result["tokens_before"]
assert db_state["compaction_count"] == 1
assert db_state["summary_count"] == 1
assert db_state["entry_count"] == result["kept_count"]
assert db_state["running_tasks"] == 0
@pytest.mark.asyncio
async def test_gateway_restart_during_slow_manual_compaction_leaves_db_recoverable(
tmp_path: Path,
) -> None:
if os.environ.get("OPENSQUILLA_GATEWAY_COMPACTION_E2E") != "1":
pytest.skip("set OPENSQUILLA_GATEWAY_COMPACTION_E2E=1 to run compaction e2e")
port = _free_port()
session_key = "agent:main:webchat:slowcompact"
state_dir = tmp_path / "state"
log_dir = tmp_path / "logs"
db_path = state_dir / "sessions.db"
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(state_dir)
env["OPENSQUILLA_LOG_DIR"] = str(log_dir)
server = _start_slow_compaction_gateway(
tmp_path,
port=port,
db_path=db_path,
session_key=session_key,
env=env,
)
client: GatewayClient | None = None
compact_task: asyncio.Task[Any] | None = None
try:
_wait_for_health(port, server)
client, compact_task, _payload = await _start_manual_compact_and_wait_for_started(
port,
session_key,
)
_kill_process(server)
with pytest.raises(Exception):
await asyncio.wait_for(compact_task, timeout=5.0)
finally:
if client is not None:
await client.close()
_stop_process(server)
interrupted_state = _read_compaction_db_state(db_path)
assert interrupted_state == {
"compaction_count": 0,
"summary_count": 0,
"entry_count": 11,
"running_tasks": 0,
}
restarted = _start_slow_compaction_gateway(
tmp_path,
port=port,
db_path=db_path,
session_key=session_key,
env=env,
)
try:
_wait_for_health(port, restarted)
observed = await asyncio.wait_for(
_manual_compact_over_websocket(port, session_key),
timeout=90.0,
)
finally:
_stop_process(restarted)
assert observed["statuses"] == ["started", "observed", "observed", "completed"]
assert observed["result"]["compacted"] is True
recovered_state = _read_compaction_db_state(db_path)
assert recovered_state["compaction_count"] == 1
assert recovered_state["summary_count"] == 1
assert recovered_state["running_tasks"] == 0
+219
View File
@@ -0,0 +1,219 @@
"""Opt-in gateway-level live LLM e2e.
Runs a real gateway, creates a session over the public WebSocket client, sends
one prompt through the normal session path, and verifies the provider response.
It skips unless explicitly enabled and credentialed.
"""
from __future__ import annotations
import asyncio
import json
import os
import socket
import subprocess
import sys
import textwrap
import time
from pathlib import Path
import httpx
import pytest
from opensquilla.cli.gateway_client import GatewayClient
pytestmark = [pytest.mark.llm, pytest.mark.llm_smoke, pytest.mark.llm_gateway]
_EXPECTED_TOKEN = "opensquilla-gateway-live-ok"
def _free_port() -> int:
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
return int(sock.getsockname()[1])
def _wait_for_health(port: int, server: subprocess.Popen[str]) -> None:
url = f"http://127.0.0.1:{port}/health"
deadline = time.monotonic() + 45.0
last_error = ""
while time.monotonic() < deadline:
if server.poll() is not None:
stdout = server.stdout.read() if server.stdout else ""
stderr = server.stderr.read() if server.stderr else ""
raise AssertionError(
f"gateway exited early code={server.returncode}\nstdout={stdout}\nstderr={stderr}"
)
try:
response = httpx.get(url, timeout=1.0)
if response.status_code == 200 and response.json().get("ok") is True:
return
except Exception as exc: # noqa: BLE001 - surfaced on timeout.
last_error = str(exc)
time.sleep(0.2)
raise AssertionError(f"gateway did not become healthy: {last_error}")
def _stop_process(process: subprocess.Popen[str]) -> None:
process.terminate()
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=10)
def _toml_string(value: str | Path) -> str:
return json.dumps(str(value))
def _write_gateway_config(
path: Path,
*,
port: int,
state_dir: Path,
workspace_dir: Path,
) -> None:
path.write_text(
textwrap.dedent(
f"""
host = "127.0.0.1"
port = {port}
state_dir = {_toml_string(state_dir)}
workspace_dir = {_toml_string(workspace_dir)}
[auth]
mode = "none"
"""
).lstrip(),
encoding="utf-8",
)
def _write_gateway_server_script(path: Path) -> None:
path.write_text(
textwrap.dedent(
"""
import asyncio
import os
from opensquilla.gateway.boot import start_gateway_server
from opensquilla.gateway.config import GatewayConfig, LlmProviderConfig
from opensquilla.gateway.websocket import SubscriptionManager
config = GatewayConfig.load(os.environ["OPENSQUILLA_GATEWAY_CONFIG_PATH"])
config.llm = LlmProviderConfig(
provider="openrouter",
model=os.environ.get("LLM_TEST_MODEL", "deepseek/deepseek-v4-flash"),
api_key=os.environ["OPENROUTER_API_KEY"],
base_url=os.environ.get(
"OPENROUTER_BASE_URL",
"https://openrouter.ai/api/v1",
),
)
async def main():
await start_gateway_server(
config=config,
subscription_manager=SubscriptionManager(),
run=True,
)
await asyncio.Event().wait()
asyncio.run(main())
"""
),
encoding="utf-8",
)
async def _send_live_prompt(port: int) -> list[dict]:
client = GatewayClient()
await client.connect(f"ws://127.0.0.1:{port}/ws")
try:
session_key = await client.create_session(display_name="live-gateway-llm")
return [
event
async for event in client.send_message(
session_key,
f"Reply with exactly {_EXPECTED_TOKEN}.",
)
]
finally:
await client.close()
def _event_text(events: list[dict]) -> str:
chunks: list[str] = []
for event in events:
for key in ("text", "delta", "message", "content"):
value = event.get(key)
if isinstance(value, str):
chunks.append(value)
return "\n".join(chunks).lower()
def test_gateway_llm_e2e_uses_explicit_temp_gateway_config(tmp_path: Path) -> None:
port = 18891
config_path = tmp_path / "gateway.toml"
server_script = tmp_path / "gateway_llm_server.py"
state_dir = tmp_path / "state"
workspace_dir = tmp_path / "workspace"
_write_gateway_config(
config_path,
port=port,
state_dir=state_dir,
workspace_dir=workspace_dir,
)
_write_gateway_server_script(server_script)
config_source = config_path.read_text(encoding="utf-8")
script_source = server_script.read_text(encoding="utf-8")
assert f"state_dir = {_toml_string(state_dir)}" in config_source
assert f"workspace_dir = {_toml_string(workspace_dir)}" in config_source
assert 'GatewayConfig.load(os.environ["OPENSQUILLA_GATEWAY_CONFIG_PATH"])' in script_source
@pytest.mark.asyncio
async def test_gateway_session_send_reaches_live_llm(tmp_path: Path) -> None:
if os.environ.get("OPENSQUILLA_GATEWAY_LLM_E2E") != "1":
pytest.skip("set OPENSQUILLA_GATEWAY_LLM_E2E=1 to run gateway LLM e2e")
if not os.environ.get("OPENROUTER_API_KEY"):
pytest.skip("OPENROUTER_API_KEY not set")
port = _free_port()
config_path = tmp_path / "gateway.toml"
server_script = tmp_path / "gateway_llm_server.py"
state_dir = tmp_path / "state"
workspace_dir = tmp_path / "workspace"
_write_gateway_config(
config_path,
port=port,
state_dir=state_dir,
workspace_dir=workspace_dir,
)
_write_gateway_server_script(server_script)
env = os.environ.copy()
env["OPENSQUILLA_GATEWAY_CONFIG_PATH"] = str(config_path)
env["OPENSQUILLA_STATE_DIR"] = str(state_dir)
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
env["OPENSQUILLA_TURN_CALL_LOG"] = "0"
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
events = await asyncio.wait_for(_send_live_prompt(port), timeout=120)
finally:
_stop_process(server)
assert _EXPECTED_TOKEN in _event_text(events)
@@ -0,0 +1,897 @@
"""Gateway non-image attachment materialization e2e tests.
These tests exercise the real upload -> sessions.send -> transcript material ->
SquillaRouter -> runtime/provider path. The upstream provider is deterministic
and only captures final request messages; no live LLM credentials are used.
"""
from __future__ import annotations
import asyncio
import base64
import hashlib
import json
import uuid
from collections.abc import AsyncIterator
from pathlib import Path
from typing import Any
import httpx
import pytest
import opensquilla.engine.steps.squilla_router as squilla_router_step
from opensquilla.attachment_refs import transcript_material_path
from opensquilla.engine import AgentConfig
from opensquilla.engine.runtime import TurnRunner
from opensquilla.gateway import rpc_sessions as _rpc_sessions # noqa: F401
from opensquilla.gateway.agent_tasks import get_agent_task_registry
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.auth import Principal
from opensquilla.gateway.config import GatewayConfig
from opensquilla.gateway.rpc import RpcContext, get_dispatcher
from opensquilla.gateway.uploads import UploadStore, set_upload_store
from opensquilla.gateway.websocket import SubscriptionManager, get_registry
from opensquilla.provider import ChatConfig, DoneEvent, Message, ModelCapabilities
from opensquilla.provider.types import (
ContentBlockImage,
ContentBlockText,
ModelInfo,
TextDeltaEvent,
)
from opensquilla.session.manager import SessionManager
from opensquilla.session.storage import SessionStorage
_TEXT_MODEL = "test/text"
_GATE_MODEL = "test/gate"
_VISION_MODEL = "test/vision"
_TURN_TERMINAL_EVENT_TIMEOUT_SECONDS = 30.0
_TURN_TASK_DRAIN_TIMEOUT_SECONDS = 10.0
_PNG_BYTES = (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01"
b"\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4"
b"\x89\x00\x00\x00\nIDATx\x9cc\xf8\x0f\x00\x01\x01"
b"\x01\x00\x18\xdd\x8d\xb0\x00\x00\x00\x00IEND\xaeB`\x82"
)
def _sample_pdf_bytes(text: str = "Machine Learning") -> bytes:
stream = f"BT /F1 24 Tf 72 720 Td ({text}) Tj ET".encode("ascii")
objects = [
b"<< /Type /Catalog /Pages 2 0 R >>",
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
(
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] "
b"/Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>"
),
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
b"<< /Length " + str(len(stream)).encode("ascii") + b" >>\nstream\n"
+ stream + b"\nendstream",
]
body = bytearray(b"%PDF-1.4\n")
offsets = [0]
for idx, obj in enumerate(objects, start=1):
offsets.append(len(body))
body.extend(f"{idx} 0 obj\n".encode("ascii"))
body.extend(obj)
body.extend(b"\nendobj\n")
xref_offset = len(body)
body.extend(f"xref\n0 {len(objects) + 1}\n".encode("ascii"))
body.extend(b"0000000000 65535 f \n")
for offset in offsets[1:]:
body.extend(f"{offset:010d} 00000 n \n".encode("ascii"))
body.extend(
f"trailer\n<< /Root 1 0 R /Size {len(objects) + 1} >>\n"
f"startxref\n{xref_offset}\n%%EOF\n".encode("ascii")
)
return bytes(body)
def _b64(payload: bytes) -> str:
return base64.b64encode(payload).decode("ascii")
class _RecordingProvider:
provider_name = "fake"
def __init__(self, text: str = "ok") -> None:
self.text = text
self.calls: list[dict[str, Any]] = []
async def chat(
self,
messages: list[Message],
tools: list[Any] | None = None,
config: ChatConfig | None = None,
) -> AsyncIterator[Any]:
self.calls.append({"messages": messages, "tools": tools, "config": config})
yield TextDeltaEvent(text=self.text)
yield DoneEvent(stop_reason="end_turn", input_tokens=3, output_tokens=1)
async def list_models(self) -> list[ModelInfo]:
return []
class _RecordingSelector:
active_provider_id = "openrouter"
def __init__(
self,
providers: dict[str, _RecordingProvider],
model: str = _TEXT_MODEL,
) -> None:
self.providers = providers
self.model = model
def clone(self) -> _RecordingSelector:
return _RecordingSelector(self.providers, self.model)
def override_model(self, model: str) -> None:
self.model = model
def override_model_with_fallback_chain(
self,
model: str,
fallback_chain: list[object], # noqa: ARG002
) -> None:
self.override_model(model)
def resolve(self) -> _RecordingProvider:
return self.providers.get(self.model, self.providers[_TEXT_MODEL])
async def list_models(self) -> list[dict[str, Any]]:
return []
class _FakeModelCatalog:
def resolve_max_tokens(
self,
model_id: str, # noqa: ARG002
*,
user_override: int = 0,
provider: str = "openrouter", # noqa: ARG002
) -> int:
return user_override if user_override > 0 else 1024
def resolve_context_window(
self,
model_id: str, # noqa: ARG002
*,
provider: str = "openrouter", # noqa: ARG002
) -> int:
return 8192
def get_capabilities(
self,
model_id: str,
provider_name: str = "openrouter", # noqa: ARG002
base_url: str = "", # noqa: ARG002
) -> ModelCapabilities:
return ModelCapabilities(supports_vision=model_id == _VISION_MODEL)
class _EventSink:
authenticated = True
def __init__(self, conn_id: str) -> None:
self.conn_id = conn_id
self.events: list[tuple[str, dict[str, Any]]] = []
async def send_event(
self,
event: str,
payload: Any = None,
meta: dict[str, Any] | None = None, # noqa: ARG002
) -> None:
self.events.append((event, dict(payload or {})))
class _TextTierStrategy:
async def classify(
self,
message: str, # noqa: ARG002
valid_tiers: list[str],
routing_history: list[dict] | None = None, # noqa: ARG002
**kwargs: object, # noqa: ARG002
) -> tuple[str, float, str, dict[str, Any]]:
tier = "c1" if "c1" in valid_tiers else valid_tiers[0]
return (
tier,
0.87,
"test_text_route",
{
"route_class": "R1",
"thinking_mode": "T1",
"prompt_policy": "P0",
},
)
def _configure_gateway(tmp_path: Path) -> GatewayConfig:
config = GatewayConfig()
config.state_dir = str(tmp_path / "state")
config.workspace_dir = str(tmp_path / "workspace")
config.attachments.media_root = str(tmp_path / "media")
config.squilla_router.enabled = True
config.squilla_router.rollout_phase = "full"
config.squilla_router.require_router_runtime = False
config.squilla_router.vision_history_lookback_turns = 8
config.squilla_router.vision_history_candidate_turns = 8
config.squilla_router.vision_sticky_followup_turns = 3
config.squilla_router.vision_followup_gate_tier = "c0"
config.squilla_router.tiers = {
"c0": {
"provider": "openrouter",
"model": _GATE_MODEL,
"supports_image": False,
},
"c1": {
"provider": "openrouter",
"model": _TEXT_MODEL,
"supports_image": False,
},
"image_model": {
"provider": "openrouter",
"model": _VISION_MODEL,
"supports_image": True,
"image_only": True,
},
}
config.squilla_router.default_tier = "c1"
config.llm.provider = "openrouter"
config.llm.model = _TEXT_MODEL
return config
async def _upload_file(
app: Any,
*,
name: str,
mime: str,
payload: bytes,
) -> str:
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(
transport=transport,
base_url="http://testserver",
) as client:
response = await client.post(
"/api/v1/files/upload",
files={"file": (name, payload, mime)},
)
assert response.status_code == 200, response.text
file_uuid = response.json().get("file_uuid")
assert isinstance(file_uuid, str) and file_uuid.startswith("u-")
return file_uuid
async def _send_session_turn(
*,
ctx: RpcContext,
key: str,
sink: _EventSink,
message: str,
attachments: list[dict[str, Any]] | None = None,
) -> None:
done_before = sum(1 for event, _payload in sink.events if event == "session.event.done")
event_count_before = len(sink.events)
result = await get_dispatcher().dispatch(
"test",
"sessions.send",
{"key": key, "message": message, "attachments": attachments or []},
ctx,
)
assert result.ok, result.error
task = get_agent_task_registry().get(key)
loop = asyncio.get_running_loop()
deadline = loop.time() + _TURN_TERMINAL_EVENT_TIMEOUT_SECONDS
while loop.time() < deadline:
done_count = sum(
1 for event, _payload in sink.events if event == "session.event.done"
)
if done_count > done_before:
if task is not None:
try:
await asyncio.wait_for(
asyncio.shield(task),
timeout=_TURN_TASK_DRAIN_TIMEOUT_SECONDS,
)
except TimeoutError as exc:
raise AssertionError(
"timed out waiting for agent task to finish after done event; "
f"events={sink.events!r}"
) from exc
return
new_errors = [
payload
for event, payload in sink.events[event_count_before:]
if event == "session.event.error"
]
if new_errors:
raise AssertionError(f"turn emitted error events: {sink.events!r}")
if task is not None and task.done():
if task.cancelled():
raise AssertionError(f"agent task was cancelled; events={sink.events!r}")
exc = task.exception()
if exc is not None:
raise AssertionError(f"agent task failed; events={sink.events!r}") from exc
raise AssertionError(f"agent task ended without done event; events={sink.events!r}")
await asyncio.sleep(0.01)
raise AssertionError(f"timed out waiting for done event; events={sink.events!r}")
def _event_payloads(sink: _EventSink, event_name: str) -> list[dict[str, Any]]:
return [payload for event, payload in sink.events if event == event_name]
def _attachment(file_uuid: str, *, mime: str, name: str) -> dict[str, str]:
return {"file_uuid": file_uuid, "mime": mime, "name": name}
def _inline_attachment(payload: bytes, *, mime: str, name: str) -> dict[str, str]:
return {"type": mime, "mime": mime, "name": name, "data": _b64(payload)}
def _message_text(message: Message) -> str:
if isinstance(message.content, str):
return message.content
if isinstance(message.content, list):
parts: list[str] = []
for block in message.content:
if isinstance(block, ContentBlockText):
parts.append(block.text)
return "\n".join(parts)
return str(message.content)
def _all_provider_text(messages: list[Message]) -> str:
return "\n".join(_message_text(message) for message in messages)
def _message_has_image(message: Message) -> bool:
return isinstance(message.content, list) and any(
isinstance(block, ContentBlockImage) for block in message.content
)
@pytest.fixture
async def _e2e_stack(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("OPENSQUILLA_OPENROUTER_LIVE_PRICING", "0")
monkeypatch.setattr(squilla_router_step, "_get_strategy", lambda _cfg: _TextTierStrategy())
config = _configure_gateway(tmp_path)
store = UploadStore(marker_dir=tmp_path / "upload-markers")
set_upload_store(store)
storage = SessionStorage(str(tmp_path / "sessions.sqlite"))
await storage.connect()
manager = SessionManager(
storage,
inject_time_prefix=False,
media_root=config.attachments.media_root,
)
text_provider = _RecordingProvider("text ok")
gate_provider = _RecordingProvider(
'{"decision":"needs_image","confidence":0.94,"reason":"visual detail"}'
)
vision_provider = _RecordingProvider("vision ok")
selector = _RecordingSelector(
{
_TEXT_MODEL: text_provider,
_GATE_MODEL: gate_provider,
_VISION_MODEL: vision_provider,
}
)
runner = TurnRunner(
provider_selector=selector,
session_manager=manager,
config=config,
model_catalog=_FakeModelCatalog(),
)
bootstrap_configs: list[AgentConfig] = []
original_bootstrap_run = runner._agent_bootstrap_stage.run
async def _record_bootstrap_config(inp: Any) -> Any:
outcome = await original_bootstrap_run(inp)
if not outcome.terminate and outcome.output is not None:
bootstrap_configs.append(outcome.output.agent_config)
return outcome
runner._agent_bootstrap_stage.run = _record_bootstrap_config # type: ignore[method-assign]
subscription_manager = SubscriptionManager()
sink = _EventSink(f"non-image-attachments-e2e-{uuid.uuid4().hex}")
get_registry().register(sink) # type: ignore[arg-type]
ctx = RpcContext(
conn_id=sink.conn_id,
principal=Principal(
role="operator",
scopes=frozenset(["operator.admin"]),
is_owner=True,
authenticated=True,
),
session_manager=manager,
config=config,
provider_selector=selector,
subscription_manager=subscription_manager,
turn_runner=runner,
)
app = create_gateway_app(
config,
session_manager=manager,
provider_selector=selector,
subscription_manager=subscription_manager,
turn_runner=runner,
)
try:
yield {
"app": app,
"bootstrap_configs": bootstrap_configs,
"config": config,
"ctx": ctx,
"gate_provider": gate_provider,
"manager": manager,
"runner": runner,
"sink": sink,
"storage": storage,
"store": store,
"subscription_manager": subscription_manager,
"text_provider": text_provider,
"vision_provider": vision_provider,
}
finally:
get_registry().unregister(sink.conn_id)
set_upload_store(None)
await storage.close()
@pytest.mark.asyncio
async def test_current_turn_pdf_is_materialized_to_workspace_path(
_e2e_stack: dict[str, Any],
) -> None:
manager: SessionManager = _e2e_stack["manager"]
subscription_manager: SubscriptionManager = _e2e_stack["subscription_manager"]
sink: _EventSink = _e2e_stack["sink"]
text_provider: _RecordingProvider = _e2e_stack["text_provider"]
config: GatewayConfig = _e2e_stack["config"]
key = "agent:main:pdf-materialization-current"
session = await manager.create(session_key=key, agent_id="main")
subscription_manager.subscribe_messages(sink.conn_id, key)
pdf_bytes = _sample_pdf_bytes()
file_uuid = await _upload_file(
_e2e_stack["app"],
name="L11 RL.pdf",
mime="application/pdf",
payload=pdf_bytes,
)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="请读取这个 PDF",
attachments=[_attachment(file_uuid, mime="application/pdf", name="L11 RL.pdf")],
)
transcript = await manager.get_transcript(key)
persisted = json.loads(transcript[0].content)
persisted_attachment = persisted["attachments"][0]
assert persisted_attachment["name"] == "L11 RL.pdf"
assert persisted_attachment["mime"] == "application/pdf"
sha = persisted_attachment["sha256_ref"]
assert "file_uuid" not in json.dumps(persisted)
material_path = transcript_material_path(
Path(config.attachments.media_root or ""),
session.session_id,
sha,
)
assert material_path.read_bytes() == pdf_bytes
workspace_paths = list(
(Path(config.workspace_dir or "") / ".opensquilla" / "attachments").glob("**/*.pdf")
)
assert len(workspace_paths) == 1
assert workspace_paths[0].read_bytes() == pdf_bytes
sent_text = _all_provider_text(text_provider.calls[-1]["messages"])
assert "Machine Learning" in sent_text
assert "attachment available: L11 RL.pdf (application/pdf" in sent_text
assert ".opensquilla/attachments/" in sent_text
assert workspace_paths[0].name in sent_text
@pytest.mark.asyncio
async def test_current_turn_inline_pdf_is_materialized_to_workspace_path(
_e2e_stack: dict[str, Any],
) -> None:
manager: SessionManager = _e2e_stack["manager"]
subscription_manager: SubscriptionManager = _e2e_stack["subscription_manager"]
sink: _EventSink = _e2e_stack["sink"]
text_provider: _RecordingProvider = _e2e_stack["text_provider"]
config: GatewayConfig = _e2e_stack["config"]
key = "agent:main:pdf-materialization-current-inline"
await manager.create(session_key=key, agent_id="main")
subscription_manager.subscribe_messages(sink.conn_id, key)
pdf_bytes = _sample_pdf_bytes()
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="请读取这个 PDF",
attachments=[
_inline_attachment(
pdf_bytes,
mime="application/pdf",
name="L11 RL.pdf",
)
],
)
transcript = await manager.get_transcript(key)
persisted = json.loads(transcript[0].content)
persisted_attachment = persisted["attachments"][0]
assert persisted_attachment["name"] == "L11 RL.pdf"
assert persisted_attachment["type"] == "application/pdf"
assert persisted_attachment["data"] == _b64(pdf_bytes)
assert "sha256_ref" not in persisted_attachment
workspace_paths = list(
(Path(config.workspace_dir or "") / ".opensquilla" / "attachments").glob("**/*.pdf")
)
assert len(workspace_paths) == 1
assert workspace_paths[0].read_bytes() == pdf_bytes
sent_text = _all_provider_text(text_provider.calls[-1]["messages"])
assert "Machine Learning" in sent_text
assert "attachment available: L11 RL.pdf (application/pdf" in sent_text
assert ".opensquilla/attachments/" in sent_text
assert workspace_paths[0].name in sent_text
@pytest.mark.asyncio
async def test_historical_pdf_followup_materializes_path_from_sha256_ref(
_e2e_stack: dict[str, Any],
) -> None:
manager: SessionManager = _e2e_stack["manager"]
subscription_manager: SubscriptionManager = _e2e_stack["subscription_manager"]
sink: _EventSink = _e2e_stack["sink"]
text_provider: _RecordingProvider = _e2e_stack["text_provider"]
config: GatewayConfig = _e2e_stack["config"]
key = "agent:main:pdf-materialization-history"
await manager.create(session_key=key, agent_id="main")
subscription_manager.subscribe_messages(sink.conn_id, key)
pdf_bytes = _sample_pdf_bytes()
file_uuid = await _upload_file(
_e2e_stack["app"],
name="L11 RL.pdf",
mime="application/pdf",
payload=pdf_bytes,
)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="请读取这个 PDF",
attachments=[_attachment(file_uuid, mime="application/pdf", name="L11 RL.pdf")],
)
await manager.append_message(key, "user", "中间普通文本。")
await manager.append_message(key, "assistant", "普通回答。")
calls_before = len(text_provider.calls)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="把刚才那个 PDF 左下角 Machine Learning 遮住",
)
assert len(text_provider.calls) == calls_before + 1
sent_messages = text_provider.calls[-1]["messages"]
sent_text = _all_provider_text(sent_messages)
assert "historical attachment available: L11 RL.pdf (application/pdf" in sent_text
assert "historical attachment omitted: L11 RL.pdf" not in sent_text
assert ".opensquilla/attachments/" in sent_text
assert isinstance(sent_messages[-1].content, str)
assert sent_messages[-1].content.startswith("把刚才那个 PDF")
workspace_paths = list(
(Path(config.workspace_dir or "") / ".opensquilla" / "attachments").glob("**/*.pdf")
)
assert len(workspace_paths) == 1
assert hashlib.sha256(workspace_paths[0].read_bytes()).digest() == hashlib.sha256(
pdf_bytes
).digest()
@pytest.mark.asyncio
async def test_historical_inline_pdf_followup_materializes_path_from_inline_data(
_e2e_stack: dict[str, Any],
) -> None:
manager: SessionManager = _e2e_stack["manager"]
subscription_manager: SubscriptionManager = _e2e_stack["subscription_manager"]
sink: _EventSink = _e2e_stack["sink"]
text_provider: _RecordingProvider = _e2e_stack["text_provider"]
config: GatewayConfig = _e2e_stack["config"]
key = "agent:main:pdf-materialization-history-inline"
await manager.create(session_key=key, agent_id="main")
subscription_manager.subscribe_messages(sink.conn_id, key)
pdf_bytes = _sample_pdf_bytes()
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="请读取这个 PDF",
attachments=[
_inline_attachment(
pdf_bytes,
mime="application/pdf",
name="L11 RL.pdf",
)
],
)
await manager.append_message(key, "user", "中间普通文本。")
await manager.append_message(key, "assistant", "普通回答。")
calls_before = len(text_provider.calls)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="把刚才那个 PDF 左下角 Machine Learning 遮住",
)
assert len(text_provider.calls) == calls_before + 1
sent_messages = text_provider.calls[-1]["messages"]
sent_text = _all_provider_text(sent_messages)
assert "historical attachment available: L11 RL.pdf (application/pdf" in sent_text
assert "historical attachment omitted: L11 RL.pdf" not in sent_text
assert ".opensquilla/attachments/" in sent_text
assert isinstance(sent_messages[-1].content, str)
assert sent_messages[-1].content.startswith("把刚才那个 PDF")
workspace_paths = list(
(Path(config.workspace_dir or "") / ".opensquilla" / "attachments").glob("**/*.pdf")
)
assert len(workspace_paths) == 1
assert hashlib.sha256(workspace_paths[0].read_bytes()).digest() == hashlib.sha256(
pdf_bytes
).digest()
@pytest.mark.asyncio
async def test_pdf_materialization_does_not_require_image_tier(
_e2e_stack: dict[str, Any],
) -> None:
config: GatewayConfig = _e2e_stack["config"]
config.squilla_router.tiers.pop("image_model", None)
manager: SessionManager = _e2e_stack["manager"]
subscription_manager: SubscriptionManager = _e2e_stack["subscription_manager"]
sink: _EventSink = _e2e_stack["sink"]
text_provider: _RecordingProvider = _e2e_stack["text_provider"]
key = "agent:main:pdf-materialization-no-image-tier"
await manager.create(session_key=key, agent_id="main")
subscription_manager.subscribe_messages(sink.conn_id, key)
file_uuid = await _upload_file(
_e2e_stack["app"],
name="L11 RL.pdf",
mime="application/pdf",
payload=_sample_pdf_bytes(),
)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="请读取这个 PDF",
attachments=[_attachment(file_uuid, mime="application/pdf", name="L11 RL.pdf")],
)
sent_text = _all_provider_text(text_provider.calls[-1]["messages"])
assert "attachment available: L11 RL.pdf (application/pdf" in sent_text
done_events = _event_payloads(sink, "session.event.done")
assert done_events[-1].get("image_route_reason") is None
@pytest.mark.asyncio
async def test_mixed_historical_image_and_pdf_followup_replays_image_and_materializes_pdf(
_e2e_stack: dict[str, Any],
) -> None:
manager: SessionManager = _e2e_stack["manager"]
subscription_manager: SubscriptionManager = _e2e_stack["subscription_manager"]
sink: _EventSink = _e2e_stack["sink"]
vision_provider: _RecordingProvider = _e2e_stack["vision_provider"]
key = "agent:main:mixed-image-pdf-history"
await manager.create(session_key=key, agent_id="main")
subscription_manager.subscribe_messages(sink.conn_id, key)
image_uuid = await _upload_file(
_e2e_stack["app"],
name="first.png",
mime="image/png",
payload=_PNG_BYTES,
)
pdf_bytes = _sample_pdf_bytes()
pdf_uuid = await _upload_file(
_e2e_stack["app"],
name="L11 RL.pdf",
mime="application/pdf",
payload=pdf_bytes,
)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="请同时看看这张图和 PDF",
attachments=[
_attachment(image_uuid, mime="image/png", name="first.png"),
_attachment(pdf_uuid, mime="application/pdf", name="L11 RL.pdf"),
],
)
await manager.append_message(key, "user", "中间普通文本。")
await manager.append_message(key, "assistant", "普通回答。")
vision_calls_before = len(vision_provider.calls)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="刚才那张图右上角是什么?另外 PDF 也保持可编辑。",
)
assert len(vision_provider.calls) == vision_calls_before + 1
sent_messages = vision_provider.calls[-1]["messages"]
assert any(_message_has_image(message) for message in sent_messages[:-1])
sent_text = _all_provider_text(sent_messages)
assert "historical attachment available: L11 RL.pdf (application/pdf" in sent_text
done_events = _event_payloads(sink, "session.event.done")
assert done_events[-1]["image_route_reason"] == "gate_history"
assert done_events[-1]["vision_followup_needs_image"] is True
@pytest.mark.asyncio
async def test_router_text_only_does_not_replay_image_but_keeps_pdf_path(
_e2e_stack: dict[str, Any],
) -> None:
manager: SessionManager = _e2e_stack["manager"]
subscription_manager: SubscriptionManager = _e2e_stack["subscription_manager"]
sink: _EventSink = _e2e_stack["sink"]
gate_provider: _RecordingProvider = _e2e_stack["gate_provider"]
text_provider: _RecordingProvider = _e2e_stack["text_provider"]
vision_provider: _RecordingProvider = _e2e_stack["vision_provider"]
key = "agent:main:mixed-image-pdf-text-only"
await manager.create(session_key=key, agent_id="main")
subscription_manager.subscribe_messages(sink.conn_id, key)
image_uuid = await _upload_file(
_e2e_stack["app"],
name="first.png",
mime="image/png",
payload=_PNG_BYTES,
)
pdf_uuid = await _upload_file(
_e2e_stack["app"],
name="L11 RL.pdf",
mime="application/pdf",
payload=_sample_pdf_bytes(),
)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="请同时看看这张图和 PDF",
attachments=[
_attachment(image_uuid, mime="image/png", name="first.png"),
_attachment(pdf_uuid, mime="application/pdf", name="L11 RL.pdf"),
],
)
await manager.append_message(key, "user", "中间普通文本。")
await manager.append_message(key, "assistant", "普通回答。")
gate_provider.text = (
'{"decision":"text_only","confidence":0.91,"reason":"file edit only"}'
)
text_calls_before = len(text_provider.calls)
vision_calls_before = len(vision_provider.calls)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="刚才那个 PDF 的 Machine Learning 文字需要遮住。",
)
assert len(text_provider.calls) == text_calls_before + 1
assert len(vision_provider.calls) == vision_calls_before
sent_messages = text_provider.calls[-1]["messages"]
assert not any(_message_has_image(message) for message in sent_messages)
sent_text = _all_provider_text(sent_messages)
assert "historical attachment available: L11 RL.pdf (application/pdf" in sent_text
done_events = _event_payloads(sink, "session.event.done")
assert done_events[-1]["vision_followup_gate_decision"] == "text_only"
assert done_events[-1]["vision_followup_needs_image"] is False
@pytest.mark.asyncio
async def test_attachment_filename_traversal_is_sanitized(
_e2e_stack: dict[str, Any],
) -> None:
manager: SessionManager = _e2e_stack["manager"]
subscription_manager: SubscriptionManager = _e2e_stack["subscription_manager"]
sink: _EventSink = _e2e_stack["sink"]
text_provider: _RecordingProvider = _e2e_stack["text_provider"]
config: GatewayConfig = _e2e_stack["config"]
key = "agent:main:pdf-materialization-traversal"
await manager.create(session_key=key, agent_id="main")
subscription_manager.subscribe_messages(sink.conn_id, key)
file_uuid = await _upload_file(
_e2e_stack["app"],
name="../../evil.pdf",
mime="application/pdf",
payload=_sample_pdf_bytes(),
)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="请读取这个 PDF",
attachments=[_attachment(file_uuid, mime="application/pdf", name="../../evil.pdf")],
)
workspace_root = Path(config.workspace_dir or "").resolve()
workspace_paths = list((workspace_root / ".opensquilla" / "attachments").glob("**/*.pdf"))
assert len(workspace_paths) == 1
materialized = workspace_paths[0].resolve()
materialized.relative_to(workspace_root)
assert ".." not in materialized.relative_to(workspace_root).as_posix()
assert materialized.name.endswith("evil.pdf")
sent_text = _all_provider_text(text_provider.calls[-1]["messages"])
assert "../" not in sent_text
assert ".opensquilla/attachments/" in sent_text
@pytest.mark.asyncio
async def test_missing_material_emits_unavailable_marker(
_e2e_stack: dict[str, Any],
) -> None:
manager: SessionManager = _e2e_stack["manager"]
subscription_manager: SubscriptionManager = _e2e_stack["subscription_manager"]
sink: _EventSink = _e2e_stack["sink"]
text_provider: _RecordingProvider = _e2e_stack["text_provider"]
config: GatewayConfig = _e2e_stack["config"]
key = "agent:main:pdf-materialization-missing"
session = await manager.create(session_key=key, agent_id="main")
subscription_manager.subscribe_messages(sink.conn_id, key)
file_uuid = await _upload_file(
_e2e_stack["app"],
name="L11 RL.pdf",
mime="application/pdf",
payload=_sample_pdf_bytes(),
)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="请读取这个 PDF",
attachments=[_attachment(file_uuid, mime="application/pdf", name="L11 RL.pdf")],
)
transcript = await manager.get_transcript(key)
persisted = json.loads(transcript[0].content)
sha = persisted["attachments"][0]["sha256_ref"]
transcript_material_path(
Path(config.attachments.media_root or ""),
session.session_id,
sha,
).unlink()
calls_before = len(text_provider.calls)
await _send_session_turn(
ctx=_e2e_stack["ctx"],
key=key,
sink=sink,
message="刚才那个 PDF 还能编辑吗?",
)
assert len(text_provider.calls) == calls_before + 1
sent_text = _all_provider_text(text_provider.calls[-1]["messages"])
assert "historical attachment unavailable: L11 RL.pdf (application/pdf)" in sent_text
assert "historical attachment available: L11 RL.pdf" not in sent_text
@@ -0,0 +1,73 @@
"""Opt-in live agent context-boundary smoke.
This test uses public synthetic text only and skips unless explicitly enabled
with local credentials.
"""
from __future__ import annotations
import os
import pytest
from opensquilla.engine import Agent, AgentConfig
from opensquilla.provider import Message
from opensquilla.provider.openai import OpenAIProvider
pytestmark = [pytest.mark.llm, pytest.mark.llm_smoke, pytest.mark.agent_context_boundary]
_EXPECTED_TOKEN = "opensquilla-agent-boundary-live-ok"
_INTERNAL_MARKERS = (
"opensquilla_compacted",
"tool_use_argument_projection",
"provider_request_compacted",
"invalid_provider_context_projection",
)
@pytest.mark.asyncio
async def test_live_openrouter_agent_boundary_smoke() -> None:
if os.environ.get("OPENSQUILLA_AGENT_CONTEXT_BOUNDARY_LIVE") != "1":
pytest.skip("set OPENSQUILLA_AGENT_CONTEXT_BOUNDARY_LIVE=1 to run live smoke")
api_key = os.environ.get("OPENROUTER_API_KEY")
if not api_key:
pytest.skip("OPENROUTER_API_KEY not set")
provider = OpenAIProvider(
api_key=api_key,
model=os.environ.get("LLM_TEST_MODEL", "openai/gpt-4o-mini"),
base_url=os.environ.get("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"),
provider_kind="openrouter",
)
agent = Agent(
provider=provider,
config=AgentConfig(
context_window_tokens=200_000,
max_tokens=64,
provider_request_proof_max_chars=8_000,
flush_enabled=False,
max_iterations=1,
request_timeout=45.0,
),
)
agent.set_history(
[
Message(
role="user",
content="Public dummy archive context.\n" + ("alpha beta gamma\n" * 2000),
),
Message(
role="assistant",
content="Public dummy previous answer.\n" + ("delta epsilon\n" * 2000),
),
]
)
events = [
event
async for event in agent.run_turn(f"Reply with exactly {_EXPECTED_TOKEN}.")
]
text = "\n".join(str(getattr(event, "text", "")) for event in events).lower()
assert _EXPECTED_TOKEN in text
assert not any(marker in text for marker in _INTERNAL_MARKERS)
@@ -0,0 +1,580 @@
"""Opt-in live Feishu platform smoke.
This maintainer-only gate hits a real Feishu tenant when explicitly enabled.
Credentials come from environment variables or the local OpenSquilla config;
do not store them in fixtures or repository files.
"""
from __future__ import annotations
import json
import os
import tempfile
import time
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Literal
import pytest
from opensquilla.channels.feishu import FeishuChannel, FeishuChannelConfig
from opensquilla.tools.builtin.feishu_platform import (
_platform_json,
clear_feishu_channels,
feishu_doc_create,
feishu_doc_list_blocks,
feishu_doc_read_raw,
feishu_drive_search,
feishu_drive_upload_artifact,
feishu_perm_grant_member,
feishu_scopes_status,
feishu_wiki_get_node,
feishu_wiki_list_nodes,
feishu_wiki_list_spaces,
register_feishu_channel,
)
from opensquilla.tools.builtin.file_authoring import create_csv
from opensquilla.tools.types import CallerKind, ToolContext, current_tool_context
pytestmark = pytest.mark.live_channel
@dataclass(frozen=True)
class LiveFeishuCredentials:
app_id: str
app_secret: str
channel_name: str
api_base: str
domain: Literal["feishu", "lark"]
source: str
@dataclass
class LiveCheck:
name: str
status: str
detail: dict[str, Any] = field(default_factory=dict)
def _require_live_credentials() -> LiveFeishuCredentials:
if os.environ.get("OPENSQUILLA_FEISHU_LIVE") != "1":
pytest.skip("set OPENSQUILLA_FEISHU_LIVE=1 to run live Feishu platform smoke")
app_id = os.environ.get("OPENSQUILLA_FEISHU_APP_ID", "").strip()
app_secret = os.environ.get("OPENSQUILLA_FEISHU_APP_SECRET", "").strip()
if app_id or app_secret:
if not app_id or not app_secret:
pytest.skip("set both OPENSQUILLA_FEISHU_APP_ID and OPENSQUILLA_FEISHU_APP_SECRET")
return LiveFeishuCredentials(
app_id=app_id,
app_secret=app_secret,
channel_name=os.environ.get("OPENSQUILLA_FEISHU_CHANNEL", "env"),
api_base=os.environ.get(
"OPENSQUILLA_FEISHU_API_BASE",
"https://open.feishu.cn/open-apis",
),
domain="lark" if os.environ.get("OPENSQUILLA_FEISHU_DOMAIN") == "lark" else "feishu",
source="env",
)
credentials = _load_live_config_credentials()
if credentials is None:
pytest.skip(
"set OPENSQUILLA_FEISHU_APP_ID/OPENSQUILLA_FEISHU_APP_SECRET or configure a Feishu "
"channel in OPENSQUILLA_FEISHU_CONFIG_PATH, OPENSQUILLA_GATEWAY_CONFIG_PATH, or "
"~/.opensquilla/config.toml"
)
return credentials
def _load_live_config_credentials() -> LiveFeishuCredentials | None:
from opensquilla.gateway.config import GatewayConfig
config_path = (
os.environ.get("OPENSQUILLA_FEISHU_CONFIG_PATH")
or os.environ.get("OPENSQUILLA_GATEWAY_CONFIG_PATH")
or None
)
candidates: list[str | Path | None] = [config_path]
if config_path is None:
userprofile = os.environ.get("USERPROFILE", "").strip()
if userprofile:
userprofile_config = Path(userprofile) / ".opensquilla" / "config.toml"
if userprofile_config.is_file():
candidates.append(userprofile_config)
for candidate in candidates:
config = GatewayConfig.load(candidate)
credentials = _credentials_from_gateway_config(config)
if credentials is not None:
return credentials
return None
def _credentials_from_gateway_config(config: Any) -> LiveFeishuCredentials | None:
feishu_entries = [
entry for entry in config.channels.channels if getattr(entry, "type", None) == "feishu"
]
if not feishu_entries:
return None
preferred_name = os.environ.get("OPENSQUILLA_FEISHU_CHANNEL", "").strip()
if preferred_name:
selected = next((entry for entry in feishu_entries if entry.name == preferred_name), None)
else:
selected = next((entry for entry in feishu_entries if entry.enabled), feishu_entries[0])
if selected is None:
return None
app_id = getattr(selected, "app_id", "").strip()
app_secret = getattr(selected, "app_secret", "").strip()
if not app_id or not app_secret:
return None
return LiveFeishuCredentials(
app_id=app_id,
app_secret=app_secret,
channel_name=selected.name,
api_base=selected.api_base,
domain=selected.domain,
source="config",
)
def test_live_feishu_credentials_prefer_explicit_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("OPENSQUILLA_FEISHU_LIVE", "1")
monkeypatch.setenv("OPENSQUILLA_FEISHU_APP_ID", "cli_from_env")
monkeypatch.setenv("OPENSQUILLA_FEISHU_APP_SECRET", "secret_from_env")
credentials = _require_live_credentials()
assert credentials.app_id == "cli_from_env"
assert credentials.app_secret == "secret_from_env"
assert credentials.source == "env"
def test_live_feishu_credentials_fall_back_to_enabled_config_channel(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
config_path = tmp_path / "config.toml"
config_path.write_text(
"""
[[channels.channels]]
name = "old-feishu"
type = "feishu"
enabled = false
app_id = "cli_old"
app_secret = "secret_old"
[[channels.channels]]
name = "feishu"
type = "feishu"
enabled = true
app_id = "cli_from_config"
app_secret = "secret_from_config"
connection_mode = "websocket"
api_base = "https://open.feishu.cn/open-apis"
domain = "feishu"
""".lstrip(),
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_FEISHU_LIVE", "1")
monkeypatch.delenv("OPENSQUILLA_FEISHU_APP_ID", raising=False)
monkeypatch.delenv("OPENSQUILLA_FEISHU_APP_SECRET", raising=False)
monkeypatch.setenv("OPENSQUILLA_FEISHU_CONFIG_PATH", str(config_path))
credentials = _require_live_credentials()
assert credentials.app_id == "cli_from_config"
assert credentials.app_secret == "secret_from_config"
assert credentials.channel_name == "feishu"
assert credentials.source == "config"
def test_live_feishu_credentials_fall_back_to_windows_userprofile(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
wrong_home = tmp_path / "wrong-home"
userprofile = tmp_path / "windows-user"
config_dir = userprofile / ".opensquilla"
config_dir.mkdir(parents=True)
(config_dir / "config.toml").write_text(
"""
[[channels.channels]]
name = "feishu"
type = "feishu"
enabled = true
app_id = "cli_from_userprofile"
app_secret = "secret_from_userprofile"
""".lstrip(),
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_FEISHU_LIVE", "1")
monkeypatch.delenv("OPENSQUILLA_FEISHU_APP_ID", raising=False)
monkeypatch.delenv("OPENSQUILLA_FEISHU_APP_SECRET", raising=False)
monkeypatch.delenv("OPENSQUILLA_FEISHU_CONFIG_PATH", raising=False)
monkeypatch.delenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", raising=False)
monkeypatch.setenv("HOME", str(wrong_home))
monkeypatch.setenv("USERPROFILE", str(userprofile))
credentials = _load_live_config_credentials()
assert credentials is not None
assert credentials.app_id == "cli_from_userprofile"
assert credentials.app_secret == "secret_from_userprofile"
assert credentials.source == "config"
async def _json_result(awaitable: Awaitable[str]) -> dict[str, Any]:
payload = json.loads(await awaitable)
if not isinstance(payload, dict):
raise AssertionError(f"expected JSON object, got: {payload!r}")
return payload
def _check_success(name: str, payload: dict[str, Any], **detail: Any) -> LiveCheck:
assert payload.get("status") != "error", payload
return LiveCheck(name=name, status="PASS", detail=detail or _compact(payload))
def _check_missing_scope(name: str, payload: dict[str, Any]) -> LiveCheck:
if payload.get("status") == "error" and payload.get("error_type") == "missing_scope":
diagnostic = payload.get("diagnostic")
assert isinstance(diagnostic, dict)
scopes = diagnostic.get("required_scopes")
assert isinstance(scopes, list) and scopes, payload
return LiveCheck(
name=name,
status="EXPECTED_MISSING_SCOPE",
detail={
"feature": diagnostic.get("feature"),
"code": diagnostic.get("code"),
"required_scopes": scopes,
},
)
return _check_success(name, payload)
def _compact(value: Any) -> Any:
if isinstance(value, dict):
result: dict[str, Any] = {}
for key, item in value.items():
if key == "content" and isinstance(item, str):
result[key] = {"length": len(item), "preview": item[:80]}
elif key == "grant_url":
result[key] = "<redacted>"
elif key == "features" and isinstance(item, dict):
result[key] = sorted(item)
else:
result[key] = _compact(item)
return result
if isinstance(value, list):
return [_compact(item) for item in value[:5]]
return value
def _document_id(payload: dict[str, Any]) -> str:
document = payload.get("document")
assert isinstance(document, dict), payload
value = document.get("document_id")
assert isinstance(value, str) and value, payload
return value
async def _record(
checks: list[LiveCheck],
name: str,
func: Callable[[], Awaitable[LiveCheck]],
) -> LiveCheck:
try:
check = await func()
except Exception as exc:
check = LiveCheck(
name=name,
status="FAIL",
detail={"type": type(exc).__name__, "message": str(exc)},
)
checks.append(check)
return check
def _assert_no_failures(checks: list[LiveCheck]) -> None:
failures = [check for check in checks if check.status == "FAIL"]
assert not failures, [check.__dict__ for check in failures]
@pytest.mark.asyncio
async def test_feishu_platform_live_smoke() -> None:
credentials = _require_live_credentials()
marker = f"opensquilla-live-smoke-{int(time.time())}"
checks: list[LiveCheck] = []
channel = FeishuChannel(
FeishuChannelConfig(
app_id=credentials.app_id,
app_secret=credentials.app_secret,
connection_mode="webhook",
api_base=credentials.api_base,
domain=credentials.domain,
)
)
register_feishu_channel("live", channel)
try:
await _record(
checks,
"tenant_access_token",
lambda: _tenant_access_token_check(channel),
)
await _record(checks, "bot_info", lambda: _bot_info_check(channel))
await _record(
checks,
"scopes_status",
lambda: _scopes_status_check(),
)
doc_check = await _record(
checks,
"doc_create",
lambda: _doc_create_check(marker),
)
document_id = str(doc_check.detail.get("document_id") or "")
if document_id:
await _record(
checks,
"doc_read_raw",
lambda: _doc_read_raw_check(document_id),
)
await _record(
checks,
"doc_list_blocks",
lambda: _doc_list_blocks_check(document_id),
)
else:
checks.append(
LiveCheck("doc_read_raw", "SKIP", {"reason": "doc_create returned no document_id"})
)
checks.append(
LiveCheck(
"doc_list_blocks",
"SKIP",
{"reason": "doc_create returned no document_id"},
)
)
root_token = await _root_folder_token(channel, checks)
if root_token:
await _record(
checks,
"drive_upload_csv",
lambda: _drive_upload_csv_check(root_token, marker),
)
else:
checks.append(
LiveCheck(
"drive_upload_csv",
"SKIP",
{"reason": "drive root folder token unavailable"},
)
)
await _record(checks, "drive_search", lambda: _drive_search_check(marker))
await _run_wiki_checks(checks)
await _record(
checks,
"permission_grant_member_dry_run",
lambda: _permission_dry_run_check(document_id),
)
if (
os.environ.get("OPENSQUILLA_FEISHU_LIVE_MUTATE_PERM") == "1"
and os.environ.get("OPENSQUILLA_FEISHU_TEST_OPEN_ID")
and document_id
):
await _record(
checks,
"permission_grant_member_mutation",
lambda: _permission_mutation_check(document_id),
)
else:
checks.append(
LiveCheck(
"permission_grant_member_mutation",
"SKIP",
{
"reason": (
"requires OPENSQUILLA_FEISHU_TEST_OPEN_ID, "
"OPENSQUILLA_FEISHU_LIVE_MUTATE_PERM=1, and doc_create success"
)
},
)
)
finally:
await channel.stop()
clear_feishu_channels()
_assert_no_failures(checks)
async def _tenant_access_token_check(channel: FeishuChannel) -> LiveCheck:
token = await channel._get_token()
assert token
return LiveCheck(
name="tenant_access_token",
status="PASS",
detail={"token_received": True, "token_length": len(token)},
)
async def _bot_info_check(channel: FeishuChannel) -> LiveCheck:
await channel._refresh_bot_identity()
assert channel.bot_open_id
return LiveCheck(name="bot_info", status="PASS", detail={"bot_open_id_present": True})
async def _scopes_status_check() -> LiveCheck:
payload = await _json_result(feishu_scopes_status(channel="live"))
return _check_success("scopes_status", payload, features=sorted(payload.get("features", {})))
async def _doc_create_check(marker: str) -> LiveCheck:
payload = await _json_result(feishu_doc_create(title=f"{marker} doc", channel="live"))
document_id = _document_id(payload)
return _check_success("doc_create", payload, document_id=document_id)
async def _doc_read_raw_check(document_id: str) -> LiveCheck:
payload = await _json_result(feishu_doc_read_raw(document_id=document_id, channel="live"))
assert "content" in payload, payload
return _check_success("doc_read_raw", payload, content_length=len(str(payload["content"])))
async def _doc_list_blocks_check(document_id: str) -> LiveCheck:
payload = await _json_result(feishu_doc_list_blocks(document_id=document_id, channel="live"))
assert isinstance(payload.get("items"), list), payload
return _check_success("doc_list_blocks", payload, block_count=len(payload["items"]))
async def _root_folder_token(channel: FeishuChannel, checks: list[LiveCheck]) -> str | None:
try:
payload = await _platform_json(channel, "GET", "/drive/explorer/v2/root_folder/meta")
except Exception as exc:
checks.append(
LiveCheck(
"drive_root_folder_meta",
"FAIL",
{"type": type(exc).__name__, "message": str(exc)},
)
)
return None
token = payload.get("token")
status = "PASS" if isinstance(token, str) and token else "FAIL"
checks.append(
LiveCheck(
"drive_root_folder_meta",
status,
{"token_present": bool(token), "id_present": bool(payload.get("id"))},
)
)
return token if isinstance(token, str) else None
async def _drive_upload_csv_check(root_token: str, marker: str) -> LiveCheck:
with tempfile.TemporaryDirectory(prefix="opensquilla-feishu-live-") as tmp_dir:
ctx = ToolContext(
is_owner=False,
caller_kind=CallerKind.CHANNEL,
source_name="live",
channel_kind="feishu",
channel_id="live-drive",
sender_id="live-smoke",
artifact_media_root=str(Path(tmp_dir) / "media"),
artifact_session_id="live-session",
session_key=f"agent:main:feishu:live:{marker}",
)
token = current_tool_context.set(ctx)
try:
artifact = await _json_result(
create_csv(rows=[["kind", "value"], ["live", marker]], name=f"{marker}.csv")
)
artifact_id = artifact.get("artifact", {}).get("id")
assert isinstance(artifact_id, str) and artifact_id, artifact
payload = await _json_result(
feishu_drive_upload_artifact(artifact_id=artifact_id, parent_node=root_token)
)
finally:
current_tool_context.reset(token)
return _check_missing_scope("drive_upload_csv", payload)
async def _drive_search_check(marker: str) -> LiveCheck:
payload = await _json_result(feishu_drive_search(query=marker, channel="live"))
return _check_missing_scope("drive_search", payload)
async def _run_wiki_checks(checks: list[LiveCheck]) -> None:
spaces_payload = await _json_result(feishu_wiki_list_spaces(channel="live"))
spaces_check = _check_missing_scope("wiki_list_spaces", spaces_payload)
checks.append(spaces_check)
if spaces_check.status == "EXPECTED_MISSING_SCOPE":
checks.append(LiveCheck("wiki_list_nodes", "SKIP", {"reason": "wiki scopes missing"}))
checks.append(LiveCheck("wiki_get_node", "SKIP", {"reason": "wiki scopes missing"}))
return
items = spaces_payload.get("items")
if not isinstance(items, list) or not items:
checks.append(LiveCheck("wiki_list_nodes", "SKIP", {"reason": "no wiki spaces returned"}))
checks.append(LiveCheck("wiki_get_node", "SKIP", {"reason": "no wiki spaces returned"}))
return
first_space = items[0]
assert isinstance(first_space, dict), spaces_payload
space_id = first_space.get("space_id")
assert isinstance(space_id, str) and space_id, spaces_payload
nodes_payload = await _json_result(feishu_wiki_list_nodes(space_id=space_id, channel="live"))
nodes_check = _check_missing_scope("wiki_list_nodes", nodes_payload)
checks.append(nodes_check)
if nodes_check.status == "EXPECTED_MISSING_SCOPE":
checks.append(LiveCheck("wiki_get_node", "SKIP", {"reason": "wiki node scopes missing"}))
return
nodes = nodes_payload.get("items")
if not isinstance(nodes, list) or not nodes:
checks.append(LiveCheck("wiki_get_node", "SKIP", {"reason": "no wiki nodes returned"}))
return
first_node = nodes[0]
assert isinstance(first_node, dict), nodes_payload
node_token = first_node.get("node_token") or first_node.get("token")
assert isinstance(node_token, str) and node_token, nodes_payload
node_payload = await _json_result(feishu_wiki_get_node(token=node_token, channel="live"))
checks.append(_check_missing_scope("wiki_get_node", node_payload))
async def _permission_dry_run_check(document_id: str) -> LiveCheck:
payload = await _json_result(
feishu_perm_grant_member(
token=document_id or "dry_run_token",
doc_type="docx" if document_id else "file",
member_type="openid",
member_id=os.environ.get("OPENSQUILLA_FEISHU_TEST_OPEN_ID", "ou_placeholder"),
perm="view",
channel="live",
)
)
assert payload.get("status") == "dry_run", payload
return LiveCheck(
name="permission_grant_member_dry_run",
status="PASS",
detail={"operation": payload.get("operation")},
)
async def _permission_mutation_check(document_id: str) -> LiveCheck:
payload = await _json_result(
feishu_perm_grant_member(
token=document_id,
doc_type="docx",
member_type="openid",
member_id=os.environ["OPENSQUILLA_FEISHU_TEST_OPEN_ID"],
perm="view",
dry_run=False,
channel="live",
)
)
return _check_missing_scope("permission_grant_member_mutation", payload)
@@ -0,0 +1,42 @@
"""Opt-in live Telegram channel smoke.
This is a maintainer-only release gate. It sends one real message through the
OpenSquilla Telegram adapter when explicitly enabled and credentialed.
"""
from __future__ import annotations
import os
import time
import pytest
from opensquilla.channels.telegram import TelegramChannel, TelegramChannelConfig
from opensquilla.channels.types import OutgoingMessage
pytestmark = pytest.mark.live_channel
@pytest.mark.asyncio
async def test_telegram_adapter_can_send_real_message() -> None:
if os.environ.get("OPENSQUILLA_LIVE_TELEGRAM_E2E") != "1":
pytest.skip("set OPENSQUILLA_LIVE_TELEGRAM_E2E=1 to run live Telegram smoke")
token = os.environ.get("OPENSQUILLA_LIVE_TELEGRAM_BOT_TOKEN", "").strip()
chat_id = os.environ.get("OPENSQUILLA_LIVE_TELEGRAM_CHAT_ID", "").strip()
if not token or not chat_id:
pytest.skip("Telegram token/chat id not set")
marker = f"opensquilla live channel smoke {int(time.time())}"
channel = TelegramChannel(
TelegramChannelConfig(
token=token,
default_chat_id=chat_id,
drop_pending_updates=True,
)
)
try:
result = await channel.send(OutgoingMessage(content=marker))
finally:
await channel.stop()
assert isinstance(result.get("message_id"), int)
@@ -0,0 +1,162 @@
"""Opt-in live OpenRouter compaction smoke.
Uses only public synthetic text and skips unless explicitly enabled with local
credentials. The goal is to prove the compaction path can complete with a real
LLM call without making normal test runs credentialed or costful.
"""
from __future__ import annotations
import os
from pathlib import Path
import pytest
from opensquilla.session.compaction import CompactionConfig
from opensquilla.session.manager import SessionManager
from opensquilla.session.storage import SessionStorage
pytestmark = [pytest.mark.llm, pytest.mark.llm_smoke, pytest.mark.agent_context_boundary]
@pytest.mark.asyncio
async def test_live_openrouter_session_compaction_succeeds(tmp_path: Path) -> None:
if os.environ.get("OPENSQUILLA_LIVE_COMPACTION_E2E") != "1":
pytest.skip("set OPENSQUILLA_LIVE_COMPACTION_E2E=1 to run live compaction smoke")
api_key = os.environ.get("OPENROUTER_API_KEY")
if not api_key:
pytest.skip("OPENROUTER_API_KEY not set")
assert api_key is not None
storage = SessionStorage(str(tmp_path / "sessions.db"))
await storage.connect()
manager = SessionManager(storage)
session_key = "agent:main:live-compaction"
try:
await manager.create(session_key)
for index in range(12):
await manager.append_message(
session_key,
"user" if index % 2 == 0 else "assistant",
(
f"Synthetic compaction fact {index}: "
"the public dummy project uses alpha, beta, and gamma markers. "
"Keep the current goal, completed steps, failures, and next action. " * 20
),
token_count=450,
)
result = await manager.compact_with_result(
session_key,
context_window_tokens=2_000,
config=CompactionConfig(
model=os.environ.get("LLM_TEST_MODEL", "openai/gpt-4o-mini"),
api_key=api_key,
base_url=os.environ.get(
"OPENROUTER_BASE_URL",
"https://openrouter.ai/api/v1",
),
timeout_seconds=90,
),
)
assert result.summary
assert result.summary_source == "llm"
assert result.removed_count > 0
assert result.chunks_processed >= 1
assert result.tokens_after < result.tokens_before
node = await manager._storage.get_session(session_key)
assert node is not None
assert node.compaction_count == 1
assert await manager.get_transcript(session_key)
summaries = await manager.get_summaries(session_key)
assert [summary.summary_text for summary in summaries] == [result.summary]
finally:
await storage.close()
@pytest.mark.asyncio
async def test_live_openrouter_coding_profile_preserves_recent_tail(
tmp_path: Path,
) -> None:
if os.environ.get("OPENSQUILLA_LIVE_COMPACTION_E2E") != "1":
pytest.skip("set OPENSQUILLA_LIVE_COMPACTION_E2E=1 to run live compaction smoke")
api_key = os.environ.get("OPENROUTER_API_KEY")
if not api_key:
pytest.skip("OPENROUTER_API_KEY not set")
assert api_key is not None
storage = SessionStorage(str(tmp_path / "sessions.db"))
await storage.connect()
manager = SessionManager(storage)
session_key = "agent:main:live-coding-profile-compaction"
protected_tail = [
(
"user",
"Current task: keep the beta migration open and verify the next shell check.",
),
("assistant", "Acknowledged current task and pending verification."),
("user", "Do not lose marker PROFILE-LIVE-TAIL-001."),
("assistant", "Marker PROFILE-LIVE-TAIL-001 is still active."),
]
try:
await manager.create(session_key)
for index in range(14):
await manager.append_message(
session_key,
"user" if index % 2 == 0 else "assistant",
(
f"Synthetic coding history {index}: "
"completed setup, reviewed logs, and kept neutral public markers. "
"Summarize this older context without private data. " * 20
),
token_count=450,
)
for role, content in protected_tail:
await manager.append_message(
session_key,
role,
content,
token_count=20,
)
result = await manager.compact_with_result(
session_key,
context_window_tokens=2_000,
config=CompactionConfig(
model=os.environ.get("LLM_TEST_MODEL", "openai/gpt-4o-mini"),
api_key=api_key,
base_url=os.environ.get(
"OPENROUTER_BASE_URL",
"https://openrouter.ai/api/v1",
),
timeout_seconds=90,
compaction_profile="coding",
protected_recent_messages=len(protected_tail),
),
)
assert result.summary
assert result.summary_source == "llm"
assert result.removed_count > 0
assert result.quality_report["profile"] == "coding"
assert result.quality_report["protected_tail_preserved"] is True
assert result.quality_report["fits_context_window"] is True
assert result.quality_report["passes_structural_gate"] is True
transcript = await manager.get_transcript(session_key)
tail_contents = [
entry.content or ""
for entry in transcript[-len(protected_tail) :]
]
assert all(
expected_content in actual_content
for actual_content, (_, expected_content) in zip(
tail_contents,
protected_tail,
strict=True,
)
)
finally:
await storage.close()
+81
View File
@@ -0,0 +1,81 @@
"""Opt-in live LLM smoke.
This is intentionally the only live LLM smoke restored for open-source
readiness. It skips unless credentials are explicitly present.
"""
from __future__ import annotations
import os
import pytest
from opensquilla.provider.openai import OpenAIProvider
from opensquilla.provider.types import ChatConfig, DoneEvent, ErrorEvent, Message, TextDeltaEvent
pytestmark = [pytest.mark.llm, pytest.mark.llm_smoke]
_EXPECTED_TOKEN = "opensquilla-live-smoke-ok"
@pytest.mark.asyncio
async def test_openrouter_live_smoke_returns_expected_token() -> None:
api_key = os.environ.get("OPENROUTER_API_KEY")
if not api_key:
pytest.skip("OPENROUTER_API_KEY not set")
provider = OpenAIProvider(
api_key=api_key,
model=os.environ.get("LLM_TEST_MODEL", "openai/gpt-4o-mini"),
base_url=os.environ.get("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"),
provider_kind="openrouter",
)
text_parts: list[str] = []
done = False
async for event in provider.chat(
[Message(role="user", content=f"Reply with exactly {_EXPECTED_TOKEN}.")],
config=ChatConfig(max_tokens=32, temperature=0.0, timeout=45.0),
):
if isinstance(event, ErrorEvent):
pytest.fail(f"live LLM smoke failed: {event.code} {event.message}")
if isinstance(event, TextDeltaEvent):
text_parts.append(event.text)
if isinstance(event, DoneEvent):
done = True
assert done is True
assert _EXPECTED_TOKEN in "".join(text_parts).strip().lower()
@pytest.mark.asyncio
async def test_tokenrhythm_live_smoke_returns_expected_token() -> None:
api_key = os.environ.get("TOKENRHYTHM_API_KEY")
if not api_key:
pytest.skip("TOKENRHYTHM_API_KEY not set")
provider = OpenAIProvider(
api_key=api_key,
model=os.environ.get("TOKENRHYTHM_MODEL", "deepseek-v4-flash"),
base_url=os.environ.get("TOKENRHYTHM_BASE_URL", "https://tokenrhythm.studio/v1"),
provider_kind="tokenrhythm",
)
text_parts: list[str] = []
done = False
async for event in provider.chat(
[Message(role="user", content=f"Reply with exactly {_EXPECTED_TOKEN}.")],
# Every TokenRhythm model spends reasoning_content tokens out of
# max_tokens before any text; a small budget returns empty content
# with finish_reason "length".
config=ChatConfig(max_tokens=1024, temperature=0.0, timeout=90.0),
):
if isinstance(event, ErrorEvent):
pytest.fail(f"live LLM smoke failed: {event.code} {event.message}")
if isinstance(event, TextDeltaEvent):
text_parts.append(event.text)
if isinstance(event, DoneEvent):
done = True
assert done is True
assert _EXPECTED_TOKEN in "".join(text_parts).strip().lower()
File diff suppressed because it is too large Load Diff
+244
View File
@@ -0,0 +1,244 @@
"""Opt-in real-browser smoke for the Control UI.
The default test suite skips this file. Run it with:
OPENSQUILLA_WEBUI_BROWSER_E2E=1 uv run pytest tests/functional/test_webui_browser_e2e.py -q -s
"""
from __future__ import annotations
import json
import os
import socket
import subprocess
import sys
import textwrap
import time
from pathlib import Path
import httpx
import pytest
pytestmark = pytest.mark.webui_browser
def _free_port() -> int:
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
return int(sock.getsockname()[1])
def _npm() -> str:
return "npm.cmd" if os.name == "nt" else "npm"
def _node() -> str:
return "node.exe" if os.name == "nt" else "node"
def _install_playwright(work_dir: Path) -> None:
result = subprocess.run(
[_npm(), "--prefix", str(work_dir), "install", "playwright"],
cwd=Path.cwd(),
check=False,
capture_output=True,
text=True,
timeout=120,
)
assert result.returncode == 0, result.stderr or result.stdout
browser_result = subprocess.run(
[_npm(), "--prefix", str(work_dir), "exec", "playwright", "install", "chromium"],
cwd=Path.cwd(),
check=False,
capture_output=True,
text=True,
timeout=180,
)
assert browser_result.returncode == 0, browser_result.stderr or browser_result.stdout
def _wait_for_health(port: int, server: subprocess.Popen[str]) -> None:
url = f"http://127.0.0.1:{port}/health"
deadline = time.monotonic() + 20.0
last_error = ""
while time.monotonic() < deadline:
if server.poll() is not None:
stdout = server.stdout.read() if server.stdout else ""
stderr = server.stderr.read() if server.stderr else ""
raise AssertionError(
f"gateway exited early code={server.returncode}\nstdout={stdout}\nstderr={stderr}"
)
try:
response = httpx.get(url, timeout=1.0)
if response.status_code == 200 and response.json().get("ok") is True:
return
except Exception as exc: # noqa: BLE001 - included in timeout assertion.
last_error = str(exc)
time.sleep(0.1)
raise AssertionError(f"gateway did not become healthy: {last_error}")
def _stop_process(process: subprocess.Popen[str]) -> None:
process.terminate()
try:
process.wait(timeout=8)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=8)
def test_control_ui_loads_in_real_browser(tmp_path: Path) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_E2E=1 to run browser smoke")
port = _free_port()
server_script = tmp_path / "webui_smoke_server.py"
browser_script = tmp_path / "webui_smoke_browser.js"
state_dir = tmp_path / "state"
proposal_dir = state_dir / "proposals" / "deadbeef"
proposal_dir.mkdir(parents=True)
(proposal_dir / "SKILL.md").write_text(
"---\n"
"name: browser-audit-proposal\n"
"kind: meta\n"
"description: Browser smoke proposal.\n"
"triggers: [browser audit proposal]\n"
"composition:\n"
" steps:\n"
" - id: classify\n"
" kind: llm_classify\n"
" output_choices: [A, B]\n"
" with: {text: x}\n"
"---\n"
"# browser-audit-proposal\n",
encoding="utf-8",
)
(proposal_dir / "gates.json").write_text(
json.dumps(
{
"auto_enable_eligible": True,
"auto_enable": {
"status": "skipped",
"reason": "risk_too_high",
"risk_level": "medium",
"max_risk": "low",
"details": {
"validation_profile": "static-safety-v2",
"skills": ["artifact-writer"],
"tools": [],
"reasons": ["capability:artifact-writer:filesystem-write"],
},
},
}
),
encoding="utf-8",
)
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
"""
const { chromium } = require("playwright");
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
const response = await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForFunction(
() => document.querySelector("#conn-pill")?.textContent === "Connected",
{ timeout: 15000 }
);
await page.locator('a[data-path="/skills"]').click();
await page.waitForSelector('[data-proposal-show="deadbeef"]', {
state: "attached",
timeout: 15000,
});
await page.locator('[data-proposal-show="deadbeef"]').click();
await page.waitForSelector('dialog[open] .sk-audit-grid', { timeout: 15000 });
const auditText = await page.locator('dialog[open]').innerText();
const result = {
status: response ? response.status() : 0,
title: await page.title(),
appCount: await page.locator("#app").count(),
basePath: await page.locator("#opensquilla-data").getAttribute("data-base-path"),
authMode: await page.locator("#opensquilla-data").getAttribute("data-auth-mode"),
proposalButtons: await page.locator('[data-proposal-show="deadbeef"]').count(),
auditText,
pageErrors: errors,
};
await browser.close();
console.log(JSON.stringify(result));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(state_dir)
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
browser_env = dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/")
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=browser_env,
check=False,
capture_output=True,
text=True,
timeout=60,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["status"] == 200
assert payload["title"] == "OpenSquilla Control"
assert payload["appCount"] == 1
assert payload["basePath"] == "/control"
assert payload["authMode"] == "none"
assert payload["proposalButtons"] == 1
assert "Auto-enable Audit" in payload["auditText"]
assert "static-safety-v2" in payload["auditText"]
assert "medium / low" in payload["auditText"]
assert "capability:artifact-writer:filesystem-write" in payload["auditText"]
assert payload["pageErrors"] == []
@@ -0,0 +1,44 @@
"""Static-text acceptance: chat.js contains per-turn bubble markers.
These checks do not require a browser or network. They verify the generated
JavaScript source ships the expected per-turn semantics introduced in Phase 3:
- The bubble tooltip uses 'Turn — input:' (per-turn label, not session total).
- The bubble chip argument is 'u.input_tokens | 0' (per-turn source, not the
session accumulator).
"""
from __future__ import annotations
from pathlib import Path
_CHAT_JS = (
Path(__file__).parent.parent.parent
/ "src"
/ "opensquilla"
/ "gateway"
/ "static"
/ "js"
/ "views"
/ "chat.js"
)
def test_chat_js_bubble_tooltip_uses_per_turn_label():
"""chat.js must label the bubble tooltip with 'Turn — input:' so the UI
clearly shows per-turn semantics to users."""
assert _CHAT_JS.exists(), f"chat.js not found at {_CHAT_JS}"
source = _CHAT_JS.read_text(encoding="utf-8")
assert "Turn — input:" in source, (
"Expected per-turn tooltip label 'Turn — input:' not found in chat.js"
)
def test_chat_js_bubble_chip_uses_per_turn_argument():
"""chat.js must pass 'u.input_tokens | 0' (per-turn field) to the bubble
chip, not the session accumulator, ensuring per-bubble token counts reflect
only the current turn."""
assert _CHAT_JS.exists(), f"chat.js not found at {_CHAT_JS}"
source = _CHAT_JS.read_text(encoding="utf-8")
assert "u.input_tokens | 0" in source, (
"Expected per-turn chip argument 'u.input_tokens | 0' not found in chat.js"
)