mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 00:40:45 +08:00
Simplify oclaw product surface: drop niche specialists and admin noise.
Remove stock/image/video specialists, Gmail watcher, CocoLoop market, desktop packaging, and extra memory plugins; disable dynamic agents and hide Session Monitor / API grants / Admin Audit from nav. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
efa72df362
commit
cf31fd104d
290 changed files with 127 additions and 64576 deletions
|
|
@ -141,20 +141,19 @@ class AdminSkillsApiTests(unittest.TestCase):
|
|||
gb = g.json() or {}
|
||||
self.assertTrue(gb.get("ok"))
|
||||
self.assertIn("market_provider", gb)
|
||||
self.assertIn(str(gb.get("market_provider") or ""), {"clawhub", "cocoloop"})
|
||||
self.assertIn(str(gb.get("market_provider") or ""), {"clawhub"})
|
||||
s = self.client.post(
|
||||
"/admin/api/skills/mode",
|
||||
json={"prompt_in_system": True, "toolcall_enabled": False, "market_provider": "cocoloop"},
|
||||
json={"prompt_in_system": True, "market_provider": "cocoloop"},
|
||||
headers=self._h(),
|
||||
)
|
||||
self.assertEqual(s.status_code, 200, s.text)
|
||||
sb = s.json() or {}
|
||||
self.assertTrue(sb.get("ok"))
|
||||
self.assertTrue(bool(sb.get("prompt_in_system")))
|
||||
self.assertFalse(bool(sb.get("toolcall_enabled")))
|
||||
self.assertEqual(str(sb.get("market_provider") or ""), "cocoloop")
|
||||
self.assertEqual(str(sb.get("market_provider") or ""), "clawhub")
|
||||
g2 = self.client.get("/admin/api/skills/mode", headers=self._h())
|
||||
self.assertEqual((g2.json() or {}).get("market_provider"), "cocoloop")
|
||||
self.assertEqual((g2.json() or {}).get("market_provider"), "clawhub")
|
||||
|
||||
def test_skills_effective_dashboard(self) -> None:
|
||||
c = self.client.post(
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
"""Image specialist lane must use ``AIA_IMAGE_EXPERT_*``, not ``AIA_OCR_*``."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from svc.llm.image_legacy_client import send_legacy_image_messages
|
||||
|
||||
|
||||
def test_legacy_fails_when_only_ocr_env_configured(monkeypatch: object) -> None:
|
||||
monkeypatch.setenv("AIA_OCR_API_KEY", "k-ocr")
|
||||
monkeypatch.setenv("AIA_OCR_BASE_URL", "https://ocr.example/v1")
|
||||
monkeypatch.setenv("AIA_OCR_MODEL", "ocr-model")
|
||||
for k in ("AIA_IMAGE_EXPERT_API_KEY", "AIA_IMAGE_EXPERT_BASE_URL", "AIA_IMAGE_EXPERT_MODEL"):
|
||||
monkeypatch.delenv(k, raising=False)
|
||||
out = send_legacy_image_messages(
|
||||
images=["https://example.com/x.png"],
|
||||
prompt="hi",
|
||||
model=None,
|
||||
api_key=None,
|
||||
base_url=None,
|
||||
)
|
||||
assert out.get("ok") is False
|
||||
err = str(out.get("error") or "")
|
||||
assert "AIA_IMAGE_EXPERT" in err
|
||||
|
||||
|
||||
def test_legacy_accepts_explicit_kwargs_without_expert_env(monkeypatch: object) -> None:
|
||||
"""Explicit api_key/base_url/model override env (for scripts/tests only)."""
|
||||
for k in (
|
||||
"AIA_OCR_API_KEY",
|
||||
"AIA_OCR_BASE_URL",
|
||||
"AIA_IMAGE_EXPERT_API_KEY",
|
||||
"AIA_IMAGE_EXPERT_BASE_URL",
|
||||
):
|
||||
monkeypatch.delenv(k, raising=False)
|
||||
|
||||
recorded: dict[str, object] = {}
|
||||
|
||||
def fake_post(client, *, url: str, headers: dict[str, str], payload: dict[str, object]) -> object:
|
||||
recorded["url"] = url
|
||||
|
||||
class R:
|
||||
status_code = 200
|
||||
|
||||
def json(_self): # noqa: PLR6301
|
||||
return {"choices": [{"message": {"content": [{"text": "ok"}]}}]}
|
||||
|
||||
return R()
|
||||
|
||||
import svc.llm.image_legacy_client as mod
|
||||
|
||||
monkeypatch.setattr(mod, "post_with_retry", fake_post)
|
||||
out = send_legacy_image_messages(
|
||||
images=["https://example.com/x.png"],
|
||||
prompt="hello",
|
||||
model="m-special",
|
||||
api_key="explicit-key",
|
||||
base_url="https://gw.example/expert/v1",
|
||||
)
|
||||
assert out.get("ok") is True
|
||||
assert "/chat/completions" in str(recorded.get("url") or "")
|
||||
|
|
@ -1,388 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from svc.llm.image_http_common import (
|
||||
dashscope_multimodal_http_ok,
|
||||
dashscope_native_multimodal_url_from_compatible_base,
|
||||
extract_text_and_images,
|
||||
)
|
||||
from svc.llm.image_http_common import build_extract_diag_empty
|
||||
from svc.llm.image_legacy_client import (
|
||||
collect_legacy_lane_images_from_attachments,
|
||||
collect_legacy_lane_images_with_session_fallback,
|
||||
legacy_image_assistant_body_with_placeholder,
|
||||
legacy_image_turn_bundle,
|
||||
normalize_legacy_output_image_urls,
|
||||
parse_message_attachments_json,
|
||||
)
|
||||
from svc.llm.image_legacy_client import _http_content_blocks
|
||||
from svc.llm.image_legacy_client import _openai_compatible_vision_content
|
||||
|
||||
|
||||
def test_openai_compatible_vision_has_type_per_part() -> None:
|
||||
"""compatible-mode /chat/completions expects type on each content element."""
|
||||
b = _openai_compatible_vision_content(
|
||||
["https://example.invalid/a.png", "data:image/png;base64,abcd"],
|
||||
"prompt",
|
||||
)
|
||||
assert len(b) == 3
|
||||
assert b[0]["type"] == "image_url" and "url" in b[0]["image_url"]
|
||||
assert b[1]["type"] == "image_url"
|
||||
assert b[2]["type"] == "text" and b[2]["text"] == "prompt"
|
||||
|
||||
|
||||
def test_http_content_blocks_multi_image_then_text() -> None:
|
||||
"""DashScope samples: one user message, content = N × {"image": url} then {"text": ...}."""
|
||||
b = _http_content_blocks(
|
||||
[
|
||||
"https://example.invalid/a.png",
|
||||
"https://example.invalid/b.png",
|
||||
],
|
||||
"合成说明",
|
||||
typed=False,
|
||||
)
|
||||
assert b == [
|
||||
{"image": "https://example.invalid/a.png"},
|
||||
{"image": "https://example.invalid/b.png"},
|
||||
{"text": "合成说明"},
|
||||
]
|
||||
|
||||
|
||||
def test_normalize_legacy_output_dict_image_parts() -> None:
|
||||
u = "https://dashscope-result-sz.oss-cn-shenzhen.aliyuncs.com/x.png?Expires=1"
|
||||
assert normalize_legacy_output_image_urls([{"image": u}]) == [u]
|
||||
assert normalize_legacy_output_image_urls([{"image_url": {"url": u}}]) == [u]
|
||||
|
||||
|
||||
def test_legacy_turn_bundle_coerces_dict_images_to_attachments() -> None:
|
||||
ok, text, att = legacy_image_turn_bundle(
|
||||
{"ok": True, "text": "", "images": [{"image": "https://example.invalid/a.png"}]}
|
||||
)
|
||||
assert ok is True
|
||||
assert len(att) == 1
|
||||
assert att[0]["type"] == "image_url"
|
||||
assert att[0]["url"] == "https://example.invalid/a.png"
|
||||
|
||||
|
||||
def test_dashscope_native_url_from_compatible_base() -> None:
|
||||
assert (
|
||||
dashscope_native_multimodal_url_from_compatible_base(
|
||||
"https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
)
|
||||
== "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
|
||||
)
|
||||
assert (
|
||||
dashscope_native_multimodal_url_from_compatible_base("https://example.com/openai/v1") is None
|
||||
)
|
||||
|
||||
|
||||
def test_collect_legacy_lane_images_image_url() -> None:
|
||||
atts = [{"type": "image_url", "url": "https://example.invalid/x.png"}]
|
||||
assert collect_legacy_lane_images_from_attachments(atts) == ["https://example.invalid/x.png"]
|
||||
|
||||
|
||||
def test_collect_legacy_lane_images_raw_base64() -> None:
|
||||
atts = [{"type": "input_image", "mime": "image/png", "image_base64": "SGVsbG8="}]
|
||||
got = collect_legacy_lane_images_from_attachments(atts)
|
||||
assert len(got) == 1
|
||||
assert got[0].startswith("data:image/png;base64,")
|
||||
|
||||
|
||||
def test_legacy_turn_bundle_text_only_success() -> None:
|
||||
ok, text, att = legacy_image_turn_bundle({"ok": True, "text": "caption only", "images": []})
|
||||
assert ok is True
|
||||
assert text == "caption only"
|
||||
assert att == []
|
||||
|
||||
|
||||
def test_legacy_image_assistant_placeholder_zh_en() -> None:
|
||||
produced = [{"type": "image_ref", "attachment_id": "a" * 64}]
|
||||
assert "附件" in legacy_image_assistant_body_with_placeholder(
|
||||
lang="zh", body_text="", produced=produced
|
||||
)
|
||||
assert "attachment" in legacy_image_assistant_body_with_placeholder(
|
||||
lang="en", body_text="", produced=produced
|
||||
).lower()
|
||||
assert legacy_image_assistant_body_with_placeholder(lang="zh", body_text="x", produced=produced) == "x"
|
||||
|
||||
|
||||
def test_legacy_turn_bundle_upstream_error() -> None:
|
||||
ok, text, att = legacy_image_turn_bundle({"ok": False, "error": "rate"})
|
||||
assert ok is False
|
||||
assert "rate" in text
|
||||
assert att == []
|
||||
|
||||
|
||||
def test_extract_harvests_nested_https_under_message() -> None:
|
||||
url = "https://cdn.example.invalid/generated.png"
|
||||
text, images = extract_text_and_images(
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"metadata": {"preview_image": url},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
assert text == ""
|
||||
assert images == [url]
|
||||
|
||||
|
||||
def test_build_extract_diag_top_level_openai_choices() -> None:
|
||||
d = build_extract_diag_empty(
|
||||
{
|
||||
"choices": [],
|
||||
"model": "x",
|
||||
}
|
||||
)
|
||||
assert d.get("choices_len") == 0
|
||||
|
||||
|
||||
def test_legacy_turn_bundle_includes_provider_redacted() -> None:
|
||||
ok, msg, att = legacy_image_turn_bundle(
|
||||
{
|
||||
"ok": True,
|
||||
"text": "",
|
||||
"images": [],
|
||||
"extract_diag": {"choices_len": 0},
|
||||
"provider_response_redacted": '{"choices":[]}',
|
||||
}
|
||||
)
|
||||
assert ok is False
|
||||
assert "provider_json=" in msg
|
||||
assert att == []
|
||||
|
||||
|
||||
def test_legacy_turn_bundle_empty_ok_response_fails() -> None:
|
||||
ok, text, att = legacy_image_turn_bundle({"ok": True, "text": "", "images": []})
|
||||
assert ok is False
|
||||
assert att == []
|
||||
|
||||
|
||||
def test_extract_text_and_images_dashscope_output_wrapper() -> None:
|
||||
"""Native multimodal HTTP wraps ``choices`` under ``output`` (not top-level OpenAI shape)."""
|
||||
url = "https://dashscope-result-hz.oss-cn-hangzhou.aliyuncs.com/x.png?Expires=1"
|
||||
payload = {
|
||||
"status_code": 200,
|
||||
"request_id": "959afba6-544e-487e-b58a-6bd9fea97xxx",
|
||||
"code": "",
|
||||
"message": "",
|
||||
"output": {
|
||||
"text": None,
|
||||
"finish_reason": None,
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [{"image": url}],
|
||||
},
|
||||
}
|
||||
],
|
||||
"audio": None,
|
||||
},
|
||||
"usage": {
|
||||
"input_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
"image_count": 1,
|
||||
"width": 2048,
|
||||
"height": 2048,
|
||||
},
|
||||
}
|
||||
text, images = extract_text_and_images(payload)
|
||||
assert text == ""
|
||||
assert images == [url]
|
||||
assert dashscope_multimodal_http_ok(payload)[0] is True
|
||||
|
||||
|
||||
def test_extract_text_and_images_content_dict_not_list() -> None:
|
||||
"""Some gateways return a single object for ``message.content`` instead of an array."""
|
||||
url = "https://dashscope-result-hz.oss-cn-hangzhou.aliyuncs.com/out.png"
|
||||
text, images = extract_text_and_images(
|
||||
{
|
||||
"output": {
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": {"image": url},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
assert text == ""
|
||||
assert images == [url]
|
||||
|
||||
|
||||
def test_extract_text_and_images_messages_fallback() -> None:
|
||||
text, images = extract_text_and_images(
|
||||
{
|
||||
"output": {
|
||||
"messages": [
|
||||
{"role": "user", "content": "x"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{"image": "https://example.invalid/gen.png"}],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
assert images == ["https://example.invalid/gen.png"]
|
||||
|
||||
|
||||
def test_extract_text_and_images_typed_image_url_part() -> None:
|
||||
text, images = extract_text_and_images(
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "https://example.invalid/v.png"},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
assert images == ["https://example.invalid/v.png"]
|
||||
|
||||
|
||||
def test_dashscope_envelope_rejects_non_success_code() -> None:
|
||||
ok, msg = dashscope_multimodal_http_ok({"code": "InvalidParameter", "message": "bad"})
|
||||
assert ok is False
|
||||
assert "bad" in msg
|
||||
|
||||
|
||||
def test_extract_text_and_images_openai_top_level_unchanged() -> None:
|
||||
text, images = extract_text_and_images(
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": [
|
||||
{"type": "text", "text": "hi"},
|
||||
{"image": "https://example.invalid/a.jpg"},
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
assert "hi" in text
|
||||
assert images == ["https://example.invalid/a.jpg"]
|
||||
|
||||
|
||||
def test_parse_message_attachments_json_string_list() -> None:
|
||||
raw = json.dumps([{"type": "image_url", "url": "https://example.invalid/z.png"}])
|
||||
got = parse_message_attachments_json(raw)
|
||||
assert len(got) == 1 and got[0]["url"] == "https://example.invalid/z.png"
|
||||
|
||||
|
||||
class _FakeHistMsg:
|
||||
__slots__ = ("role", "attachments")
|
||||
|
||||
def __init__(self, role: str, attachments: object) -> None:
|
||||
self.role = role
|
||||
self.attachments = attachments
|
||||
|
||||
|
||||
class _FakeHistStore:
|
||||
def __init__(self, msgs: list[_FakeHistMsg]) -> None:
|
||||
self._msgs = msgs
|
||||
|
||||
def get_messages(self, session_id: str, limit: int = 200) -> list[_FakeHistMsg]:
|
||||
_ = session_id
|
||||
return self._msgs[-limit:] if len(self._msgs) > limit else list(self._msgs)
|
||||
|
||||
|
||||
def test_session_fallback_prefers_latest_assistant_images() -> None:
|
||||
"""Newest assistant row with images wins over older user uploads."""
|
||||
msgs = [
|
||||
_FakeHistMsg(
|
||||
"user",
|
||||
json.dumps([{"type": "image_url", "url": "https://example.invalid/old-user.png"}]),
|
||||
),
|
||||
_FakeHistMsg(
|
||||
"assistant",
|
||||
json.dumps([{"type": "image_url", "url": "https://example.invalid/from-assistant.png"}]),
|
||||
),
|
||||
_FakeHistMsg("user", "null"),
|
||||
]
|
||||
store = _FakeHistStore(msgs)
|
||||
imgs, src = collect_legacy_lane_images_with_session_fallback(
|
||||
store=store,
|
||||
session_id="s1",
|
||||
attachments=[],
|
||||
)
|
||||
assert src == "assistant_history"
|
||||
assert imgs == ["https://example.invalid/from-assistant.png"]
|
||||
|
||||
|
||||
def test_session_fallback_user_history_when_no_assistant_images() -> None:
|
||||
msgs = [
|
||||
_FakeHistMsg(
|
||||
"user",
|
||||
json.dumps([{"type": "image_url", "url": "https://example.invalid/only-user.png"}]),
|
||||
),
|
||||
_FakeHistMsg("assistant", "[]"),
|
||||
_FakeHistMsg("user", "null"),
|
||||
]
|
||||
store = _FakeHistStore(msgs)
|
||||
imgs, src = collect_legacy_lane_images_with_session_fallback(
|
||||
store=store,
|
||||
session_id="s1",
|
||||
attachments=[],
|
||||
)
|
||||
assert src == "user_history"
|
||||
assert imgs == ["https://example.invalid/only-user.png"]
|
||||
|
||||
|
||||
def test_session_fallback_disabled_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("AIA_IMAGE_SPECIALIST_SESSION_IMAGE_FALLBACK", "0")
|
||||
msgs = [
|
||||
_FakeHistMsg(
|
||||
"assistant",
|
||||
json.dumps([{"type": "image_url", "url": "https://example.invalid/a.png"}]),
|
||||
),
|
||||
_FakeHistMsg("user", "null"),
|
||||
]
|
||||
store = _FakeHistStore(msgs)
|
||||
imgs, src = collect_legacy_lane_images_with_session_fallback(
|
||||
store=store,
|
||||
session_id="s1",
|
||||
attachments=[],
|
||||
)
|
||||
assert src == ""
|
||||
assert imgs == []
|
||||
|
||||
|
||||
def test_session_fallback_current_attachments_skip_history(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("AIA_IMAGE_SPECIALIST_SESSION_IMAGE_FALLBACK", raising=False)
|
||||
msgs = [
|
||||
_FakeHistMsg(
|
||||
"assistant",
|
||||
json.dumps([{"type": "image_url", "url": "https://example.invalid/hist.png"}]),
|
||||
),
|
||||
_FakeHistMsg("user", "null"),
|
||||
]
|
||||
store = _FakeHistStore(msgs)
|
||||
imgs, src = collect_legacy_lane_images_with_session_fallback(
|
||||
store=store,
|
||||
session_id="s1",
|
||||
attachments=[{"type": "image_url", "url": "https://example.invalid/current.png"}],
|
||||
)
|
||||
assert src == "current"
|
||||
assert imgs == ["https://example.invalid/current.png"]
|
||||
|
|
@ -509,7 +509,7 @@ def test_gateway_comprehensive_mode_has_manager_final_pass(monkeypatch: pytest.M
|
|||
assert out.reply_text == "final_from_manager"
|
||||
|
||||
|
||||
def test_gateway_comprehensive_mode_dynamic_agent_dispatches_instruction_only(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_gateway_comprehensive_mode_dynamic_agent_falls_back_to_generalist(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
class Store:
|
||||
def get_setting(self, _k: str) -> str:
|
||||
return ""
|
||||
|
|
@ -538,21 +538,26 @@ def test_gateway_comprehensive_mode_dynamic_agent_dispatches_instruction_only(mo
|
|||
"runtime.gateway.get_manager_prompt_prebuild",
|
||||
lambda **kwargs: {
|
||||
"manager_context": "manager",
|
||||
"allowed_fixed": ("generalist", "ops", "memory", "image"),
|
||||
"allowed_fixed_quoted": '"generalist", "ops", "memory", "image"',
|
||||
"allowed_fixed": ("generalist", "ops", "memory"),
|
||||
"allowed_fixed_quoted": '"generalist", "ops", "memory"',
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr("runtime.gateway.build_ephemeral_executor", lambda *args, **kwargs: _Exec(model=object()))
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
def _run_agent_core(**kwargs):
|
||||
data = kwargs.get("data")
|
||||
captured["exec_text"] = getattr(getattr(data, "msg", None), "text", None)
|
||||
return SimpleNamespace(outcome=SimpleNamespace(final_text="dynamic_specialist_answer"))
|
||||
return SimpleNamespace(outcome=SimpleNamespace(final_text="generalist_answer", turn_uuid="t1"))
|
||||
|
||||
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core)
|
||||
|
||||
chosen: dict[str, str] = {}
|
||||
|
||||
def _factory(sid: str):
|
||||
chosen["sid"] = sid
|
||||
return _Exec(model=object())
|
||||
|
||||
gw = OclawGateway(store=Store())
|
||||
msg = StandardMessage(
|
||||
session_id="sid-dyn-1",
|
||||
|
|
@ -564,9 +569,11 @@ def test_gateway_comprehensive_mode_dynamic_agent_dispatches_instruction_only(mo
|
|||
attachments=[],
|
||||
metadata={"interaction_mode": "comprehensive"},
|
||||
)
|
||||
out = gw.handle_turn(msg=msg, lang="en", executor=_Exec(model=_ManagerModel()))
|
||||
out = gw.handle_turn(msg=msg, lang="en", executor=_Exec(model=_ManagerModel()), specialist_executor_factory=_factory)
|
||||
assert out.interaction_mode == "comprehensive"
|
||||
assert out.selected_specialist == "dyn:sql"
|
||||
assert out.selected_specialist == "generalist"
|
||||
assert chosen.get("sid") == "generalist"
|
||||
assert out.dispatch_reason == "dynamic_agent_disabled_fallback"
|
||||
assert captured.get("exec_text") == "Write a SQL query to compute daily active users."
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from runtime.hooks.gmail_watcher import start_gmail_watcher
|
||||
|
||||
|
||||
def test_start_gmail_watcher_top_level_hooks_disabled() -> None:
|
||||
r = start_gmail_watcher({"hooks": {"enabled": False, "internal": {"enabled": True}}})
|
||||
assert r.started is False and r.reason == "hooks not enabled"
|
||||
|
||||
|
||||
def test_start_gmail_watcher_internal_disabled() -> None:
|
||||
r = start_gmail_watcher({"hooks": {"internal": {"enabled": False}}})
|
||||
assert r.started is False and r.reason == "hooks not enabled"
|
||||
|
||||
|
||||
def test_start_gmail_watcher_no_account() -> None:
|
||||
r = start_gmail_watcher({"hooks": {"internal": {"enabled": True}, "gmail": {}}})
|
||||
assert r.started is False and r.reason == "no gmail account configured"
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from runtime.hooks.gmail_watcher import GmailWatcherResult
|
||||
from runtime.hooks.gmail_watcher_lifecycle import start_gmail_watcher_with_logs
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Log:
|
||||
infos: list[str] = field(default_factory=list)
|
||||
warns: list[str] = field(default_factory=list)
|
||||
errors: list[str] = field(default_factory=list)
|
||||
|
||||
def info(self, msg: str) -> None:
|
||||
self.infos.append(msg)
|
||||
|
||||
def warn(self, msg: str) -> None:
|
||||
self.warns.append(msg)
|
||||
|
||||
def error(self, msg: str) -> None:
|
||||
self.errors.append(msg)
|
||||
|
||||
|
||||
def test_gmail_lifecycle_logs_startup_success() -> None:
|
||||
log = _Log()
|
||||
start_gmail_watcher_with_logs(
|
||||
cfg={},
|
||||
log=log,
|
||||
starter=lambda _cfg: GmailWatcherResult(started=True),
|
||||
)
|
||||
assert log.infos == ["gmail watcher started"]
|
||||
assert not log.warns
|
||||
assert not log.errors
|
||||
|
||||
|
||||
def test_gmail_lifecycle_logs_actionable_reason() -> None:
|
||||
log = _Log()
|
||||
start_gmail_watcher_with_logs(
|
||||
cfg={},
|
||||
log=log,
|
||||
starter=lambda _cfg: GmailWatcherResult(started=False, reason="auth failed"),
|
||||
)
|
||||
assert log.warns == ["gmail watcher not started: auth failed"]
|
||||
|
||||
|
||||
def test_gmail_lifecycle_suppresses_expected_reason() -> None:
|
||||
log = _Log()
|
||||
start_gmail_watcher_with_logs(
|
||||
cfg={},
|
||||
log=log,
|
||||
starter=lambda _cfg: GmailWatcherResult(started=False, reason="hooks not enabled"),
|
||||
)
|
||||
assert not log.warns
|
||||
|
||||
|
||||
def test_gmail_lifecycle_supports_skip_callback_oclaw_env(monkeypatch) -> None:
|
||||
monkeypatch.setenv("OCLAW_SKIP_GMAIL_WATCHER", "1")
|
||||
monkeypatch.delenv("OPENCLAW_SKIP_GMAIL_WATCHER", raising=False)
|
||||
log = _Log()
|
||||
called = {"skip": 0}
|
||||
start_gmail_watcher_with_logs(
|
||||
cfg={},
|
||||
log=log,
|
||||
on_skipped=lambda: called.__setitem__("skip", called["skip"] + 1),
|
||||
starter=lambda _cfg: GmailWatcherResult(started=True),
|
||||
)
|
||||
assert called["skip"] == 1
|
||||
assert not log.infos
|
||||
|
||||
|
||||
def test_gmail_lifecycle_supports_skip_callback(monkeypatch) -> None:
|
||||
monkeypatch.delenv("OCLAW_SKIP_GMAIL_WATCHER", raising=False)
|
||||
monkeypatch.setenv("OPENCLAW_SKIP_GMAIL_WATCHER", "1")
|
||||
log = _Log()
|
||||
called = {"skip": 0}
|
||||
start_gmail_watcher_with_logs(
|
||||
cfg={},
|
||||
log=log,
|
||||
on_skipped=lambda: called.__setitem__("skip", called["skip"] + 1),
|
||||
starter=lambda _cfg: GmailWatcherResult(started=True),
|
||||
)
|
||||
assert called["skip"] == 1
|
||||
assert not log.infos
|
||||
|
||||
|
||||
def test_gmail_lifecycle_logs_startup_error() -> None:
|
||||
log = _Log()
|
||||
|
||||
def _raise(_cfg):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
start_gmail_watcher_with_logs(
|
||||
cfg={},
|
||||
log=log,
|
||||
starter=_raise,
|
||||
)
|
||||
assert log.errors == ["gmail watcher failed to start: boom"]
|
||||
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from runtime.hooks_runtime import _reset_hooks_runtime_state_for_test, initialize_hooks_runtime
|
||||
|
||||
|
||||
def test_initialize_hooks_runtime_invokes_gmail_lifecycle(monkeypatch, tmp_path: Path) -> None:
|
||||
_reset_hooks_runtime_state_for_test()
|
||||
calls: list[object] = []
|
||||
|
||||
def _cap(*, cfg, log, on_skipped=None, starter=None):
|
||||
calls.append(cfg)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"runtime.hooks.gmail_watcher_lifecycle.start_gmail_watcher_with_logs",
|
||||
_cap,
|
||||
)
|
||||
bundled = tmp_path / "bundled"
|
||||
bundled.mkdir()
|
||||
monkeypatch.setattr("runtime.hooks_runtime.runtime_hooks_bundled_root", lambda: str(bundled))
|
||||
monkeypatch.setattr("runtime.hooks.merge_skill_hook_dirs.discover_workspace_skill_manifests", lambda: ())
|
||||
|
||||
ws = tmp_path / "ws"
|
||||
ws.mkdir()
|
||||
cfg = {"hooks": {"internal": {"enabled": True}}}
|
||||
initialize_hooks_runtime(cfg=cfg, workspace_dir=str(ws))
|
||||
assert len(calls) == 1
|
||||
assert isinstance(calls[0], dict)
|
||||
|
|
@ -106,15 +106,15 @@ def test_interaction_mode_normalization_supports_legacy_values() -> None:
|
|||
|
||||
def test_requested_specialist_normalization_defaults_to_generalist() -> None:
|
||||
assert normalize_requested_specialist("ops") == "ops"
|
||||
# "stock" is a dynamic specialist discovered from runtime workspaces.
|
||||
assert normalize_requested_specialist("stock") == "stock"
|
||||
assert normalize_requested_specialist("image") == "image"
|
||||
# Unknown ids fall back to generalist.
|
||||
# Removed specialists map to generalist.
|
||||
assert normalize_requested_specialist("stock") == "generalist"
|
||||
assert normalize_requested_specialist("image") == "generalist"
|
||||
assert normalize_requested_specialist("video") == "generalist"
|
||||
assert normalize_requested_specialist("memory") == "memory"
|
||||
assert normalize_requested_specialist("unknown") == "generalist"
|
||||
|
||||
|
||||
def test_router_video_expert_sync_despite_long_attachments() -> None:
|
||||
def test_router_removed_video_expert_uses_normal_routing() -> None:
|
||||
long_text = "x" * 150
|
||||
msg = StandardMessage(
|
||||
session_id="s1",
|
||||
|
|
@ -127,9 +127,8 @@ def test_router_video_expert_sync_despite_long_attachments() -> None:
|
|||
metadata={"interaction_mode": "expert", "selected_specialist": "video"},
|
||||
)
|
||||
d = decide_route(msg)
|
||||
assert d.mode == "sync_direct"
|
||||
assert d.reason == "video_expert_legacy_lane"
|
||||
assert d.requested_specialist == "video"
|
||||
assert d.requested_specialist == "generalist"
|
||||
assert d.reason != "video_expert_legacy_lane"
|
||||
|
||||
|
||||
def test_router_carries_interaction_mode_and_requested_specialist() -> None:
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ def test_skill_market_install_tool_provider_arg_overrides_setting(monkeypatch, t
|
|||
tool = skill_market_install_tool()
|
||||
result = tool.handler({"slug": "demo", "provider": "cocoloop", "version": "latest"})
|
||||
assert bool(result.get("ok")) is True
|
||||
assert captured["provider"] == "cocoloop"
|
||||
# CocoLoop market removed; provider args normalize to clawhub.
|
||||
assert captured["provider"] == "clawhub"
|
||||
assert captured["skills_root"].replace("\\", "/").endswith("/skills/_workspace")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from runtime import skills_market
|
||||
|
||||
|
||||
def test_get_market_adapter_clawhub_default() -> None:
|
||||
a = skills_market.get_market_adapter("clawhub")
|
||||
assert a.provider == "clawhub"
|
||||
|
||||
|
||||
def test_get_market_adapter_cocoloop() -> None:
|
||||
a = skills_market.get_market_adapter("cocoloop")
|
||||
assert a.provider == "cocoloop"
|
||||
|
||||
|
||||
def test_get_market_adapter_cocoloop_alias() -> None:
|
||||
a = skills_market.get_market_adapter("cocoloop-cn")
|
||||
assert a.provider == "cocoloop"
|
||||
|
||||
|
||||
def test_cocoloop_resolve_archive_url(monkeypatch) -> None:
|
||||
def _fake_detail(slug: str) -> dict: # noqa: ANN001
|
||||
return {
|
||||
"source": "cocoloop",
|
||||
"slug": slug,
|
||||
"latestVersion": "1.0.0",
|
||||
"archiveUrl": "https://dl.example/bss/skills/demo.zip",
|
||||
"versions": [{"version": "1.0.0", "archiveUrl": "https://dl.example/bss/skills/demo.zip"}],
|
||||
}
|
||||
|
||||
monkeypatch.setattr("runtime.skills_market.cocoloop_get_skill_detail", _fake_detail)
|
||||
a = skills_market.CocoloopMarketAdapter()
|
||||
url, ver = a.resolve_archive_url(slug="demo", version=None)
|
||||
assert url.endswith("demo.zip")
|
||||
assert ver == "1.0.0"
|
||||
|
|
@ -1,257 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from svc.llm.video_generation_client import (
|
||||
_effective_video_model_for_request,
|
||||
_i2v_first_frame_input_style,
|
||||
dashscope_api_root_from_base_url,
|
||||
legacy_video_assistant_body_with_placeholder,
|
||||
legacy_video_turn_bundle,
|
||||
materialize_video_output_attachments,
|
||||
send_video_generation_request,
|
||||
)
|
||||
|
||||
|
||||
def test_dashscope_api_root_strips_compatible_suffix() -> None:
|
||||
assert (
|
||||
dashscope_api_root_from_base_url("https://dashscope.aliyuncs.com/compatible-mode/v1")
|
||||
== "https://dashscope.aliyuncs.com"
|
||||
)
|
||||
|
||||
|
||||
def test_send_video_generation_missing_config() -> None:
|
||||
r = send_video_generation_request(prompt="hello", api_key="", base_url="", model="wan2.2-t2v-plus")
|
||||
assert r["ok"] is False
|
||||
assert "missing" in str(r.get("error") or "").lower()
|
||||
|
||||
|
||||
def test_send_video_generation_poll_succeeds() -> None:
|
||||
post_resp = MagicMock()
|
||||
post_resp.status_code = 200
|
||||
post_resp.json.return_value = {"output": {"task_id": "tid-1", "task_status": "PENDING"}}
|
||||
|
||||
get_pending = MagicMock()
|
||||
get_pending.status_code = 200
|
||||
get_pending.json.return_value = {"output": {"task_id": "tid-1", "task_status": "PENDING"}}
|
||||
|
||||
get_ok = MagicMock()
|
||||
get_ok.status_code = 200
|
||||
get_ok.json.return_value = {
|
||||
"output": {
|
||||
"task_id": "tid-1",
|
||||
"task_status": "SUCCEEDED",
|
||||
"video_url": "https://example.invalid/out.mp4",
|
||||
}
|
||||
}
|
||||
|
||||
clients: list[MagicMock] = []
|
||||
|
||||
def _client_factory(*_a, **_k):
|
||||
inst = MagicMock()
|
||||
inst.__enter__ = lambda *_x: inst
|
||||
inst.__exit__ = lambda *_x: None
|
||||
if len(clients) == 1:
|
||||
inst.get.side_effect = [get_pending, get_ok]
|
||||
clients.append(inst)
|
||||
return inst
|
||||
|
||||
with patch("svc.llm.video_generation_client.httpx.Client", side_effect=_client_factory):
|
||||
with patch(
|
||||
"svc.llm.video_generation_client.post_with_retry",
|
||||
return_value=post_resp,
|
||||
):
|
||||
with patch("svc.llm.video_generation_client.time.sleep"):
|
||||
r = send_video_generation_request(
|
||||
prompt="cat",
|
||||
model="wan2.2-t2v-plus",
|
||||
api_key="sk-test",
|
||||
base_url="https://dashscope.aliyuncs.com",
|
||||
)
|
||||
|
||||
assert r["ok"] is True
|
||||
assert r.get("video_urls") == ["https://example.invalid/out.mp4"]
|
||||
assert len(clients) == 2
|
||||
assert clients[1].get.call_count == 2
|
||||
|
||||
|
||||
def test_legacy_video_turn_bundle_ok_without_materialize() -> None:
|
||||
ok, text, att = legacy_video_turn_bundle(
|
||||
{"ok": True, "text": "done", "video_urls": ["https://example.invalid/x.mp4"]}
|
||||
)
|
||||
assert ok is True
|
||||
assert text == "done"
|
||||
assert len(att) >= 1
|
||||
assert att[0].get("type") == "video_ref"
|
||||
|
||||
|
||||
def test_legacy_video_placeholder_zh() -> None:
|
||||
produced = [{"type": "video_ref", "attachment_id": "a" * 64}]
|
||||
s = legacy_video_assistant_body_with_placeholder(lang="zh", body_text="", produced=produced)
|
||||
assert "视频" in s
|
||||
|
||||
|
||||
def test_materialize_video_output_attachments_empty() -> None:
|
||||
assert materialize_video_output_attachments([], max_videos=1) == []
|
||||
|
||||
|
||||
def test_effective_video_model_coerces_t2v_to_i2v_when_image() -> None:
|
||||
assert _effective_video_model_for_request("wan2.6-t2v-flash", img_url="https://x/a.png") == "wan2.6-i2v-flash"
|
||||
assert _effective_video_model_for_request("wan2.6-i2v-flash", img_url="https://x/a.png") == "wan2.6-i2v-flash"
|
||||
assert _effective_video_model_for_request("wan2.6-t2v-flash", img_url=None) == "wan2.6-t2v-flash"
|
||||
assert _effective_video_model_for_request("wan2.6-t2v-flash", img_url="") == "wan2.6-t2v-flash"
|
||||
assert _effective_video_model_for_request("wan2.7-t2v", img_url="https://x/a.png") == "wan2.7-i2v"
|
||||
|
||||
|
||||
def test_i2v_input_style_auto(monkeypatch) -> None:
|
||||
monkeypatch.delenv("AIA_VIDEO_I2V_INPUT_STYLE", raising=False)
|
||||
assert _i2v_first_frame_input_style(model_name="wan2.7-i2v") == "media"
|
||||
assert _i2v_first_frame_input_style(model_name="WAN2.7-i2v-plus") == "media"
|
||||
assert _i2v_first_frame_input_style(model_name="wan2.6-i2v-flash") == "img_url"
|
||||
|
||||
|
||||
def test_send_video_coerces_payload_model_when_t2v_plus_img(monkeypatch) -> None:
|
||||
monkeypatch.delenv("AIA_VIDEO_EXPERT_I2V_MODEL", raising=False)
|
||||
monkeypatch.delenv("AIA_VIDEO_EXPERT_DISABLE_I2V_MODEL_COERCION", raising=False)
|
||||
post_resp = MagicMock()
|
||||
post_resp.status_code = 200
|
||||
post_resp.json.return_value = {
|
||||
"output": {
|
||||
"task_id": "tid-i2v",
|
||||
"task_status": "SUCCEEDED",
|
||||
"video_url": "https://example.invalid/i2v.mp4",
|
||||
}
|
||||
}
|
||||
inst = MagicMock()
|
||||
inst.__enter__ = lambda *_x: inst
|
||||
inst.__exit__ = lambda *_x: None
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def _capture_post(_client, **kw):
|
||||
p = kw.get("payload")
|
||||
if isinstance(p, dict):
|
||||
captured.clear()
|
||||
captured.update(p)
|
||||
return post_resp
|
||||
|
||||
with patch("svc.llm.video_generation_client.httpx.Client", return_value=inst):
|
||||
with patch("svc.llm.video_generation_client.post_with_retry", side_effect=_capture_post):
|
||||
r = send_video_generation_request(
|
||||
prompt="motion",
|
||||
model="wan2.6-t2v-flash",
|
||||
api_key="k",
|
||||
base_url="https://dashscope.aliyuncs.com",
|
||||
img_url="https://example.invalid/frame.png",
|
||||
)
|
||||
assert r["ok"] is True
|
||||
assert captured.get("model") == "wan2.6-i2v-flash"
|
||||
|
||||
|
||||
def test_send_video_includes_img_url_in_payload() -> None:
|
||||
post_resp = MagicMock()
|
||||
post_resp.status_code = 200
|
||||
post_resp.json.return_value = {
|
||||
"output": {
|
||||
"task_id": "tid-i2v",
|
||||
"task_status": "SUCCEEDED",
|
||||
"video_url": "https://example.invalid/i2v.mp4",
|
||||
}
|
||||
}
|
||||
inst = MagicMock()
|
||||
inst.__enter__ = lambda *_x: inst
|
||||
inst.__exit__ = lambda *_x: None
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def _capture_post(_client, **kw):
|
||||
p = kw.get("payload")
|
||||
if isinstance(p, dict):
|
||||
captured.clear()
|
||||
captured.update(p)
|
||||
return post_resp
|
||||
|
||||
with patch("svc.llm.video_generation_client.httpx.Client", return_value=inst):
|
||||
with patch("svc.llm.video_generation_client.post_with_retry", side_effect=_capture_post):
|
||||
r = send_video_generation_request(
|
||||
prompt="motion",
|
||||
model="wan2.6-i2v-flash",
|
||||
api_key="k",
|
||||
base_url="https://dashscope.aliyuncs.com",
|
||||
img_url="https://example.invalid/frame.png",
|
||||
)
|
||||
assert r["ok"] is True
|
||||
inp = captured.get("input")
|
||||
assert isinstance(inp, dict)
|
||||
assert inp.get("img_url") == "https://example.invalid/frame.png"
|
||||
assert inp.get("media") is None
|
||||
|
||||
|
||||
def test_send_video_wan27_uses_media_first_frame(monkeypatch) -> None:
|
||||
monkeypatch.delenv("AIA_VIDEO_I2V_INPUT_STYLE", raising=False)
|
||||
post_resp = MagicMock()
|
||||
post_resp.status_code = 200
|
||||
post_resp.json.return_value = {
|
||||
"output": {
|
||||
"task_id": "tid-i2v",
|
||||
"task_status": "SUCCEEDED",
|
||||
"video_url": "https://example.invalid/i2v.mp4",
|
||||
}
|
||||
}
|
||||
inst = MagicMock()
|
||||
inst.__enter__ = lambda *_x: inst
|
||||
inst.__exit__ = lambda *_x: None
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def _capture_post(_client, **kw):
|
||||
p = kw.get("payload")
|
||||
if isinstance(p, dict):
|
||||
captured.clear()
|
||||
captured.update(p)
|
||||
return post_resp
|
||||
|
||||
u = "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250925/wpimhv/rap.png"
|
||||
with patch("svc.llm.video_generation_client.httpx.Client", return_value=inst):
|
||||
with patch("svc.llm.video_generation_client.post_with_retry", side_effect=_capture_post):
|
||||
r = send_video_generation_request(
|
||||
prompt="你好",
|
||||
model="wan2.7-i2v",
|
||||
api_key="k",
|
||||
base_url="https://dashscope.aliyuncs.com",
|
||||
img_url=u,
|
||||
)
|
||||
assert r["ok"] is True
|
||||
inp = captured.get("input")
|
||||
assert isinstance(inp, dict)
|
||||
assert inp.get("img_url") is None
|
||||
media = inp.get("media")
|
||||
assert isinstance(media, list) and len(media) >= 1
|
||||
assert media[0].get("type") == "first_frame"
|
||||
assert media[0].get("url") == u
|
||||
|
||||
|
||||
def test_send_immediate_succeeded_on_submit() -> None:
|
||||
post_resp = MagicMock()
|
||||
post_resp.status_code = 200
|
||||
post_resp.json.return_value = {
|
||||
"output": {
|
||||
"task_id": "tid-x",
|
||||
"task_status": "SUCCEEDED",
|
||||
"video_url": "https://example.invalid/immediate.mp4",
|
||||
}
|
||||
}
|
||||
inst = MagicMock()
|
||||
inst.__enter__ = lambda *_x: inst
|
||||
inst.__exit__ = lambda *_x: None
|
||||
with patch("svc.llm.video_generation_client.httpx.Client", return_value=inst):
|
||||
with patch(
|
||||
"svc.llm.video_generation_client.post_with_retry",
|
||||
return_value=post_resp,
|
||||
):
|
||||
r = send_video_generation_request(
|
||||
prompt="x",
|
||||
model="m",
|
||||
api_key="k",
|
||||
base_url="https://dashscope.aliyuncs.com",
|
||||
)
|
||||
assert r["ok"] is True
|
||||
assert r["video_urls"] == ["https://example.invalid/immediate.mp4"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue