test: rename SearchSpace -> Workspace across tests + fixtures (Phase 2 Wave F)

Apply the same rename to surfsense_backend/tests: workspace_id fields/vars,
Workspace* classes/schemas, table name searchspaces -> workspaces in raw SQL,
and the API URL spellings -> /workspaces. Preserves the carve-out wire literals
tests assert (Celery task names, OTel key "search_space.id").
This commit is contained in:
CREDO23 2026-06-26 18:36:46 +02:00
parent 56826a63bc
commit ca9bd28934
124 changed files with 1269 additions and 1269 deletions

View file

@ -94,7 +94,7 @@ def fake_session_factory():
class TestActionLogMiddlewareDisabled:
@pytest.mark.asyncio
async def test_no_op_when_flag_off(self, patch_get_flags) -> None:
mw = ActionLogMiddleware(thread_id=1, search_space_id=1, user_id=None)
mw = ActionLogMiddleware(thread_id=1, workspace_id=1, user_id=None)
request = _FakeRequest(
tool_call={
"name": "make_widget",
@ -110,7 +110,7 @@ class TestActionLogMiddlewareDisabled:
@pytest.mark.asyncio
async def test_no_op_when_thread_id_none(self, patch_get_flags) -> None:
mw = ActionLogMiddleware(thread_id=None, search_space_id=1, user_id=None)
mw = ActionLogMiddleware(thread_id=None, workspace_id=1, user_id=None)
request = _FakeRequest(
tool_call={"name": "make_widget", "args": {}, "id": "tc1"}
)
@ -126,7 +126,7 @@ class TestActionLogMiddlewarePersistence:
self, patch_get_flags, fake_session_factory
) -> None:
captured, factory = fake_session_factory
mw = ActionLogMiddleware(thread_id=42, search_space_id=7, user_id="u1")
mw = ActionLogMiddleware(thread_id=42, workspace_id=7, user_id="u1")
request = _FakeRequest(
tool_call={
"name": "make_widget",
@ -150,7 +150,7 @@ class TestActionLogMiddlewarePersistence:
assert len(captured["rows"]) == 1
row = captured["rows"][0]
assert row.thread_id == 42
assert row.search_space_id == 7
assert row.workspace_id == 7
assert row.user_id == "u1"
assert row.tool_name == "make_widget"
assert row.args == {"color": "red", "size": 3}
@ -170,7 +170,7 @@ class TestActionLogMiddlewarePersistence:
) -> None:
"""``chat_turn_id`` falls back to NULL when ``runtime.config`` is absent."""
captured, factory = fake_session_factory
mw = ActionLogMiddleware(thread_id=1, search_space_id=1, user_id=None)
mw = ActionLogMiddleware(thread_id=1, workspace_id=1, user_id=None)
request = _FakeRequest(
tool_call={"name": "make_widget", "args": {}, "id": "tc-1"},
runtime=None,
@ -190,7 +190,7 @@ class TestActionLogMiddlewarePersistence:
self, patch_get_flags, fake_session_factory
) -> None:
captured, factory = fake_session_factory
mw = ActionLogMiddleware(thread_id=42, search_space_id=7, user_id="u1")
mw = ActionLogMiddleware(thread_id=42, workspace_id=7, user_id="u1")
request = _FakeRequest(
tool_call={"name": "make_widget", "args": {"color": "red"}, "id": "tc1"}
)
@ -214,7 +214,7 @@ class TestActionLogMiddlewarePersistence:
self, patch_get_flags
) -> None:
"""Even if the DB write blows up, the tool's result must reach the model."""
mw = ActionLogMiddleware(thread_id=1, search_space_id=1, user_id=None)
mw = ActionLogMiddleware(thread_id=1, workspace_id=1, user_id=None)
request = _FakeRequest(
tool_call={"name": "make_widget", "args": {}, "id": "tc1"}
)
@ -250,7 +250,7 @@ class TestReverseDescriptor:
)
mw = ActionLogMiddleware(
thread_id=1,
search_space_id=1,
workspace_id=1,
user_id="u",
tool_definitions={"make_widget": tool_def},
)
@ -296,7 +296,7 @@ class TestReverseDescriptor:
)
mw = ActionLogMiddleware(
thread_id=1,
search_space_id=1,
workspace_id=1,
user_id=None,
tool_definitions={"make_widget": tool_def},
)
@ -321,7 +321,7 @@ class TestReverseDescriptor:
self, patch_get_flags, fake_session_factory
) -> None:
captured, factory = fake_session_factory
mw = ActionLogMiddleware(thread_id=1, search_space_id=1, user_id=None)
mw = ActionLogMiddleware(thread_id=1, workspace_id=1, user_id=None)
request = _FakeRequest(
tool_call={"name": "unknown_tool", "args": {}, "id": "tc1"}
)
@ -343,7 +343,7 @@ class TestActionLogDispatch:
self, patch_get_flags, fake_session_factory
) -> None:
_captured, factory = fake_session_factory
mw = ActionLogMiddleware(thread_id=42, search_space_id=7, user_id="u1")
mw = ActionLogMiddleware(thread_id=42, workspace_id=7, user_id="u1")
request = _FakeRequest(
tool_call={
"name": "make_widget",
@ -383,7 +383,7 @@ class TestActionLogDispatch:
@pytest.mark.asyncio
async def test_no_dispatch_when_persistence_fails(self, patch_get_flags) -> None:
"""If commit fails the dispatch is suppressed (no row to surface)."""
mw = ActionLogMiddleware(thread_id=1, search_space_id=1, user_id=None)
mw = ActionLogMiddleware(thread_id=1, workspace_id=1, user_id=None)
request = _FakeRequest(
tool_call={"name": "make_widget", "args": {}, "id": "tc1"}
)
@ -411,7 +411,7 @@ class TestArgsTruncation:
self, patch_get_flags, fake_session_factory
) -> None:
captured, factory = fake_session_factory
mw = ActionLogMiddleware(thread_id=1, search_space_id=1, user_id=None)
mw = ActionLogMiddleware(thread_id=1, workspace_id=1, user_id=None)
# Build a > 32KB string so the persisted payload triggers the truncation path.
huge = "x" * (40 * 1024)
request = _FakeRequest(

View file

@ -99,7 +99,7 @@ class TestResolveMentions:
session.execute = AsyncMock()
result = await resolve_mentions(
session,
search_space_id=1,
workspace_id=1,
mentioned_documents=None,
)
assert isinstance(result, ResolvedMentionSet)
@ -134,7 +134,7 @@ class TestResolveMentions:
out = await resolve_mentions(
session,
search_space_id=5,
workspace_id=5,
mentioned_documents=[chip],
)
assert len(out.mentions) == 1
@ -170,7 +170,7 @@ class TestResolveMentions:
out = await resolve_mentions(
session,
search_space_id=3,
workspace_id=3,
mentioned_documents=[chip],
)
assert len(out.mentions) == 1
@ -201,7 +201,7 @@ class TestResolveMentions:
out = await resolve_mentions(
session,
search_space_id=1,
workspace_id=1,
mentioned_documents=[chip],
)
assert out.mentions == []
@ -238,7 +238,7 @@ class TestResolveMentions:
out = await resolve_mentions(
session,
search_space_id=1,
workspace_id=1,
mentioned_documents=[chip_short, chip_long],
)
tokens = [tok for tok, _ in out.token_to_path]
@ -265,7 +265,7 @@ class TestResolveMentions:
out = await resolve_mentions(
session,
search_space_id=2,
workspace_id=2,
mentioned_documents=None,
mentioned_document_ids=[7],
)

View file

@ -149,7 +149,7 @@ class TestVirtualPathToDoc:
document = await virtual_path_to_doc(
session,
search_space_id=5,
workspace_id=5,
virtual_path=f"{DOCUMENTS_ROOT}/{encoded_basename}",
)
assert document is target_doc
@ -169,7 +169,7 @@ class TestVirtualPathToDoc:
document = await virtual_path_to_doc(
session,
search_space_id=5,
workspace_id=5,
virtual_path=f"{DOCUMENTS_ROOT}/Calendar_ Happy birthday!.xml",
)
assert document is None
@ -191,7 +191,7 @@ class TestVirtualPathToDoc:
document = await virtual_path_to_doc(
session,
search_space_id=5,
workspace_id=5,
virtual_path=f"{DOCUMENTS_ROOT}/Plain Note.xml",
)
assert document is target_doc
@ -217,7 +217,7 @@ class TestVirtualPathToDoc:
document = await virtual_path_to_doc(
session,
search_space_id=5,
workspace_id=5,
virtual_path=f"{DOCUMENTS_ROOT}/2025-W2.pdf.xml",
)
assert document is target_doc
@ -239,7 +239,7 @@ class TestVirtualPathToDoc:
document = await virtual_path_to_doc(
session,
search_space_id=5,
workspace_id=5,
virtual_path=f"{DOCUMENTS_ROOT}/2025-W2.pdf",
)
assert document is target_doc

View file

@ -30,7 +30,7 @@ class _DummyMiddleware(AgentMiddleware):
def _ctx() -> PluginContext:
return PluginContext.build(
search_space_id=1,
workspace_id=1,
user_id="u",
thread_visibility="PRIVATE", # type: ignore[arg-type]
llm=MagicMock(),
@ -165,12 +165,12 @@ class TestPluginContext:
def test_build_includes_required_fields(self) -> None:
llm = MagicMock()
ctx = PluginContext.build(
search_space_id=42,
workspace_id=42,
user_id="user-1",
thread_visibility="PRIVATE", # type: ignore[arg-type]
llm=llm,
)
assert ctx["search_space_id"] == 42
assert ctx["workspace_id"] == 42
assert ctx["user_id"] == "user-1"
assert ctx["llm"] is llm

View file

@ -11,7 +11,7 @@ from app.agents.chat.multi_agent_chat.main_agent.skills.backends import (
SKILLS_BUILTIN_PREFIX,
SKILLS_SPACE_PREFIX,
BuiltinSkillsBackend,
SearchSpaceSkillsBackend,
WorkspaceSkillsBackend,
build_skills_backend_factory,
default_skills_sources,
)
@ -176,7 +176,7 @@ class _FakeKBBackend:
return out
class TestSearchSpaceSkillsBackend:
class TestWorkspaceSkillsBackend:
def test_remaps_paths_when_listing(self) -> None:
listing = [
{"path": "/documents/_skills/policy", "is_dir": True},
@ -184,7 +184,7 @@ class TestSearchSpaceSkillsBackend:
{"path": "/documents/other-folder/x.md", "is_dir": False},
]
kb = _FakeKBBackend(listing=listing, file_contents={})
backend = SearchSpaceSkillsBackend(kb)
backend = WorkspaceSkillsBackend(kb)
infos = asyncio.run(backend.als_info("/"))
assert kb.last_ls_path == "/documents/_skills"
paths = [info["path"] for info in infos]
@ -200,7 +200,7 @@ class TestSearchSpaceSkillsBackend:
"/documents/_skills/policy/SKILL.md": b"---\nname: policy\n---\n",
},
)
backend = SearchSpaceSkillsBackend(kb)
backend = WorkspaceSkillsBackend(kb)
responses = asyncio.run(backend.adownload_files(["/policy/SKILL.md"]))
assert kb.last_download_paths == ["/documents/_skills/policy/SKILL.md"]
assert responses[0].path == "/policy/SKILL.md"
@ -208,7 +208,7 @@ class TestSearchSpaceSkillsBackend:
assert responses[0].content is not None
def test_sync_methods_raise_not_implemented(self) -> None:
backend = SearchSpaceSkillsBackend(_FakeKBBackend([], {}))
backend = WorkspaceSkillsBackend(_FakeKBBackend([], {}))
with pytest.raises(NotImplementedError):
backend.ls_info("/")
with pytest.raises(NotImplementedError):
@ -221,7 +221,7 @@ class TestSearchSpaceSkillsBackend:
],
file_contents={},
)
backend = SearchSpaceSkillsBackend(kb, kb_root="/skills_admin")
backend = WorkspaceSkillsBackend(kb, kb_root="/skills_admin")
infos = asyncio.run(backend.als_info("/"))
assert kb.last_ls_path == "/skills_admin"
assert infos[0]["path"] == "/x"

View file

