mirror of
https://github.com/TencentCloud/Octop.git
synced 2026-10-13 05:09:39 +08:00
* feat: workspace backend sandboxing, execute-env injection, and ephemeral image refs Workspace file-I/O hardening across backend and dashboard: - Host directory sandboxing via root_dir allow-list and denied prefixes - Execute-environment defaults injected into harness backend specs - Ephemeral workspace image rematerialization for model calls - Attachment hint expansion and inbound store improvements - Portable memory backend and skill workspace catalog updates - Dashboard UI for agent backend fields and root-dir selection - Bump harness-agent 0.9.23 / harness-memory 0.9.7; docs + install notes * feat: add user invitation codes with admin management and redeem flow - Add user_invites table (migration 009) plus SQLite/Postgres reconcile paths - Add InviteRepo and InviteService for create/revoke/list/redeem - Expose public validate/redeem endpoints and admin create/revoke/list routers - Extract default-agent bootstrap into octop.infra.agents.default_agent - Add invite error codes and en/zh i18n strings - Dashboard: invite drawer, login redeem UI, users list, and locales * fix: add Windows cross-platform guards for pre-existing sandbox/rootfs tests - Guard POSIX/container-only tests (rootfs-absolute /.octop/... workspace paths) with posix_only in test_thread_artifacts.py and test_agent_manager.py - Normalize the rendered workspace path assertion in test_octop_builtin_skills.py (forward slashes on Windows, not str(tmp_path) backslashes) - Canonicalize ws_dir before the prefix check in attachment hint/path tests (tempfile may yield an 8.3 short path on Windows while the resolved attachment path uses the long form) These failures predate the user-invites commit and are unrelated to it; the product path logic is correct. --------- Co-authored-by: jubaoliang <jubaoliang@tencent.com>
146 lines
4.7 KiB
Python
146 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from octop.infra.knowledge import embed
|
|
|
|
|
|
def test_remote_embedding_routes_to_provider_embeddings_endpoint(monkeypatch) -> None:
|
|
provider = SimpleNamespace(base_url="https://example.test/v1/", api_key="secret")
|
|
services = SimpleNamespace(
|
|
settings_repo=SimpleNamespace(
|
|
get=lambda key: {
|
|
"knowledge_embedding_backend": "remote",
|
|
"knowledge_embedding_model": "embed-1",
|
|
"knowledge_embedding_provider_id": "7",
|
|
}.get(key)
|
|
),
|
|
provider_repo=SimpleNamespace(
|
|
get=lambda provider_id: provider if provider_id == 7 else None
|
|
),
|
|
)
|
|
captured = {}
|
|
|
|
class Response:
|
|
def raise_for_status(self):
|
|
return None
|
|
|
|
def json(self):
|
|
return {"data": [{"embedding": [1.0, 2.0]}]}
|
|
|
|
class Client:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_args):
|
|
return None
|
|
|
|
def post(self, url, *, headers, json):
|
|
captured.update(url=url, headers=headers, json=json)
|
|
return Response()
|
|
|
|
monkeypatch.setattr(embed.httpx, "Client", lambda **_kwargs: Client())
|
|
|
|
assert embed.embed_knowledge_texts(services, ["hello"]) == [[1.0, 2.0]]
|
|
assert captured["url"] == "https://example.test/v1/embeddings"
|
|
assert captured["json"] == {"model": "embed-1", "input": ["hello"]}
|
|
assert captured["headers"]["Authorization"] == "Bearer secret"
|
|
|
|
|
|
def test_remote_embedding_merges_provider_extra_headers(monkeypatch) -> None:
|
|
provider = SimpleNamespace(
|
|
base_url="https://example.test/v1/",
|
|
api_key="secret",
|
|
extra_json='{"headers": {"X-Custom": "1"}}',
|
|
)
|
|
services = SimpleNamespace(
|
|
settings_repo=SimpleNamespace(
|
|
get=lambda key: {
|
|
"knowledge_embedding_backend": "remote",
|
|
"knowledge_embedding_model": "embed-1",
|
|
"knowledge_embedding_provider_id": "7",
|
|
}.get(key)
|
|
),
|
|
provider_repo=SimpleNamespace(
|
|
get=lambda provider_id: provider if provider_id == 7 else None
|
|
),
|
|
)
|
|
captured = {}
|
|
|
|
class Response:
|
|
def raise_for_status(self):
|
|
return None
|
|
|
|
def json(self):
|
|
return {"data": [{"embedding": [0.5]}]}
|
|
|
|
class Client:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_args):
|
|
return None
|
|
|
|
def post(self, url, *, headers, json):
|
|
captured.update(url=url, headers=headers, json=json)
|
|
return Response()
|
|
|
|
monkeypatch.setattr(embed.httpx, "Client", lambda **_kwargs: Client())
|
|
|
|
assert embed.embed_knowledge_texts(services, ["hello"]) == [[0.5]]
|
|
assert captured["headers"]["Authorization"] == "Bearer secret"
|
|
assert captured["headers"]["X-Custom"] == "1"
|
|
|
|
|
|
def test_remote_embedding_batches_large_input(monkeypatch) -> None:
|
|
provider = SimpleNamespace(base_url="https://example.test/v1/", api_key="secret")
|
|
services = SimpleNamespace(
|
|
settings_repo=SimpleNamespace(
|
|
get=lambda key: {
|
|
"knowledge_embedding_backend": "remote",
|
|
"knowledge_embedding_model": "embed-1",
|
|
"knowledge_embedding_provider_id": "7",
|
|
}.get(key)
|
|
),
|
|
provider_repo=SimpleNamespace(
|
|
get=lambda provider_id: provider if provider_id == 7 else None
|
|
),
|
|
)
|
|
calls: list[dict] = []
|
|
|
|
class Response:
|
|
def __init__(self, batch: list[str]) -> None:
|
|
# One embedding per input, tagged with the input's global position.
|
|
self._data = [{"embedding": [float(t.split("-")[1])]} for t in batch]
|
|
|
|
def raise_for_status(self):
|
|
return None
|
|
|
|
def json(self):
|
|
return {"data": self._data}
|
|
|
|
class Client:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_args):
|
|
return None
|
|
|
|
def post(self, url, *, headers, json):
|
|
calls.append({"url": url, "json": json})
|
|
return Response(json["input"])
|
|
|
|
monkeypatch.setattr(embed.httpx, "Client", lambda **_kwargs: Client())
|
|
|
|
texts = [f"chunk-{i}" for i in range(45)]
|
|
result = embed.embed_knowledge_texts(services, texts)
|
|
|
|
# AssertiveProviders cap at 20 -> 45 inputs => 3 requests (20/20/5).
|
|
assert len(calls) == 3
|
|
assert calls[0]["json"]["input"] == texts[0:20]
|
|
assert calls[1]["json"]["input"] == texts[20:40]
|
|
assert calls[2]["json"]["input"] == texts[40:45]
|
|
# Merged vectors stay aligned with input order.
|
|
assert len(result) == 45
|
|
assert result == [[float(i)] for i in range(45)]
|