@ -148,7 +148,7 @@ async def test_generate_resume_defaults_to_one_page_target(monkeypatch) -> None:
monkeypatch.setattr(resume_tool, "_compile_typst", lambda _source: b"pdf")
monkeypatch.setattr(resume_tool, "_count_pdf_pages", lambda _pdf: 1)
tool = resume_tool.create_generate_resume_tool(search_space_id=1, thread_id=1)
tool = resume_tool.create_generate_resume_tool(workspace_id=1, thread_id=1)
result = await _invoke(tool, {"user_info": "Jane Doe experience"})
assert result["status"] == "ready"
@ -178,7 +178,7 @@ async def test_generate_resume_compresses_when_over_limit(monkeypatch) -> None:
page_counts = iter([2, 1])
monkeypatch.setattr(resume_tool, "_count_pdf_pages", lambda _pdf: next(page_counts))
tool = resume_tool.create_generate_resume_tool(search_space_id=1, thread_id=1)
tool = resume_tool.create_generate_resume_tool(workspace_id=1, thread_id=1)
result = await _invoke(tool, {"user_info": "Jane Doe experience", "max_pages": 1})
assert result["status"] == "ready"
@ -213,7 +213,7 @@ async def test_generate_resume_returns_ready_when_target_not_met(monkeypatch) ->
page_counts = iter([3, 3, 2])
monkeypatch.setattr(resume_tool, "_count_pdf_pages", lambda _pdf: next(page_counts))
tool = resume_tool.create_generate_resume_tool(search_space_id=1, thread_id=1)
tool = resume_tool.create_generate_resume_tool(workspace_id=1, thread_id=1)
result = await _invoke(tool, {"user_info": "Jane Doe experience", "max_pages": 1})
assert result["status"] == "ready"
@ -246,7 +246,7 @@ async def test_generate_resume_fails_when_hard_limit_exceeded(monkeypatch) -> No
page_counts = iter([7, 6, 6])
monkeypatch.setattr(resume_tool, "_count_pdf_pages", lambda _pdf: next(page_counts))
tool = resume_tool.create_generate_resume_tool(search_space_id=1, thread_id=1)
tool = resume_tool.create_generate_resume_tool(workspace_id=1, thread_id=1)
result = await _invoke(tool, {"user_info": "Jane Doe experience", "max_pages": 1})
assert result["status"] == "failed"

View file

@ -1,10 +1,10 @@
"""Lock the runtime model-policy backstop in ``build_dependencies``.
Automations resolve their LLM from the *captured* ``chat_model_id`` snapshot (so
runs are insulated from later chat/search-space model changes), and the model
runs are insulated from later chat/workspace model changes), and the model
policy is re-checked at run time so a captured model that is no longer billable
fails the run clearly. When no snapshot is present, resolution falls back to the
live search space.
live workspace.
"""
from __future__ import annotations
@ -25,20 +25,20 @@ pytestmark = pytest.mark.unit
class _FakeSession:
"""Minimal async session whose ``get`` returns a preset search space."""
"""Minimal async session whose ``get`` returns a preset workspace."""
def __init__(self, search_space: Any) -> None:
self._search_space = search_space
def __init__(self, workspace: Any) -> None:
self._workspace = workspace
async def get(self, _model: Any, _pk: int) -> Any:
return self._search_space
return self._workspace
@pytest.fixture
def patched_side_effects(monkeypatch: pytest.MonkeyPatch):
"""Stub the connector setup + checkpointer so only policy/LLM logic runs."""
async def _fake_setup(_session, *, search_space_id):
async def _fake_setup(_session, *, workspace_id):
return (SimpleNamespace(name="connector"), "fc-key")
monkeypatch.setattr(deps_mod, "setup_connector_and_firecrawl", _fake_setup)
@ -48,35 +48,35 @@ def patched_side_effects(monkeypatch: pytest.MonkeyPatch):
async def test_build_dependencies_resolves_captured_chat_model_id(
monkeypatch: pytest.MonkeyPatch, patched_side_effects
) -> None:
"""The bundle loads with the *captured* ``chat_model_id``, not the live search space."""
"""The bundle loads with the *captured* ``chat_model_id``, not the live workspace."""
captured: dict[str, Any] = {}
async def _fake_load(_session, *, config_id, search_space_id):
async def _fake_load(_session, *, config_id, workspace_id):
captured["config_id"] = config_id
captured["search_space_id"] = search_space_id
captured["workspace_id"] = workspace_id
return (SimpleNamespace(name="llm"), SimpleNamespace(name="agent_config"), None)
monkeypatch.setattr(deps_mod, "load_llm_bundle", _fake_load)
# Captured path validates the explicit ids; passes for this test.
monkeypatch.setattr(deps_mod, "assert_models_billable", lambda **_kw: None)
# A different value on the live search space proves we ignore it when a
# A different value on the live workspace proves we ignore it when a
# snapshot is supplied.
monkeypatch.setattr(
deps_mod,
"assert_automation_models_billable",
lambda _ss: pytest.fail("search-space policy should not run on captured path"),
lambda _ss: pytest.fail("workspace policy should not run on captured path"),
)
search_space = SimpleNamespace(chat_model_id=-99)
workspace = SimpleNamespace(chat_model_id=-99)
result = await build_dependencies(
session=_FakeSession(search_space),
search_space_id=42,
session=_FakeSession(workspace),
workspace_id=42,
chat_model_id=-7,
image_gen_model_id=5,
vision_model_id=-1,
)
assert captured == {"config_id": -7, "search_space_id": 42}
assert captured == {"config_id": -7, "workspace_id": 42}
assert result.llm.name == "llm"
assert result.firecrawl_api_key == "fc-key"
@ -84,7 +84,7 @@ async def test_build_dependencies_resolves_captured_chat_model_id(
async def test_build_dependencies_validates_captured_ids(
monkeypatch: pytest.MonkeyPatch, patched_side_effects
) -> None:
"""The captured ids (not the search space) are what gets policy-checked."""
"""The captured ids (not the workspace) are what gets policy-checked."""
seen: dict[str, Any] = {}
def _capture(**kwargs):
@ -92,14 +92,14 @@ async def test_build_dependencies_validates_captured_ids(
monkeypatch.setattr(deps_mod, "assert_models_billable", _capture)
async def _fake_load(_session, *, config_id, search_space_id):
async def _fake_load(_session, *, config_id, workspace_id):
return (SimpleNamespace(name="llm"), SimpleNamespace(name="agent_config"), None)
monkeypatch.setattr(deps_mod, "load_llm_bundle", _fake_load)
await build_dependencies(
session=_FakeSession(SimpleNamespace(chat_model_id=0)),
search_space_id=42,
workspace_id=42,
chat_model_id=-7,
image_gen_model_id=5,
vision_model_id=-1,
@ -132,20 +132,20 @@ async def test_build_dependencies_raises_on_captured_policy_violation(
with pytest.raises(DependencyError):
await build_dependencies(
session=_FakeSession(SimpleNamespace(chat_model_id=-7)),
search_space_id=42,
workspace_id=42,
chat_model_id=-7,
image_gen_model_id=-2,
vision_model_id=-1,
)
async def test_build_dependencies_falls_back_to_search_space(
async def test_build_dependencies_falls_back_to_workspace(
monkeypatch: pytest.MonkeyPatch, patched_side_effects
) -> None:
"""With no captured snapshot, resolve + validate the live search space."""
"""With no captured snapshot, resolve + validate the live workspace."""
captured: dict[str, Any] = {}
async def _fake_load(_session, *, config_id, search_space_id):
async def _fake_load(_session, *, config_id, workspace_id):
captured["config_id"] = config_id
return (SimpleNamespace(name="llm"), SimpleNamespace(name="agent_config"), None)
@ -157,18 +157,18 @@ async def test_build_dependencies_falls_back_to_search_space(
lambda **_kw: pytest.fail("captured policy should not run on fallback path"),
)
search_space = SimpleNamespace(chat_model_id=-7)
workspace = SimpleNamespace(chat_model_id=-7)
result = await build_dependencies(
session=_FakeSession(search_space), search_space_id=42
session=_FakeSession(workspace), workspace_id=42
)
assert captured == {"config_id": -7}
assert result.llm.name == "llm"
async def test_build_dependencies_raises_when_search_space_missing(
async def test_build_dependencies_raises_when_workspace_missing(
patched_side_effects,
) -> None:
"""A missing search space (fallback path) surfaces as a ``DependencyError``."""
"""A missing workspace (fallback path) surfaces as a ``DependencyError``."""
with pytest.raises(DependencyError):
await build_dependencies(session=_FakeSession(None), search_space_id=999)
await build_dependencies(session=_FakeSession(None), workspace_id=999)

View file

@ -34,7 +34,7 @@ def _action_context() -> ActionContext:
session=cast(AsyncSession, None),
run_id=1,
step_id="s1",
search_space_id=1,
workspace_id=1,
creator_user_id=None,
)

View file

@ -1,6 +1,6 @@
"""Lock that the executor propagates the captured model snapshot into the
``ActionContext``, so runs resolve their own model (insulated from chat /
search-space changes) and not the live search space.
workspace changes) and not the live workspace.
"""
from __future__ import annotations
@ -21,7 +21,7 @@ pytestmark = pytest.mark.unit
def _run() -> SimpleNamespace:
return SimpleNamespace(
id=1,
automation=SimpleNamespace(search_space_id=42, created_by_user_id="u-1"),
automation=SimpleNamespace(workspace_id=42, created_by_user_id="u-1"),
)
@ -39,7 +39,7 @@ def test_build_action_ctx_propagates_captured_models() -> None:
models,
)
assert ctx.search_space_id == 42
assert ctx.workspace_id == 42
assert ctx.chat_model_id == -1
assert ctx.image_gen_model_id == 5
assert ctx.vision_model_id == -1

View file

@ -17,11 +17,11 @@ _VALID_DEFINITION = {
def test_automation_create_accepts_valid_minimal_payload() -> None:
"""Happy path: just search_space_id, name, and a valid definition.
"""Happy path: just workspace_id, name, and a valid definition.
Triggers default to ``[]`` so users can attach them later."""
payload = AutomationCreate.model_validate(
{
"search_space_id": 1,
"workspace_id": 1,
"name": "Daily digest",
"definition": _VALID_DEFINITION,
}
@ -39,7 +39,7 @@ def test_automation_create_cascades_validation_into_nested_definition() -> None:
with pytest.raises(ValidationError):
AutomationCreate.model_validate(
{
"search_space_id": 1,
"workspace_id": 1,
"name": "Bad",
"definition": {"name": "X", "plan": []}, # empty plan
}
@ -51,7 +51,7 @@ def test_automation_create_rejects_unknown_top_level_field() -> None:
with pytest.raises(ValidationError):
AutomationCreate.model_validate(
{
"search_space_id": 1,
"workspace_id": 1,
"name": "X",
"definition": _VALID_DEFINITION,
"owner": "tg", # not allowed
@ -64,7 +64,7 @@ def test_automation_create_rejects_empty_name() -> None:
with pytest.raises(ValidationError):
AutomationCreate.model_validate(
{
"search_space_id": 1,
"workspace_id": 1,
"name": "",
"definition": _VALID_DEFINITION,
}

View file

@ -1,6 +1,6 @@
"""Lock creation-time model-policy enforcement in ``AutomationService``.
Creation (REST + manual builder) rejects search spaces whose models aren't
Creation (REST + manual builder) rejects workspaces whose models aren't
billable for automations with HTTP 422, mirroring the runtime backstop. These
tests isolate the new ``_assert_models_billable`` / ``model_eligibility`` paths
without touching the DB commit.
@ -29,13 +29,13 @@ pytestmark = pytest.mark.unit
class _FakeSession:
def __init__(self, search_space: Any) -> None:
self._search_space = search_space
def __init__(self, workspace: Any) -> None:
self._workspace = workspace
self.added: list[Any] = []
self.commits = 0
async def get(self, _model: Any, _pk: int) -> Any:
return self._search_space
return self._workspace
def add(self, obj: Any) -> None:
self.added.append(obj)
@ -44,9 +44,9 @@ class _FakeSession:
self.commits += 1
def _service(search_space: Any) -> AutomationService:
def _service(workspace: Any) -> AutomationService:
return AutomationService(
session=_FakeSession(search_space),
session=_FakeSession(workspace),
auth=AuthContext.session(SimpleNamespace(id="u-1")),
)
@ -81,7 +81,7 @@ async def test_assert_models_billable_raises_422_on_violation(
async def test_assert_models_billable_raises_404_when_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A missing search space is a 404, not a policy error."""
"""A missing workspace is a 404, not a policy error."""
monkeypatch.setattr(
automation_mod, "assert_automation_models_billable", lambda _ss: None
)
@ -93,23 +93,23 @@ async def test_assert_models_billable_raises_404_when_missing(
assert exc_info.value.status_code == 404
async def test_assert_models_billable_returns_search_space_when_ok(
async def test_assert_models_billable_returns_workspace_when_ok(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""When the policy accepts, the loaded search space is returned for reuse."""
"""When the policy accepts, the loaded workspace is returned for reuse."""
monkeypatch.setattr(
automation_mod, "assert_automation_models_billable", lambda _ss: None
)
search_space = SimpleNamespace(chat_model_id=-1)
service = _service(search_space)
assert await service._assert_models_billable(1) is search_space
workspace = SimpleNamespace(chat_model_id=-1)
service = _service(workspace)
assert await service._assert_models_billable(1) is workspace
async def test_create_injects_captured_models_from_search_space(
async def test_create_injects_captured_models_from_workspace(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""create() snapshots the search space's model prefs onto the definition."""
"""create() snapshots the workspace's model prefs onto the definition."""
monkeypatch.setattr(
automation_mod, "assert_automation_models_billable", lambda _ss: None
)
@ -124,14 +124,14 @@ async def test_create_injects_captured_models_from_search_space(
monkeypatch.setattr(AutomationService, "_get_with_triggers_or_raise", _return_added)
search_space = SimpleNamespace(
workspace = SimpleNamespace(
chat_model_id=-1,
image_gen_model_id=5,
vision_model_id=-1,
)
service = _service(search_space)
service = _service(workspace)
payload = AutomationCreate(
search_space_id=1,
workspace_id=1,
name="A",
definition=_definition(),
)
@ -148,7 +148,7 @@ async def test_create_injects_captured_models_from_search_space(
async def test_create_treats_unset_prefs_as_auto_zero(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``None`` search-space prefs are captured as ``0`` (Auto) ids."""
"""``None`` workspace prefs are captured as ``0`` (Auto) ids."""
monkeypatch.setattr(
automation_mod, "assert_automation_models_billable", lambda _ss: None
)
@ -163,13 +163,13 @@ async def test_create_treats_unset_prefs_as_auto_zero(
monkeypatch.setattr(AutomationService, "_get_with_triggers_or_raise", _return_added)
search_space = SimpleNamespace(
workspace = SimpleNamespace(
chat_model_id=None,
image_gen_model_id=None,
vision_model_id=None,
)
service = _service(search_space)
payload = AutomationCreate(search_space_id=1, name="A", definition=_definition())
service = _service(workspace)
payload = AutomationCreate(workspace_id=1, name="A", definition=_definition())
automation = await service.create(payload)
@ -185,7 +185,7 @@ async def test_create_honors_selected_models_when_provided(
) -> None:
"""When the payload carries ``definition.models`` they are validated + kept.
The search-space snapshot path is bypassed entirely (no
The workspace snapshot path is bypassed entirely (no
``assert_automation_models_billable`` call).
"""
@ -217,7 +217,7 @@ async def test_create_honors_selected_models_when_provided(
service = _service(SimpleNamespace(chat_model_id=-99))
payload = AutomationCreate(
search_space_id=1,
workspace_id=1,
name="A",
definition=_definition(
models=AutomationModels(
@ -257,7 +257,7 @@ async def test_create_rejects_unbillable_selected_models(
service = _service(SimpleNamespace(chat_model_id=-3))
payload = AutomationCreate(
search_space_id=1,
workspace_id=1,
name="A",
definition=_definition(
models=AutomationModels(
@ -284,7 +284,7 @@ async def test_update_preserves_captured_models(
"vision_model_id": -1,
}
existing = SimpleNamespace(
search_space_id=1,
workspace_id=1,
definition={"name": "A", "plan": [], "models": captured},
version=3,
)
@ -315,7 +315,7 @@ async def test_update_honors_changed_models_when_valid(
) -> None:
"""A definition edit with a *changed* models block validates + keeps it."""
existing = SimpleNamespace(
search_space_id=1,
workspace_id=1,
definition={
"name": "A",
"plan": [],
@ -376,7 +376,7 @@ async def test_update_rejects_changed_unbillable_models(
) -> None:
"""A *changed* non-billable models block is rejected with HTTP 422."""
existing = SimpleNamespace(
search_space_id=1,
workspace_id=1,
definition={
"name": "A",
"plan": [],
@ -438,7 +438,7 @@ async def test_update_keeps_unchanged_models_without_revalidation(
"vision_model_id": -1,
}
existing = SimpleNamespace(
search_space_id=1,
workspace_id=1,
definition={"name": "A", "plan": [], "models": captured},
version=3,
)
@ -477,7 +477,7 @@ async def test_model_eligibility_authorizes_and_returns_payload(
authorized: dict[str, Any] = {}
async def _fake_check_permission(_session, _user, ss_id, permission, _msg):
authorized["search_space_id"] = ss_id
authorized["workspace_id"] = ss_id
authorized["permission"] = permission
monkeypatch.setattr(automation_mod, "check_permission", _fake_check_permission)
@ -488,8 +488,8 @@ async def test_model_eligibility_authorizes_and_returns_payload(
)
service = _service(SimpleNamespace(chat_model_id=-2))
result = await service.model_eligibility(search_space_id=5)
result = await service.model_eligibility(workspace_id=5)
assert result == {"allowed": False, "violations": [{"kind": "image"}]}
assert authorized["search_space_id"] == 5
assert authorized["workspace_id"] == 5
assert authorized["permission"] == "automations:read"

View file

@ -24,8 +24,8 @@ from app.automations.services.model_policy import (
pytestmark = pytest.mark.unit
def _search_space(*, llm: int | None, image: int | None, vision: int | None):
"""Minimal stand-in for the ``SearchSpace`` ORM row the policy reads."""
def _workspace(*, llm: int | None, image: int | None, vision: int | None):
"""Minimal stand-in for the ``Workspace`` ORM row the policy reads."""
return SimpleNamespace(
chat_model_id=llm,
image_gen_model_id=image,
@ -95,15 +95,15 @@ def test_unknown_global_id_is_blocked(kind: str, patched_globals) -> None:
def test_eligibility_all_billable(patched_globals) -> None:
"""Premium LLM + BYOK image + premium vision → allowed, no violations."""
search_space = _search_space(llm=-1, image=5, vision=-1)
result = get_automation_model_eligibility(search_space)
workspace = _workspace(llm=-1, image=5, vision=-1)
result = get_automation_model_eligibility(workspace)
assert result == {"allowed": True, "violations": []}
def test_eligibility_reports_each_violation(patched_globals) -> None:
"""A free LLM, Auto image, and free vision each produce a violation."""
search_space = _search_space(llm=-2, image=0, vision=-2)
result = get_automation_model_eligibility(search_space)
workspace = _workspace(llm=-2, image=0, vision=-2)
result = get_automation_model_eligibility(workspace)
assert result["allowed"] is False
kinds = {v["kind"] for v in result["violations"]}
@ -115,9 +115,9 @@ def test_eligibility_reports_each_violation(patched_globals) -> None:
def test_assert_raises_with_violations(patched_globals) -> None:
"""``assert_automation_models_billable`` raises when any slot is blocked."""
search_space = _search_space(llm=0, image=5, vision=-1)
workspace = _workspace(llm=0, image=5, vision=-1)
with pytest.raises(AutomationModelPolicyError) as exc_info:
assert_automation_models_billable(search_space)
assert_automation_models_billable(workspace)
assert len(exc_info.value.violations) == 1
assert exc_info.value.violations[0]["kind"] == "chat"
@ -125,8 +125,8 @@ def test_assert_raises_with_violations(patched_globals) -> None:
def test_assert_passes_when_all_billable(patched_globals) -> None:
"""No exception when every slot is premium or BYOK."""
search_space = _search_space(llm=3, image=-1, vision=4)
assert assert_automation_models_billable(search_space) is None
workspace = _workspace(llm=3, image=-1, vision=4)
assert assert_automation_models_billable(workspace) is None
# --- ID-based core (used by the runtime backstop against captured snapshots) ---
@ -170,9 +170,9 @@ def test_assert_models_billable_passes(patched_globals) -> None:
)
def test_search_space_wrapper_delegates_to_core(patched_globals) -> None:
"""The search-space wrapper produces the same result as the ID core."""
search_space = _search_space(llm=-2, image=0, vision=-2)
assert get_automation_model_eligibility(search_space) == get_model_eligibility(
def test_workspace_wrapper_delegates_to_core(patched_globals) -> None:
"""The workspace wrapper produces the same result as the ID core."""
workspace = _workspace(llm=-2, image=0, vision=-2)
assert get_automation_model_eligibility(workspace) == get_model_eligibility(
chat_model_id=-2, image_gen_model_id=0, vision_model_id=-2
)

View file

@ -25,7 +25,7 @@ def test_build_run_context_exposes_run_inputs_and_steps_namespaces() -> None:
automation_id=7,
automation_name="Weekly digest",
automation_version=3,
search_space_id=1,
workspace_id=1,
creator_id=creator,
trigger_id=11,
trigger_type="schedule",
@ -41,7 +41,7 @@ def test_build_run_context_exposes_run_inputs_and_steps_namespaces() -> None:
"automation_id": 7,
"automation_name": "Weekly digest",
"automation_version": 3,
"search_space_id": 1,
"workspace_id": 1,
"creator_id": creator,
"trigger_id": 11,
"trigger_type": "schedule",

View file

@ -21,9 +21,9 @@ def test_scalar_value_is_implicit_equality() -> None:
def test_multiple_fields_are_anded() -> None:
flt = {"document_type": "FILE", "search_space_id": 7}
assert matches(flt, {"document_type": "FILE", "search_space_id": 7}) is True
assert matches(flt, {"document_type": "FILE", "search_space_id": 9}) is False
flt = {"document_type": "FILE", "workspace_id": 7}
assert matches(flt, {"document_type": "FILE", "workspace_id": 7}) is True
assert matches(flt, {"document_type": "FILE", "workspace_id": 9}) is False
def test_gt_operator_compares_greater_than() -> None:
@ -97,12 +97,12 @@ def test_missing_field_never_matches_and_never_raises() -> None:
def test_logical_operators_compose_with_fields() -> None:
flt = {
"search_space_id": 7,
"workspace_id": 7,
"$or": [{"document_type": "FILE"}, {"document_type": "WEBPAGE"}],
}
assert matches(flt, {"search_space_id": 7, "document_type": "FILE"}) is True
assert matches(flt, {"search_space_id": 9, "document_type": "FILE"}) is False
assert matches(flt, {"search_space_id": 7, "document_type": "SLACK"}) is False
assert matches(flt, {"workspace_id": 7, "document_type": "FILE"}) is True
assert matches(flt, {"workspace_id": 9, "document_type": "FILE"}) is False
assert matches(flt, {"workspace_id": 7, "document_type": "SLACK"}) is False
def test_unknown_field_operator_raises_filter_error() -> None:

View file

@ -14,7 +14,7 @@ def test_runtime_inputs_flatten_payload_with_event_metadata() -> None:
event = Event(
event_type="document.indexed",
payload={"document_id": 42, "document_type": "FILE"},
search_space_id=7,
workspace_id=7,
)
inputs = event_runtime_inputs(event)

View file

@ -11,7 +11,7 @@ pytestmark = pytest.mark.unit
def _event(event_type: str = "document.indexed", **payload) -> Event:
return Event(event_type=event_type, payload=payload, search_space_id=7)
return Event(event_type=event_type, payload=payload, workspace_id=7)
def test_matches_when_event_type_equal_and_filter_passes() -> None:

View file

@ -69,7 +69,7 @@ async def test_build_connector_doc_produces_correct_fields():
page,
markdown,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -77,7 +77,7 @@ async def test_build_connector_doc_produces_correct_fields():
assert doc.unique_id == "abc-123"
assert doc.document_type == DocumentType.CONFLUENCE_CONNECTOR
assert doc.source_markdown == markdown
assert doc.search_space_id == _SEARCH_SPACE_ID
assert doc.workspace_id == _SEARCH_SPACE_ID
assert doc.connector_id == _CONNECTOR_ID
assert doc.created_by_id == _USER_ID
assert doc.metadata["page_id"] == "abc-123"
@ -181,7 +181,7 @@ async def _run_index(mocks, **overrides):
return await index_confluence_pages(
session=mocks["session"],
connector_id=overrides.get("connector_id", _CONNECTOR_ID),
search_space_id=overrides.get("search_space_id", _SEARCH_SPACE_ID),
workspace_id=overrides.get("workspace_id", _SEARCH_SPACE_ID),
user_id=overrides.get("user_id", _USER_ID),
start_date=overrides.get("start_date", "2025-01-01"),
end_date=overrides.get("end_date", "2025-12-31"),

View file

@ -69,7 +69,7 @@ async def test_single_file_returns_one_connector_document(
mock_dropbox_client,
[_make_file_dict("f1", "test.txt")],
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -94,7 +94,7 @@ async def test_multiple_files_all_produce_documents(
mock_dropbox_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -121,7 +121,7 @@ async def test_one_download_exception_does_not_block_others(
mock_dropbox_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -147,7 +147,7 @@ async def test_etl_error_counts_as_download_failure(
mock_dropbox_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -185,7 +185,7 @@ async def test_concurrency_bounded_by_semaphore(
mock_dropbox_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
max_concurrency=2,
)
@ -224,7 +224,7 @@ async def test_heartbeat_fires_during_parallel_downloads(
mock_dropbox_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
on_heartbeat=_on_heartbeat,
)
@ -257,7 +257,7 @@ def full_scan_mocks(mock_dropbox_client, monkeypatch):
monkeypatch.setattr("app.config.config.ETL_SERVICE", "LLAMACLOUD")
async def _fake_skip(session, file, search_space_id):
async def _fake_skip(session, file, workspace_id):
from app.connectors.dropbox.file_types import should_skip_file as _skip
item_skip, unsup_ext = _skip(file)
@ -389,7 +389,7 @@ def selected_files_mocks(mock_dropbox_client, monkeypatch):
skip_results: dict[str, tuple[bool, str | None]] = {}
async def _fake_skip(session, file, search_space_id):
async def _fake_skip(session, file, workspace_id):
return skip_results.get(file["id"], (False, None))
monkeypatch.setattr(_mod, "_should_skip_file", _fake_skip)
@ -430,7 +430,7 @@ async def _run_selected(mocks, file_tuples):
mocks["session"],
file_tuples,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -547,7 +547,7 @@ async def test_delta_sync_deletions_call_remove_document(monkeypatch):
remove_calls: list[str] = []
async def _fake_remove(session, file_id, search_space_id):
async def _fake_remove(session, file_id, workspace_id):
remove_calls.append(file_id)
monkeypatch.setattr(_mod, "_remove_document", _fake_remove)
@ -641,7 +641,7 @@ async def test_delta_sync_mix_deletions_and_upserts(monkeypatch):
remove_calls: list[str] = []
async def _fake_remove(session, file_id, search_space_id):
async def _fake_remove(session, file_id, workspace_id):
remove_calls.append(file_id)
monkeypatch.setattr(_mod, "_remove_document", _fake_remove)

View file

@ -201,7 +201,7 @@ async def _run_gdrive_selected(mocks, file_ids):
mocks["session"],
file_ids,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -542,7 +542,7 @@ async def _run_onedrive_selected(mocks, file_ids):
mocks["session"],
file_ids,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -641,7 +641,7 @@ async def _run_dropbox_selected(mocks, file_paths):
mocks["session"],
file_paths,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)

View file

@ -64,7 +64,7 @@ async def test_single_file_returns_one_connector_document(
mock_drive_client,
[_make_file_dict("f1", "test.txt")],
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -88,7 +88,7 @@ async def test_multiple_files_all_produce_documents(
mock_drive_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -115,7 +115,7 @@ async def test_one_download_exception_does_not_block_others(
mock_drive_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -141,7 +141,7 @@ async def test_etl_error_counts_as_download_failure(
mock_drive_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -180,7 +180,7 @@ async def test_concurrency_bounded_by_semaphore(
mock_drive_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
max_concurrency=2,
)
@ -219,7 +219,7 @@ async def test_heartbeat_fires_during_parallel_downloads(
mock_drive_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
on_heartbeat=_on_heartbeat,
)
@ -284,7 +284,7 @@ def full_scan_mocks(mock_drive_client, monkeypatch):
skip_results: dict[str, tuple[bool, str | None]] = {}
async def _fake_skip(session, file, search_space_id):
async def _fake_skip(session, file, workspace_id):
return skip_results.get(file["id"], (False, None))
monkeypatch.setattr(_mod, "_should_skip_file", _fake_skip)
@ -458,7 +458,7 @@ async def test_delta_sync_removals_serial_rest_parallel(monkeypatch):
remove_calls: list[str] = []
async def _fake_remove(session, file_id, search_space_id):
async def _fake_remove(session, file_id, workspace_id):
remove_calls.append(file_id)
monkeypatch.setattr(_mod, "_remove_document", _fake_remove)
@ -532,7 +532,7 @@ def selected_files_mocks(mock_drive_client, monkeypatch):
skip_results: dict[str, tuple[bool, str | None]] = {}
async def _fake_skip(session, file, search_space_id):
async def _fake_skip(session, file, workspace_id):
return skip_results.get(file["id"], (False, None))
monkeypatch.setattr(_mod, "_should_skip_file", _fake_skip)
@ -563,7 +563,7 @@ async def _run_selected(mocks, file_ids):
mocks["session"],
file_ids,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)

View file

@ -68,7 +68,7 @@ async def test_build_connector_doc_produces_correct_fields():
formatted,
markdown,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -76,7 +76,7 @@ async def test_build_connector_doc_produces_correct_fields():
assert doc.unique_id == "abc-123"
assert doc.document_type == DocumentType.LINEAR_CONNECTOR
assert doc.source_markdown == markdown
assert doc.search_space_id == _SEARCH_SPACE_ID
assert doc.workspace_id == _SEARCH_SPACE_ID
assert doc.connector_id == _CONNECTOR_ID
assert doc.created_by_id == _USER_ID
assert doc.metadata["issue_id"] == "abc-123"
@ -196,7 +196,7 @@ async def _run_index(mocks, **overrides):
return await index_linear_issues(
session=mocks["session"],
connector_id=overrides.get("connector_id", _CONNECTOR_ID),
search_space_id=overrides.get("search_space_id", _SEARCH_SPACE_ID),
workspace_id=overrides.get("workspace_id", _SEARCH_SPACE_ID),
user_id=overrides.get("user_id", _USER_ID),
start_date=overrides.get("start_date", "2025-01-01"),
end_date=overrides.get("end_date", "2025-12-31"),

View file

@ -39,7 +39,7 @@ async def test_build_connector_doc_produces_correct_fields():
page,
markdown,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -47,7 +47,7 @@ async def test_build_connector_doc_produces_correct_fields():
assert doc.unique_id == "abc-123"
assert doc.document_type == DocumentType.NOTION_CONNECTOR
assert doc.source_markdown == markdown
assert doc.search_space_id == _SEARCH_SPACE_ID
assert doc.workspace_id == _SEARCH_SPACE_ID
assert doc.connector_id == _CONNECTOR_ID
assert doc.created_by_id == _USER_ID
assert doc.metadata["page_title"] == "My Notion Page"
@ -159,7 +159,7 @@ async def _run_index(mocks, **overrides):
return await index_notion_pages(
session=mocks["session"],
connector_id=overrides.get("connector_id", _CONNECTOR_ID),
search_space_id=overrides.get("search_space_id", _SEARCH_SPACE_ID),
workspace_id=overrides.get("workspace_id", _SEARCH_SPACE_ID),
user_id=overrides.get("user_id", _USER_ID),
start_date=overrides.get("start_date", "2025-01-01"),
end_date=overrides.get("end_date", "2025-12-31"),

View file

@ -63,7 +63,7 @@ async def test_single_file_returns_one_connector_document(
mock_onedrive_client,
[_make_file_dict("f1", "test.txt")],
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -88,7 +88,7 @@ async def test_multiple_files_all_produce_documents(
mock_onedrive_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -115,7 +115,7 @@ async def test_one_download_exception_does_not_block_others(
mock_onedrive_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -141,7 +141,7 @@ async def test_etl_error_counts_as_download_failure(
mock_onedrive_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
)
@ -179,7 +179,7 @@ async def test_concurrency_bounded_by_semaphore(
mock_onedrive_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
max_concurrency=2,
)
@ -218,7 +218,7 @@ async def test_heartbeat_fires_during_parallel_downloads(
mock_onedrive_client,
files,
connector_id=_CONNECTOR_ID,
search_space_id=_SEARCH_SPACE_ID,
workspace_id=_SEARCH_SPACE_ID,
user_id=_USER_ID,
on_heartbeat=_on_heartbeat,
)

View file

@ -13,7 +13,7 @@ pytestmark = pytest.mark.unit
def _event() -> Event:
return Event(event_type="x.happened", payload={"k": "v"}, search_space_id=1)
return Event(event_type="x.happened", payload={"k": "v"}, workspace_id=1)
async def _noop(_event: Event) -> None:
@ -152,13 +152,13 @@ async def test_publish_builds_a_stamped_event_and_fans_it_out() -> None:
received.append(event)
bus.subscribe(handler)
await bus.publish("document.indexed", {"document_id": 42}, search_space_id=7)
await bus.publish("document.indexed", {"document_id": 42}, workspace_id=7)
assert len(received) == 1
event = received[0]
assert event.event_type == "document.indexed"
assert event.payload == {"document_id": 42}
assert event.search_space_id == 7
assert event.workspace_id == 7
# Engine-stamped identity/time on the way through.
assert event.event_id
assert event.occurred_at
@ -172,10 +172,10 @@ async def test_publish_defaults_payload_to_empty_dict() -> None:
received.append(event)
bus.subscribe(handler)
await bus.publish("x.happened", search_space_id=1)
await bus.publish("x.happened", workspace_id=1)
assert received[0].payload == {}
async def test_publish_with_no_subscribers_is_a_noop() -> None:
await EventBus().publish("x.happened", search_space_id=1) # must not raise
await EventBus().publish("x.happened", workspace_id=1) # must not raise

View file

@ -14,7 +14,7 @@ pytestmark = pytest.mark.unit
def _call(**overrides: Any) -> dict[str, Any] | None:
defaults: dict[str, Any] = {
"document_id": 1,
"search_space_id": 10,
"workspace_id": 10,
"new_folder_id": 7,
"previous_folder_id": None,
"folder_id_changed": True,
@ -33,7 +33,7 @@ def test_folder_set_ready_fires() -> None:
assert result is not None
assert result["event_type"] == "document.entered_folder"
assert result["search_space_id"] == 10
assert result["workspace_id"] == 10
assert result["payload"]["folder_id"] == 7
assert result["payload"]["previous_folder_id"] is None

View file

@ -16,17 +16,17 @@ def test_event_carries_caller_supplied_facts() -> None:
event = Event(
event_type="document.indexed",
payload={"document_id": 42, "content_type": "pdf"},
search_space_id=7,
workspace_id=7,
)
assert event.event_type == "document.indexed"
assert event.payload == {"document_id": 42, "content_type": "pdf"}
assert event.search_space_id == 7
assert event.workspace_id == 7
def test_event_stamps_identity_and_time_when_not_supplied() -> None:
"""Engine stamps id + time so subscribers can dedup/order."""
event = Event(event_type="x.happened", payload={}, search_space_id=1)
event = Event(event_type="x.happened", payload={}, workspace_id=1)
assert event.event_id
assert isinstance(event.occurred_at, datetime)
@ -34,8 +34,8 @@ def test_event_stamps_identity_and_time_when_not_supplied() -> None:
def test_event_ids_are_unique_per_instance() -> None:
"""Two events published with identical content are still distinct facts."""
first = Event(event_type="x.happened", payload={}, search_space_id=1)
second = Event(event_type="x.happened", payload={}, search_space_id=1)
first = Event(event_type="x.happened", payload={}, workspace_id=1)
second = Event(event_type="x.happened", payload={}, workspace_id=1)
assert first.event_id != second.event_id
@ -45,7 +45,7 @@ def test_event_survives_json_round_trip() -> None:
original = Event(
event_type="podcast.generated",
payload={"podcast_id": 9, "duration_s": 123.5},
search_space_id=3,
workspace_id=3,
)
restored = Event.model_validate_json(original.model_dump_json())

View file

@ -330,10 +330,10 @@ async def test_discord_gateway_install_returns_oauth_url(monkeypatch, mocker):
"http://localhost:8000/api/v1/gateway/discord/callback",
)
monkeypatch.setattr(routes.config, "SECRET_KEY", "test-secret")
monkeypatch.setattr(routes, "check_search_space_access", mocker.AsyncMock())
monkeypatch.setattr(routes, "check_workspace_access", mocker.AsyncMock())
response = await routes.install_discord_gateway(
search_space_id=123,
workspace_id=123,
auth=AuthContext.session(
SimpleNamespace(id="00000000-0000-0000-0000-000000000001")
),

View file

@ -14,7 +14,7 @@ def test_valid_document_created_with_required_fields():
source_markdown="## Task\n\nSome content.",
unique_id="task-1",
document_type=DocumentType.CLICKUP_CONNECTOR,
search_space_id=1,
workspace_id=1,
connector_id=42,
created_by_id="00000000-0000-0000-0000-000000000001",
)
@ -32,7 +32,7 @@ def test_omitting_created_by_id_raises():
source_markdown="## Content",
unique_id="task-1",
document_type=DocumentType.CLICKUP_CONNECTOR,
search_space_id=1,
workspace_id=1,
connector_id=42,
)
@ -45,7 +45,7 @@ def test_empty_source_markdown_raises():
source_markdown="",
unique_id="task-1",
document_type=DocumentType.CLICKUP_CONNECTOR,
search_space_id=1,
workspace_id=1,
)
@ -57,7 +57,7 @@ def test_whitespace_only_source_markdown_raises():
source_markdown=" \n\t ",
unique_id="task-1",
document_type=DocumentType.CLICKUP_CONNECTOR,
search_space_id=1,
workspace_id=1,
)
@ -69,7 +69,7 @@ def test_empty_title_raises():
source_markdown="## Content",
unique_id="task-1",
document_type=DocumentType.CLICKUP_CONNECTOR,
search_space_id=1,
workspace_id=1,
)
@ -81,21 +81,21 @@ def test_empty_created_by_id_raises():
source_markdown="## Content",
unique_id="task-1",
document_type=DocumentType.CLICKUP_CONNECTOR,
search_space_id=1,
workspace_id=1,
connector_id=42,
created_by_id="",
)
def test_zero_search_space_id_raises():
"""search_space_id of zero raises a validation error."""
def test_zero_workspace_id_raises():
"""workspace_id of zero raises a validation error."""
with pytest.raises(ValidationError):
ConnectorDocument(
title="Task",
source_markdown="## Content",
unique_id="task-1",
document_type=DocumentType.CLICKUP_CONNECTOR,
search_space_id=0,
workspace_id=0,
connector_id=42,
created_by_id="00000000-0000-0000-0000-000000000001",
)
@ -109,5 +109,5 @@ def test_empty_unique_id_raises():
source_markdown="## Content",
unique_id="",
document_type=DocumentType.CLICKUP_CONNECTOR,
search_space_id=1,
workspace_id=1,
)

View file

@ -27,7 +27,7 @@ def _make_placeholder(**overrides) -> PlaceholderInfo:
"title": "Test Doc",
"document_type": DocumentType.GOOGLE_DRIVE_FILE,
"unique_id": "file-001",
"search_space_id": 1,
"workspace_id": 1,
"connector_id": 42,
"created_by_id": "00000000-0000-0000-0000-000000000001",
}
@ -37,7 +37,7 @@ def _make_placeholder(**overrides) -> PlaceholderInfo:
def _uid_hash(p: PlaceholderInfo) -> str:
return compute_identifier_hash(
p.document_type.value, p.unique_id, p.search_space_id
p.document_type.value, p.unique_id, p.workspace_id
)
@ -82,7 +82,7 @@ async def test_creates_documents_with_pending_status_and_commits():
assert doc.document_type == DocumentType.GOOGLE_DRIVE_FILE
assert doc.content == "Pending..."
assert DocumentStatus.is_state(doc.status, DocumentStatus.PENDING)
assert doc.search_space_id == 1
assert doc.workspace_id == 1
assert doc.connector_id == 42
session.commit.assert_awaited_once()

View file

@ -19,12 +19,12 @@ def test_different_unique_id_produces_different_hash(make_connector_document):
)
def test_different_search_space_produces_different_identifier_hash(
def test_different_workspace_produces_different_identifier_hash(
make_connector_document,
):
"""Same document in different search spaces produces different identifier hashes."""
doc_a = make_connector_document(search_space_id=1)
doc_b = make_connector_document(search_space_id=2)
"""Same document in different workspaces produces different identifier hashes."""
doc_a = make_connector_document(workspace_id=1)
doc_b = make_connector_document(workspace_id=2)
assert compute_unique_identifier_hash(doc_a) != compute_unique_identifier_hash(
doc_b
)
@ -42,18 +42,18 @@ def test_different_document_type_produces_different_identifier_hash(
def test_same_content_same_space_produces_same_content_hash(make_connector_document):
"""Identical content in the same search space always produces the same content hash."""
doc_a = make_connector_document(source_markdown="Hello world", search_space_id=1)
doc_b = make_connector_document(source_markdown="Hello world", search_space_id=1)
"""Identical content in the same workspace always produces the same content hash."""
doc_a = make_connector_document(source_markdown="Hello world", workspace_id=1)
doc_b = make_connector_document(source_markdown="Hello world", workspace_id=1)
assert compute_content_hash(doc_a) == compute_content_hash(doc_b)
def test_same_content_different_space_produces_different_content_hash(
make_connector_document,
):
"""Identical content in different search spaces produces different content hashes."""
doc_a = make_connector_document(source_markdown="Hello world", search_space_id=1)
doc_b = make_connector_document(source_markdown="Hello world", search_space_id=2)
"""Identical content in different workspaces produces different content hashes."""
doc_a = make_connector_document(source_markdown="Hello world", workspace_id=1)
doc_b = make_connector_document(source_markdown="Hello world", workspace_id=2)
assert compute_content_hash(doc_a) != compute_content_hash(doc_b)
@ -69,7 +69,7 @@ def test_compute_identifier_hash_matches_connector_doc_hash(make_connector_docum
doc = make_connector_document(
document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR,
unique_id="msg-123",
search_space_id=5,
workspace_id=5,
)
raw_hash = compute_identifier_hash("GOOGLE_GMAIL_CONNECTOR", "msg-123", 5)
assert raw_hash == compute_unique_identifier_hash(doc)

View file

@ -24,12 +24,12 @@ async def test_calls_prepare_then_index_per_document(pipeline, make_connector_do
doc1 = make_connector_document(
document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR,
unique_id="msg-1",
search_space_id=1,
workspace_id=1,
)
doc2 = make_connector_document(
document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR,
unique_id="msg-2",
search_space_id=1,
workspace_id=1,
)
orm1 = MagicMock(spec=Document)
@ -63,7 +63,7 @@ async def test_skips_document_without_matching_connector_doc(
doc1 = make_connector_document(
document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR,
unique_id="msg-1",
search_space_id=1,
workspace_id=1,
)
orphan_orm = MagicMock(spec=Document)

View file

@ -82,7 +82,7 @@ async def test_index_calls_embed_and_chunk_via_to_thread(
connector_doc = make_connector_document(
document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR,
unique_id="msg-1",
search_space_id=1,
workspace_id=1,
)
document = MagicMock(spec=Document)
document.id = 1
@ -140,7 +140,7 @@ async def test_non_code_documents_use_hybrid_chunker(
connector_doc = make_connector_document(
document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR,
unique_id="msg-1",
search_space_id=1,
workspace_id=1,
should_use_code_chunker=False,
)
document = MagicMock(spec=Document)
@ -184,7 +184,7 @@ async def test_batch_parallel_indexes_all_documents(
make_connector_document(
document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR,
unique_id=f"msg-{i}",
search_space_id=1,
workspace_id=1,
)
for i in range(3)
]
@ -222,7 +222,7 @@ async def test_batch_parallel_one_failure_does_not_affect_others(
make_connector_document(
document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR,
unique_id=f"msg-{i}",
search_space_id=1,
workspace_id=1,
)
for i in range(3)
]

View file

@ -42,7 +42,7 @@ async def test_updates_hash_and_type_for_legacy_document(
doc = make_connector_document(
document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR,
unique_id="msg-abc",
search_space_id=1,
workspace_id=1,
)
legacy_hash = compute_identifier_hash("COMPOSIO_GMAIL_CONNECTOR", "msg-abc", 1)
@ -70,7 +70,7 @@ async def test_noop_when_no_legacy_document_exists(
doc = make_connector_document(
document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR,
unique_id="msg-xyz",
search_space_id=1,
workspace_id=1,
)
result_mock = MagicMock()
@ -89,7 +89,7 @@ async def test_skips_non_google_doc_types(
doc = make_connector_document(
document_type=DocumentType.SLACK_CONNECTOR,
unique_id="slack-123",
search_space_id=1,
workspace_id=1,
)
await pipeline.migrate_legacy_docs([doc])
@ -111,7 +111,7 @@ async def test_handles_all_three_google_types(
doc = make_connector_document(
document_type=native_type,
unique_id="id-1",
search_space_id=1,
workspace_id=1,
)
existing = MagicMock(spec=Document)

View file

@ -32,7 +32,7 @@ def _make_connector_doc(**overrides) -> ConnectorDocument:
"source_markdown": "## Some new content",
"unique_id": "file-001",
"document_type": DocumentType.GOOGLE_DRIVE_FILE,
"search_space_id": 1,
"workspace_id": 1,
"connector_id": 42,
"created_by_id": "00000000-0000-0000-0000-000000000001",
}

View file

@ -39,11 +39,11 @@ pytestmark = pytest.mark.unit
def _make_middleware(mode: FilesystemMode = FilesystemMode.CLOUD):
selection = FilesystemSelection(mode=mode)
resolver = build_backend_resolver(selection, search_space_id=1)
resolver = build_backend_resolver(selection, workspace_id=1)
return build_filesystem_mw(
backend_resolver=resolver,
filesystem_mode=mode,
search_space_id=1,
workspace_id=1,
user_id="00000000-0000-0000-0000-000000000001",
thread_id=1,
)
@ -290,7 +290,7 @@ class TestKBPostgresBackendDeleteFilter:
def _make_backend(self, state: dict[str, Any]) -> KBPostgresBackend:
runtime = SimpleNamespace(state=state)
return KBPostgresBackend(search_space_id=1, runtime=runtime)
return KBPostgresBackend(workspace_id=1, runtime=runtime)
def test_pending_filesystem_view_returns_deleted_paths(self):
backend = self._make_backend(

View file

@ -38,16 +38,16 @@ def test_backend_resolver_returns_multi_root_backend_for_single_root(tmp_path: P
def test_backend_resolver_uses_cloud_mode_by_default():
resolver = build_backend_resolver(FilesystemSelection())
backend = resolver(_RuntimeStub())
# When no search_space_id is provided we fall back to StateBackend so
# When no workspace_id is provided we fall back to StateBackend so
# sub-agents / tests without DB access still work.
assert backend.__class__.__name__ == "StateBackend"
def test_backend_resolver_uses_kb_postgres_in_cloud_with_search_space():
resolver = build_backend_resolver(FilesystemSelection(), search_space_id=42)
def test_backend_resolver_uses_kb_postgres_in_cloud_with_workspace():
resolver = build_backend_resolver(FilesystemSelection(), workspace_id=42)
backend = resolver(_RuntimeStub())
assert backend.__class__.__name__ == "KBPostgresBackend"
assert backend.search_space_id == 42
assert backend.workspace_id == 42
def test_backend_resolver_returns_multi_root_backend_for_multiple_roots(tmp_path: Path):

View file

@ -92,7 +92,7 @@ async def test_create_document_allows_identical_content_at_different_paths() ->
session, # type: ignore[arg-type]
virtual_path="/documents/a/notes.md",
content=content,
search_space_id=42,
workspace_id=42,
created_by_id="user-1",
)
assert isinstance(first, Document)
@ -104,7 +104,7 @@ async def test_create_document_allows_identical_content_at_different_paths() ->
session, # type: ignore[arg-type]
virtual_path="/documents/b/notes-copy.md",
content=content,
search_space_id=42,
workspace_id=42,
created_by_id="user-1",
)
assert isinstance(second, Document)
@ -121,7 +121,7 @@ async def test_create_document_still_rejects_path_collision() -> None:
"""Path uniqueness remains the hard invariant.
If ``unique_identifier_hash`` already points at an existing row in
the same search space, the create call must raise ``ValueError``
the same workspace, the create call must raise ``ValueError``
with a clear message matching the behavior the commit loop relies
on to upsert via the existing-row code path.
"""
@ -137,7 +137,7 @@ async def test_create_document_still_rejects_path_collision() -> None:
session, # type: ignore[arg-type]
virtual_path="/documents/notes.md",
content="anything",
search_space_id=42,
workspace_id=42,
created_by_id="user-1",
)
@ -160,7 +160,7 @@ async def test_create_document_does_not_query_for_content_hash_collision(
session, # type: ignore[arg-type]
virtual_path="/documents/notes.md",
content="hello",
search_space_id=42,
workspace_id=42,
created_by_id="user-1",
)
# Path-collision SELECT only. No content_hash SELECT.

View file

@ -217,7 +217,7 @@ async def test_pre_write_snapshot_defers_dispatch_when_list_provided(
session, # type: ignore[arg-type]
doc=doc,
action_id=42,
search_space_id=1,
workspace_id=1,
turn_id="t-1",
deferred_dispatches=deferred,
)
@ -262,7 +262,7 @@ async def test_pre_write_snapshot_dispatches_inline_when_list_omitted(
session, # type: ignore[arg-type]
doc=doc,
action_id=88,
search_space_id=1,
workspace_id=1,
turn_id="t-1",
# No deferred_dispatches arg — fall back to inline dispatch.
)
@ -302,7 +302,7 @@ async def test_pre_mkdir_snapshot_defers_dispatch_when_list_provided(
session, # type: ignore[arg-type]
folder=folder,
action_id=55,
search_space_id=1,
workspace_id=1,
turn_id="t-1",
deferred_dispatches=deferred,
)

View file

@ -28,7 +28,7 @@ pytestmark = pytest.mark.unit
def _backend(state: dict) -> KBPostgresBackend:
return KBPostgresBackend(search_space_id=1, runtime=SimpleNamespace(state=state))
return KBPostgresBackend(workspace_id=1, runtime=SimpleNamespace(state=state))
def test_render_full_document_uses_full_view_and_registers() -> None:

View file

@ -101,7 +101,7 @@ class TestFormatTreeRendering:
docs = [_Row(**spec) for spec in doc_specs]
mw = KnowledgeTreeMiddleware(
search_space_id=1,
workspace_id=1,
filesystem_mode=None, # type: ignore[arg-type]
)
return mw._format_tree(index, docs)

View file

@ -53,7 +53,7 @@ def _notification(**overrides) -> Notification:
defaults = {
"id": 1,
"user_id": uuid.uuid4(),
"search_space_id": 3,
"workspace_id": 3,
"type": "document_processing",
"title": "Title",
"message": "Message",

View file

@ -10,7 +10,7 @@ pytestmark = pytest.mark.unit
def test_operation_id_encodes_type_and_space():
"""The operation id embeds the document type and search space id."""
"""The operation id embeds the document type and workspace id."""
op = msg.operation_id("FILE", "report.pdf", 9)
assert op.startswith("doc_FILE_9_")

View file

@ -9,8 +9,8 @@ from app.notifications.service.messages import insufficient_credits as msg
pytestmark = pytest.mark.unit
def test_operation_id_encodes_search_space():
"""The operation id embeds the search space id."""
def test_operation_id_encodes_workspace():
"""The operation id embeds the workspace id."""
assert msg.operation_id("doc.pdf", 9).startswith("insufficient_credits_9_")

View file

@ -170,7 +170,7 @@ class TestMetricHelpers:
metrics.record_tool_call_error(tool_name="web_search")
metrics.record_kb_search_duration(
4.0,
search_space_id=1,
workspace_id=1,
surface="documents",
)
metrics.record_compaction_run(reason="auto")
@ -250,7 +250,7 @@ class TestNoopSpansWhenDisabled:
helpers = [
otel.tool_call_span("write_file", input_size=42),
otel.model_call_span(model_id="openai:gpt-4o", provider="openai"),
otel.kb_search_span(search_space_id=1, query_chars=99),
otel.kb_search_span(workspace_id=1, query_chars=99),
otel.kb_persist_span(document_type="NOTE", document_id=7),
otel.compaction_span(reason="overflow", messages_in=120),
otel.interrupt_span(interrupt_type="permission_ask"),

View file

@ -48,14 +48,14 @@ async def test_retriever_wrapper_records_one_span_and_metric(monkeypatch) -> Non
self,
query_text: str,
top_k: int,
search_space_id: int,
workspace_id: int,
) -> list[str]:
del query_text, top_k, search_space_id
del query_text, top_k, workspace_id
return ["doc-1", "doc-2"]
result = await Retriever().search("hello", 3, 42)
assert result == ["doc-1", "doc-2"]
assert len(calls) == 1
assert calls[0]["search_space_id"] == 42
assert calls[0]["workspace_id"] == 42
assert calls[0]["surface"] == "documents"

View file

@ -22,7 +22,7 @@ def _podcast(*, status: PodcastStatus = PodcastStatus.PENDING, **columns) -> Pod
"""A persisted-looking row: the id and created_at a saved podcast would carry."""
podcast = Podcast(
title="Episode",
search_space_id=3,
workspace_id=3,
status=status,
spec_version=1,
**columns,

View file

@ -36,11 +36,11 @@ async def test_resolve_billing_for_auto_mode(monkeypatch):
_no_auto_candidates,
)
search_space = SimpleNamespace(id=1, user_id=None, image_gen_model_id=None)
workspace = SimpleNamespace(id=1, user_id=None, image_gen_model_id=None)
tier, model, reserve = await image_generation_routes._resolve_billing_for_image_gen(
session=None,
config_id=0, # IMAGE_GEN_AUTO_MODE_ID
search_space=search_space,
workspace=workspace,
)
assert tier == "free"
assert model == "auto"
@ -95,11 +95,11 @@ async def test_resolve_billing_for_premium_global_config(monkeypatch):
raising=False,
)
search_space = SimpleNamespace(id=1, user_id=None, image_gen_model_id=None)
workspace = SimpleNamespace(id=1, user_id=None, image_gen_model_id=None)
# Premium with override.
tier, model, reserve = await image_generation_routes._resolve_billing_for_image_gen(
session=None, config_id=-1, search_space=search_space
session=None, config_id=-1, workspace=workspace
)
assert tier == "premium"
assert model == "openai/gpt-image-1"
@ -109,7 +109,7 @@ async def test_resolve_billing_for_premium_global_config(monkeypatch):
from app.services.billable_calls import DEFAULT_IMAGE_RESERVE_MICROS
tier, model, reserve = await image_generation_routes._resolve_billing_for_image_gen(
session=None, config_id=-2, search_space=search_space
session=None, config_id=-2, workspace=workspace
)
assert tier == "free"
# Provider-prefixed model string for OpenRouter.
@ -125,9 +125,9 @@ async def test_resolve_billing_for_user_owned_byok_is_free():
from app.routes import image_generation_routes
from app.services.billable_calls import DEFAULT_IMAGE_RESERVE_MICROS
search_space = SimpleNamespace(id=1, user_id=None, image_gen_model_id=None)
workspace = SimpleNamespace(id=1, user_id=None, image_gen_model_id=None)
tier, model, reserve = await image_generation_routes._resolve_billing_for_image_gen(
session=None, config_id=42, search_space=search_space
session=None, config_id=42, workspace=workspace
)
assert tier == "free"
assert model == "user_byok"
@ -135,9 +135,9 @@ async def test_resolve_billing_for_user_owned_byok_is_free():
@pytest.mark.asyncio
async def test_resolve_billing_falls_back_to_search_space_default(monkeypatch):
async def test_resolve_billing_falls_back_to_workspace_default(monkeypatch):
"""When the request omits ``image_gen_model_id``, the helper
must consult the search space's default — so a search space pinned
must consult the workspace's default — so a workspace pinned
to a premium global config still gates new requests by quota.
"""
from app.config import config
@ -172,13 +172,13 @@ async def test_resolve_billing_falls_back_to_search_space_default(monkeypatch):
raising=False,
)
search_space = SimpleNamespace(id=1, user_id=None, image_gen_model_id=-7)
workspace = SimpleNamespace(id=1, user_id=None, image_gen_model_id=-7)
(
tier,
model,
_reserve,
) = await image_generation_routes._resolve_billing_for_image_gen(
session=None, config_id=None, search_space=search_space
session=None, config_id=None, workspace=workspace
)
assert tier == "premium"
assert model == "openai/gpt-image-1"

View file

@ -117,7 +117,7 @@ class TestRegenerateRequestValidation:
def test_revert_actions_requires_from_message_id(self) -> None:
with pytest.raises(Exception) as exc:
RegenerateRequest(
search_space_id=1,
workspace_id=1,
user_query="hi",
revert_actions=True,
)
@ -126,7 +126,7 @@ class TestRegenerateRequestValidation:
def test_from_message_id_without_revert_is_allowed(self) -> None:
req = RegenerateRequest(
search_space_id=1,
workspace_id=1,
user_query="hi",
from_message_id=42,
)
@ -135,7 +135,7 @@ class TestRegenerateRequestValidation:
def test_revert_actions_with_from_message_id_passes(self) -> None:
req = RegenerateRequest(
search_space_id=1,
workspace_id=1,
user_query="hi",
from_message_id=42,
revert_actions=True,

View file

@ -1,4 +1,4 @@
"""Unit tests for ``_resolve_agent_billing_for_search_space``."""
"""Unit tests for ``_resolve_agent_billing_for_workspace``."""
from __future__ import annotations
@ -39,7 +39,7 @@ class _FakePinResolution:
from_existing_pin: bool = False
def _make_search_space(*, chat_model_id: int | None, user_id: UUID) -> SimpleNamespace:
def _make_workspace(*, chat_model_id: int | None, user_id: UUID) -> SimpleNamespace:
return SimpleNamespace(id=42, chat_model_id=chat_model_id, user_id=user_id)
@ -50,16 +50,16 @@ def _make_byok_model(
id=id_,
model_id=model_id,
catalog={"base_model": base_model} if base_model else {},
connection=SimpleNamespace(enabled=True, search_space_id=42, user_id=None),
connection=SimpleNamespace(enabled=True, workspace_id=42, user_id=None),
)
@pytest.mark.asyncio
async def test_auto_mode_with_thread_id_resolves_to_premium_global(monkeypatch):
from app.services.billable_calls import _resolve_agent_billing_for_search_space
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_search_space(chat_model_id=0, user_id=user_id)])
session = _FakeSession([_make_workspace(chat_model_id=0, user_id=user_id)])
async def _fake_resolve_pin(*_args, **kwargs):
assert kwargs["selected_llm_config_id"] == 0
@ -84,8 +84,8 @@ async def test_auto_mode_with_thread_id_resolves_to_premium_global(monkeypatch):
)
monkeypatch.setattr(llm_module, "get_global_llm_config", _fake_get_global)
owner, tier, base_model = await _resolve_agent_billing_for_search_space(
session, search_space_id=42, thread_id=99
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42, thread_id=99
)
assert owner == user_id
@ -95,14 +95,14 @@ async def test_auto_mode_with_thread_id_resolves_to_premium_global(monkeypatch):
@pytest.mark.asyncio
async def test_auto_mode_with_thread_id_resolves_to_byok_is_free(monkeypatch):
from app.services.billable_calls import _resolve_agent_billing_for_search_space
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
search_space = _make_search_space(chat_model_id=0, user_id=user_id)
workspace = _make_workspace(chat_model_id=0, user_id=user_id)
byok_model = _make_byok_model(
id_=17, base_model="anthropic/claude-3-haiku", model_id="my-claude"
)
session = _FakeSession([search_space, byok_model])
session = _FakeSession([workspace, byok_model])
async def _fake_resolve_pin(*_args, **_kwargs):
return _FakePinResolution(resolved_llm_config_id=17, resolved_tier="free")
@ -113,8 +113,8 @@ async def test_auto_mode_with_thread_id_resolves_to_byok_is_free(monkeypatch):
pin_module, "resolve_or_get_pinned_llm_config_id", _fake_resolve_pin
)
owner, tier, base_model = await _resolve_agent_billing_for_search_space(
session, search_space_id=42, thread_id=99
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42, thread_id=99
)
assert owner == user_id
@ -124,13 +124,13 @@ async def test_auto_mode_with_thread_id_resolves_to_byok_is_free(monkeypatch):
@pytest.mark.asyncio
async def test_auto_mode_without_thread_id_falls_back_to_free():
from app.services.billable_calls import _resolve_agent_billing_for_search_space
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_search_space(chat_model_id=0, user_id=user_id)])
session = _FakeSession([_make_workspace(chat_model_id=0, user_id=user_id)])
owner, tier, base_model = await _resolve_agent_billing_for_search_space(
session, search_space_id=42, thread_id=None
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42, thread_id=None
)
assert owner == user_id
@ -140,10 +140,10 @@ async def test_auto_mode_without_thread_id_falls_back_to_free():
@pytest.mark.asyncio
async def test_auto_mode_pin_failure_falls_back_to_free(monkeypatch):
from app.services.billable_calls import _resolve_agent_billing_for_search_space
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_search_space(chat_model_id=0, user_id=user_id)])
session = _FakeSession([_make_workspace(chat_model_id=0, user_id=user_id)])
async def _fake_resolve_pin(*args, **kwargs):
raise ValueError("thread missing")
@ -154,8 +154,8 @@ async def test_auto_mode_pin_failure_falls_back_to_free(monkeypatch):
pin_module, "resolve_or_get_pinned_llm_config_id", _fake_resolve_pin
)
owner, tier, base_model = await _resolve_agent_billing_for_search_space(
session, search_space_id=42, thread_id=99
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42, thread_id=99
)
assert owner == user_id
@ -165,10 +165,10 @@ async def test_auto_mode_pin_failure_falls_back_to_free(monkeypatch):
@pytest.mark.asyncio
async def test_negative_id_premium_global_returns_premium(monkeypatch):
from app.services.billable_calls import _resolve_agent_billing_for_search_space
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_search_space(chat_model_id=-1, user_id=user_id)])
session = _FakeSession([_make_workspace(chat_model_id=-1, user_id=user_id)])
def _fake_get_global(cfg_id):
return {
@ -182,8 +182,8 @@ async def test_negative_id_premium_global_returns_premium(monkeypatch):
monkeypatch.setattr(llm_module, "get_global_llm_config", _fake_get_global)
owner, tier, base_model = await _resolve_agent_billing_for_search_space(
session, search_space_id=42, thread_id=99
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42, thread_id=99
)
assert owner == user_id
@ -193,10 +193,10 @@ async def test_negative_id_premium_global_returns_premium(monkeypatch):
@pytest.mark.asyncio
async def test_negative_id_missing_base_model_falls_back_to_model_name(monkeypatch):
from app.services.billable_calls import _resolve_agent_billing_for_search_space
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_search_space(chat_model_id=-5, user_id=user_id)])
session = _FakeSession([_make_workspace(chat_model_id=-5, user_id=user_id)])
def _fake_get_global(cfg_id):
return {"id": cfg_id, "model_name": "fallback-model", "billing_tier": "premium"}
@ -205,8 +205,8 @@ async def test_negative_id_missing_base_model_falls_back_to_model_name(monkeypat
monkeypatch.setattr(llm_module, "get_global_llm_config", _fake_get_global)
_, tier, base_model = await _resolve_agent_billing_for_search_space(
session, search_space_id=42
_, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42
)
assert tier == "premium"
@ -215,15 +215,15 @@ async def test_negative_id_missing_base_model_falls_back_to_model_name(monkeypat
@pytest.mark.asyncio
async def test_positive_id_byok_is_always_free():
from app.services.billable_calls import _resolve_agent_billing_for_search_space
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
search_space = _make_search_space(chat_model_id=23, user_id=user_id)
workspace = _make_workspace(chat_model_id=23, user_id=user_id)
byok_model = _make_byok_model(id_=23, base_model="anthropic/claude-3.5-sonnet")
session = _FakeSession([search_space, byok_model])
session = _FakeSession([workspace, byok_model])
owner, tier, base_model = await _resolve_agent_billing_for_search_space(
session, search_space_id=42
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42
)
assert owner == user_id
@ -233,13 +233,13 @@ async def test_positive_id_byok_is_always_free():
@pytest.mark.asyncio
async def test_positive_id_byok_missing_returns_free_with_empty_base_model():
from app.services.billable_calls import _resolve_agent_billing_for_search_space
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_search_space(chat_model_id=99, user_id=user_id)])
session = _FakeSession([_make_workspace(chat_model_id=99, user_id=user_id)])
owner, tier, base_model = await _resolve_agent_billing_for_search_space(
session, search_space_id=42
owner, tier, base_model = await _resolve_agent_billing_for_workspace(
session, workspace_id=42
)
assert owner == user_id
@ -248,21 +248,21 @@ async def test_positive_id_byok_missing_returns_free_with_empty_base_model():
@pytest.mark.asyncio
async def test_search_space_not_found_raises_value_error():
from app.services.billable_calls import _resolve_agent_billing_for_search_space
async def test_workspace_not_found_raises_value_error():
from app.services.billable_calls import _resolve_agent_billing_for_workspace
with pytest.raises(ValueError, match="Search space"):
await _resolve_agent_billing_for_search_space(
_FakeSession([None]), search_space_id=999
with pytest.raises(ValueError, match="Workspace"):
await _resolve_agent_billing_for_workspace(
_FakeSession([None]), workspace_id=999
)
@pytest.mark.asyncio
async def test_chat_model_id_none_raises_value_error():
from app.services.billable_calls import _resolve_agent_billing_for_search_space
from app.services.billable_calls import _resolve_agent_billing_for_workspace
user_id = uuid4()
session = _FakeSession([_make_search_space(chat_model_id=None, user_id=user_id)])
session = _FakeSession([_make_workspace(chat_model_id=None, user_id=user_id)])
with pytest.raises(ValueError, match="chat_model_id"):
await _resolve_agent_billing_for_search_space(session, search_space_id=42)
await _resolve_agent_billing_for_workspace(session, workspace_id=42)

View file

@ -11,7 +11,7 @@ def test_lock_key_format():
from app.tasks.celery_tasks.document_tasks import _ai_sort_lock_key
key = _ai_sort_lock_key(42)
assert key == "ai_sort:search_space:42:lock"
assert key == "ai_sort:workspace:42:lock"
def test_lock_prevents_duplicate_run():
@ -31,11 +31,11 @@ def test_lock_prevents_duplicate_run():
):
import asyncio
from app.tasks.celery_tasks.document_tasks import _ai_sort_search_space_async
from app.tasks.celery_tasks.document_tasks import _ai_sort_workspace_async
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(_ai_sort_search_space_async(1, "user-123"))
loop.run_until_complete(_ai_sort_workspace_async(1, "user-123"))
finally:
loop.close()

View file

@ -140,12 +140,12 @@ def _set_global_llm_configs(monkeypatch, config, configs: list[dict]):
def _thread(
*,
search_space_id: int = 10,
workspace_id: int = 10,
pinned_llm_config_id: int | None = None,
):
return SimpleNamespace(
id=1,
search_space_id=search_space_id,
workspace_id=workspace_id,
pinned_llm_config_id=pinned_llm_config_id,
)
@ -186,7 +186,7 @@ async def test_auto_first_turn_pins_one_model(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -234,7 +234,7 @@ async def test_premium_eligible_auto_prefers_premium_over_free(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -321,7 +321,7 @@ async def test_premium_eligible_auto_uses_quality_pool_not_single_preferred_mode
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -361,7 +361,7 @@ async def test_next_turn_reuses_existing_pin(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -400,7 +400,7 @@ async def test_premium_eligible_auto_can_pin_premium(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -445,7 +445,7 @@ async def test_premium_ineligible_auto_pins_free_only(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -490,7 +490,7 @@ async def test_pinned_premium_stays_premium_after_quota_exhaustion(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -535,7 +535,7 @@ async def test_force_repin_free_switches_auto_premium_pin_to_free(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
force_repin_free=True,
@ -567,7 +567,7 @@ async def test_explicit_user_model_change_clears_pin(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=7,
)
@ -605,7 +605,7 @@ async def test_invalid_pinned_config_repairs_with_new_pin(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -664,7 +664,7 @@ async def test_health_gated_config_is_excluded_from_selection(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -716,7 +716,7 @@ async def test_tier_a_locks_first_premium_user_skips_or(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -768,7 +768,7 @@ async def test_tier_a_falls_through_to_or_when_a_pool_empty_for_user(monkeypatch
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -825,7 +825,7 @@ async def test_top_k_picks_only_high_score_models(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=thread_id,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -889,7 +889,7 @@ async def test_pin_reuse_survives_health_gating_for_existing_pin(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -941,7 +941,7 @@ async def test_pin_reuse_regression_existing_healthy_pin(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -1001,7 +1001,7 @@ async def test_runtime_cooled_down_pin_is_not_reused(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -1069,7 +1069,7 @@ async def test_shared_runtime_cooldown_blocks_pin_across_workers(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -1113,7 +1113,7 @@ async def test_clearing_runtime_cooldown_restores_pin_reuse(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
)
@ -1165,7 +1165,7 @@ async def test_auto_pin_repin_excludes_previous_config_on_runtime_retry(monkeypa
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id="00000000-0000-0000-0000-000000000001",
selected_llm_config_id=0,
exclude_config_ids={-1},

View file

@ -76,7 +76,7 @@ class _FakeSession:
def _thread(*, pinned: int | None = None):
return SimpleNamespace(id=1, search_space_id=10, pinned_llm_config_id=pinned)
return SimpleNamespace(id=1, workspace_id=10, pinned_llm_config_id=pinned)
def _set_global_llm_configs(monkeypatch, config, configs: list[dict]):
@ -179,7 +179,7 @@ async def test_image_turn_filters_out_text_only_candidates(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id=None,
selected_llm_config_id=0,
requires_image_input=True,
@ -207,7 +207,7 @@ async def test_image_turn_force_repins_stale_text_only_pin(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id=None,
selected_llm_config_id=0,
requires_image_input=True,
@ -239,7 +239,7 @@ async def test_image_turn_reuses_existing_vision_pin(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id=None,
selected_llm_config_id=0,
requires_image_input=True,
@ -269,7 +269,7 @@ async def test_image_turn_with_no_vision_candidates_raises(monkeypatch):
await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id=None,
selected_llm_config_id=0,
requires_image_input=True,
@ -292,7 +292,7 @@ async def test_non_image_turn_keeps_text_only_in_pool(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id=None,
selected_llm_config_id=0,
)
@ -326,7 +326,7 @@ async def test_image_turn_unannotated_cfg_resolves_via_helper(monkeypatch):
result = await resolve_or_get_pinned_llm_config_id(
session,
thread_id=1,
search_space_id=10,
workspace_id=10,
user_id=None,
selected_llm_config_id=0,
requires_image_input=True,

View file

@ -169,7 +169,7 @@ async def test_free_path_skips_reserve_but_writes_audit_row(monkeypatch):
async with billable_call(
user_id=user_id,
search_space_id=42,
workspace_id=42,
billing_tier="free",
base_model="openai/gpt-image-1",
usage_type="image_generation",
@ -210,7 +210,7 @@ async def test_premium_reserve_denied_raises_quota_insufficient(monkeypatch):
with pytest.raises(QuotaInsufficientError) as exc_info:
async with billable_call(
user_id=user_id,
search_space_id=42,
workspace_id=42,
billing_tier="premium",
base_model="openai/gpt-image-1",
quota_reserve_micros_override=50_000,
@ -242,7 +242,7 @@ async def test_premium_success_finalizes_with_actual_cost(monkeypatch):
async with billable_call(
user_id=user_id,
search_space_id=42,
workspace_id=42,
billing_tier="premium",
base_model="openai/gpt-image-1",
quota_reserve_micros_override=50_000,
@ -292,7 +292,7 @@ async def test_premium_failure_releases_reservation(monkeypatch):
with pytest.raises(_ProviderError):
async with billable_call(
user_id=user_id,
search_space_id=42,
workspace_id=42,
billing_tier="premium",
base_model="openai/gpt-image-1",
quota_reserve_micros_override=50_000,
@ -336,7 +336,7 @@ async def test_premium_uses_estimator_when_no_micros_override(monkeypatch):
user_id = uuid4()
async with billable_call(
user_id=user_id,
search_space_id=1,
workspace_id=1,
billing_tier="premium",
base_model="openai/gpt-4o",
quota_reserve_tokens=4000,
@ -367,7 +367,7 @@ async def test_premium_finalize_failure_propagates_and_releases(monkeypatch):
with pytest.raises(BillingSettlementError):
async with billable_call(
user_id=user_id,
search_space_id=42,
workspace_id=42,
billing_tier="premium",
base_model="openai/gpt-image-1",
quota_reserve_micros_override=50_000,
@ -408,7 +408,7 @@ async def test_premium_audit_commit_hang_times_out_after_finalize(monkeypatch):
async with billable_call(
user_id=user_id,
search_space_id=42,
workspace_id=42,
billing_tier="premium",
base_model="openai/gpt-image-1",
quota_reserve_micros_override=50_000,
@ -450,7 +450,7 @@ async def test_free_audit_failure_is_best_effort(monkeypatch):
async with billable_call(
user_id=uuid4(),
search_space_id=42,
workspace_id=42,
billing_tier="free",
base_model="openai/gpt-image-1",
usage_type="image_generation",
@ -488,7 +488,7 @@ async def test_free_podcast_path_audits_with_podcast_usage_type(monkeypatch):
async with billable_call(
user_id=user_id,
search_space_id=42,
workspace_id=42,
billing_tier="free",
base_model="openrouter/some-free-model",
quota_reserve_micros_override=200_000,
@ -514,7 +514,7 @@ async def test_free_podcast_path_audits_with_podcast_usage_type(monkeypatch):
row = spies["record"][0]
assert row["usage_type"] == "podcast_generation"
assert row["thread_id"] is None
assert row["search_space_id"] == 42
assert row["workspace_id"] == 42
assert row["call_details"] == {"podcast_id": 7, "title": "Test Podcast"}
@ -539,7 +539,7 @@ async def test_premium_video_denial_raises_quota_insufficient(monkeypatch):
with pytest.raises(QuotaInsufficientError) as exc_info:
async with billable_call(
user_id=user_id,
search_space_id=42,
workspace_id=42,
billing_tier="premium",
base_model="gpt-5.4",
quota_reserve_micros_override=1_000_000,

View file

@ -46,8 +46,8 @@ async def test_global_openrouter_image_gen_sets_explicit_api_base():
image_gen.response_format = None
image_gen.model = None
search_space = MagicMock()
search_space.image_gen_model_id = global_model["id"]
workspace = MagicMock()
workspace.image_gen_model_id = global_model["id"]
session = MagicMock()
with (
@ -68,7 +68,7 @@ async def test_global_openrouter_image_gen_sets_explicit_api_base():
),
):
await image_generation_routes._execute_image_generation(
session=session, image_gen=image_gen, search_space=search_space
session=session, image_gen=image_gen, workspace=workspace
)
assert captured.get("api_base") == "https://openrouter.ai/api/v1"
@ -109,16 +109,16 @@ async def test_generate_image_tool_global_sets_explicit_api_base():
response._hidden_params = {"model": "openrouter/openai/gpt-image-1"}
return response
search_space = MagicMock()
search_space.id = 1
search_space.image_gen_model_id = global_model["id"]
workspace = MagicMock()
workspace.id = 1
workspace.image_gen_model_id = global_model["id"]
session_cm = AsyncMock()
session = AsyncMock()
session_cm.__aenter__.return_value = session
scalars = MagicMock()
scalars.first.return_value = search_space
scalars.first.return_value = workspace
exec_result = MagicMock()
exec_result.scalars.return_value = scalars
session.execute.return_value = exec_result
@ -146,7 +146,7 @@ async def test_generate_image_tool_global_sets_explicit_api_base():
),
):
tool = gi_module.create_generate_image_tool(
search_space_id=1, db_session=MagicMock()
workspace_id=1, db_session=MagicMock()
)
# The live tool takes an injected ToolRuntime and returns a Command;
# drive the raw coroutine with a minimal runtime (the tool only reads

View file

@ -77,7 +77,7 @@ async def test_ainvoke_routes_through_billable_call(monkeypatch):
wrapper = QuotaCheckedVisionLLM(
inner,
user_id=user_id,
search_space_id=99,
workspace_id=99,
billing_tier="premium",
base_model="openai/gpt-4o",
quota_reserve_tokens=4000,
@ -89,7 +89,7 @@ async def test_ainvoke_routes_through_billable_call(monkeypatch):
assert len(captured_kwargs) == 1
bc_kwargs = captured_kwargs[0]
assert bc_kwargs["user_id"] == user_id
assert bc_kwargs["search_space_id"] == 99
assert bc_kwargs["workspace_id"] == 99
assert bc_kwargs["billing_tier"] == "premium"
assert bc_kwargs["base_model"] == "openai/gpt-4o"
assert bc_kwargs["quota_reserve_tokens"] == 4000
@ -120,7 +120,7 @@ async def test_ainvoke_propagates_quota_insufficient_error(monkeypatch):
wrapper = QuotaCheckedVisionLLM(
inner,
user_id=uuid4(),
search_space_id=1,
workspace_id=1,
billing_tier="premium",
base_model="openai/gpt-4o",
quota_reserve_tokens=4000,
@ -146,7 +146,7 @@ async def test_proxies_non_overridden_attributes_to_inner():
wrapper = QuotaCheckedVisionLLM(
inner,
user_id=uuid4(),
search_space_id=1,
workspace_id=1,
billing_tier="premium",
base_model="openai/gpt-4o",
quota_reserve_tokens=4000,

View file

@ -93,7 +93,7 @@ def _action(*, tool_name: str, action_id: int = 7):
id=action_id,
tool_name=tool_name,
thread_id=1,
search_space_id=2,
workspace_id=2,
user_id="user-1",
reverse_descriptor=None,
)
@ -111,7 +111,7 @@ def _doc_revision(
revision = MagicMock()
revision.id = 100
revision.document_id = document_id
revision.search_space_id = 2
revision.workspace_id = 2
revision.content_before = content_before
revision.title_before = title_before
revision.folder_id_before = folder_id_before
@ -130,7 +130,7 @@ def _folder_revision(
revision = MagicMock()
revision.id = 200
revision.folder_id = folder_id
revision.search_space_id = 2
revision.workspace_id = 2
revision.name_before = name_before
revision.parent_id_before = parent_id_before
revision.position_before = position_before

View file

@ -32,12 +32,12 @@ def _patch_common_bundle_dependencies(monkeypatch: pytest.MonkeyPatch):
_CapturedChatLiteLLM.calls = []
async def _fake_search_space(
_session: Any, _search_space_id: int
async def _fake_workspace(
_session: Any, _workspace_id: int
) -> SimpleNamespace:
return SimpleNamespace(id=42, user_id="user-1")
monkeypatch.setattr(llm_bundle, "_load_search_space", _fake_search_space)
monkeypatch.setattr(llm_bundle, "_load_workspace", _fake_workspace)
monkeypatch.setattr(llm_bundle, "SanitizedChatLiteLLM", _CapturedChatLiteLLM)
monkeypatch.setattr(llm_bundle, "register_model_usage_metadata", lambda **_kw: None)
monkeypatch.setattr(
@ -65,9 +65,9 @@ async def test_load_llm_bundle_enables_streaming_for_db_models(
connection=connection,
)
async def _fake_db_model(_session: Any, *, model_id: int, search_space: Any) -> Any:
async def _fake_db_model(_session: Any, *, model_id: int, workspace: Any) -> Any:
assert model_id == 7
assert search_space.id == 42
assert workspace.id == 42
return model
monkeypatch.setattr(llm_bundle, "_load_db_model", _fake_db_model)
@ -83,7 +83,7 @@ async def test_load_llm_bundle_enables_streaming_for_db_models(
llm, agent_config, error = await llm_bundle.load_llm_bundle(
object(),
config_id=7,
search_space_id=42,
workspace_id=42,
)
assert error is None
@ -140,7 +140,7 @@ async def test_load_llm_bundle_enables_streaming_for_global_models(
llm, agent_config, error = await llm_bundle.load_llm_bundle(
object(),
config_id=-11,
search_space_id=42,
workspace_id=42,
)
assert error is None

View file

@ -145,14 +145,14 @@ async def test_billable_call_invoked_with_correct_kwargs_for_free_config(monkeyp
user_id = uuid4()
async def _fake_resolver(sess, search_space_id, *, thread_id=None):
assert search_space_id == 777
async def _fake_resolver(sess, workspace_id, *, thread_id=None):
assert workspace_id == 777
assert thread_id == 99
return user_id, "free", "openrouter/some-free-model"
monkeypatch.setattr(
video_presentation_tasks,
"_resolve_agent_billing_for_search_space",
"_resolve_agent_billing_for_workspace",
_fake_resolver,
)
monkeypatch.setattr(video_presentation_tasks, "billable_call", _ok_billable_call)
@ -169,7 +169,7 @@ async def test_billable_call_invoked_with_correct_kwargs_for_free_config(monkeyp
result = await video_presentation_tasks._generate_video_presentation(
video_presentation_id=11,
source_content="content",
search_space_id=777,
workspace_id=777,
user_prompt=None,
)
@ -180,7 +180,7 @@ async def test_billable_call_invoked_with_correct_kwargs_for_free_config(monkeyp
assert len(_CALL_LOG) == 1
call = _CALL_LOG[0]
assert call["user_id"] == user_id
assert call["search_space_id"] == 777
assert call["workspace_id"] == 777
assert call["billing_tier"] == "free"
assert call["base_model"] == "openrouter/some-free-model"
assert call["usage_type"] == "video_presentation_generation"
@ -213,12 +213,12 @@ async def test_billable_call_invoked_with_premium_tier(monkeypatch):
user_id = uuid4()
async def _fake_resolver(sess, search_space_id, *, thread_id=None):
async def _fake_resolver(sess, workspace_id, *, thread_id=None):
return user_id, "premium", "gpt-5.4"
monkeypatch.setattr(
video_presentation_tasks,
"_resolve_agent_billing_for_search_space",
"_resolve_agent_billing_for_workspace",
_fake_resolver,
)
monkeypatch.setattr(video_presentation_tasks, "billable_call", _ok_billable_call)
@ -235,7 +235,7 @@ async def test_billable_call_invoked_with_premium_tier(monkeypatch):
await video_presentation_tasks._generate_video_presentation(
video_presentation_id=11,
source_content="content",
search_space_id=777,
workspace_id=777,
user_prompt=None,
)
@ -256,12 +256,12 @@ async def test_quota_insufficient_marks_video_failed_and_skips_graph(monkeypatch
lambda: _FakeSessionMaker(session),
)
async def _fake_resolver(sess, search_space_id, *, thread_id=None):
async def _fake_resolver(sess, workspace_id, *, thread_id=None):
return uuid4(), "premium", "gpt-5.4"
monkeypatch.setattr(
video_presentation_tasks,
"_resolve_agent_billing_for_search_space",
"_resolve_agent_billing_for_workspace",
_fake_resolver,
)
monkeypatch.setattr(
@ -283,7 +283,7 @@ async def test_quota_insufficient_marks_video_failed_and_skips_graph(monkeypatch
result = await video_presentation_tasks._generate_video_presentation(
video_presentation_id=12,
source_content="content",
search_space_id=777,
workspace_id=777,
user_prompt=None,
)
@ -309,12 +309,12 @@ async def test_billing_settlement_failure_marks_video_failed(monkeypatch):
lambda: _FakeSessionMaker(session),
)
async def _fake_resolver(sess, search_space_id, *, thread_id=None):
async def _fake_resolver(sess, workspace_id, *, thread_id=None):
return uuid4(), "premium", "gpt-5.4"
monkeypatch.setattr(
video_presentation_tasks,
"_resolve_agent_billing_for_search_space",
"_resolve_agent_billing_for_workspace",
_fake_resolver,
)
monkeypatch.setattr(
@ -335,7 +335,7 @@ async def test_billing_settlement_failure_marks_video_failed(monkeypatch):
result = await video_presentation_tasks._generate_video_presentation(
video_presentation_id=14,
source_content="content",
search_space_id=777,
workspace_id=777,
user_prompt=None,
)
@ -360,12 +360,12 @@ async def test_resolver_failure_marks_video_failed(monkeypatch):
lambda: _FakeSessionMaker(session),
)
async def _failing_resolver(sess, search_space_id, *, thread_id=None):
raise ValueError("Search space 777 not found")
async def _failing_resolver(sess, workspace_id, *, thread_id=None):
raise ValueError("Workspace 777 not found")
monkeypatch.setattr(
video_presentation_tasks,
"_resolve_agent_billing_for_search_space",
"_resolve_agent_billing_for_workspace",
_failing_resolver,
)
@ -384,7 +384,7 @@ async def test_resolver_failure_marks_video_failed(monkeypatch):
result = await video_presentation_tasks._generate_video_presentation(
video_presentation_id=13,
source_content="content",
search_space_id=777,
workspace_id=777,
user_prompt=None,
)

View file

@ -16,7 +16,7 @@ ALLOW_ANY_EXPECTED = {
"routes/obsidian_plugin_routes.py": (
"_auth: AuthContext = Depends(allow_any_principal)"
),
"routes/search_spaces_routes.py": (
"routes/workspaces_routes.py": (
"auth: AuthContext = Depends(allow_any_principal)"
),
}
@ -60,12 +60,12 @@ def test_allow_any_principal_is_only_used_by_bootstrap_allowlist() -> None:
assert expected_snippet in text
def test_connector_listers_route_pat_through_search_space_gate() -> None:
def test_connector_listers_route_pat_through_workspace_gate() -> None:
for rel_path in CONNECTOR_LISTERS:
text = (APP_ROOT / rel_path).read_text()
assert "auth: AuthContext = Depends(get_auth_context)" in text, rel_path
assert (
"await check_search_space_access(session, auth, connector.search_space_id)"
"await check_workspace_access(session, auth, connector.workspace_id)"
in text
), rel_path

View file

@ -11,7 +11,7 @@ from app.utils.validators import (
validate_messages,
validate_research_mode,
validate_search_mode,
validate_search_space_id,
validate_workspace_id,
validate_top_k,
validate_url,
validate_uuid,
@ -34,8 +34,8 @@ pytestmark = pytest.mark.unit
(" 42 ", 42),
],
)
def test_validate_search_space_id_valid(valid_input, expected):
assert validate_search_space_id(valid_input) == expected
def test_validate_workspace_id_valid(valid_input, expected):
assert validate_workspace_id(valid_input) == expected
@pytest.mark.parametrize(
@ -54,9 +54,9 @@ def test_validate_search_space_id_valid(valid_input, expected):
"-5",
],
)
def test_validate_search_space_id_invalid(invalid_input):
def test_validate_workspace_id_invalid(invalid_input):
with pytest.raises(HTTPException) as excinfo:
validate_search_space_id(invalid_input)
validate_workspace_id(invalid_input)
assert excinfo.value.status_code == 400