Files
Jiangzhou 9700655aa6 refactor(embedder-params): drop dimensions knob; pass indexing kwargs into LiteLLM ctor (#151)
- Remove `dimensions` from the litellm whitelist in `_ACCEPTED_KWARGS`.
  Output dimension must be identical for indexing and query for vectors to
  be comparable, so it's a model-wide setting, not a per-side knob —
  exposing it under `indexing_params` / `query_params` invited
  misconfiguration. Updated comment template, README, design doc, and
  testing plan accordingly.
- Plumb `indexing_params` into `create_embedder` and pass them as
  constructor kwargs to `PacedLiteLLMEmbedder`. The values land in
  `self._kwargs` and become defaults forwarded into every
  `litellm.aembedding` call — including paths that don't go through the
  `INDEXING_EMBED_PARAMS` context var (e.g. the dim probe in `_get_dim`).
  Per-call overrides (`query_params` spread at query time) still win
  because `_embed` overlays kwargs on top of `self._kwargs`. Sentence-
  transformers ignores `indexing_params` (its constructor doesn't accept
  arbitrary kwargs; `prompt_name` is per-call only).
2026-04-25 09:33:24 -07:00

626 lines
24 KiB
Python

"""Unit tests for the settings module."""
from __future__ import annotations
from collections.abc import Iterator
from pathlib import Path
import pytest
# _resolve_chunker_registry is private to daemon.py (single call site), but its
# error paths (bad format, non-callable) are not exercised by integration tests.
from cocoindex_code.daemon import _resolve_chunker_registry
from cocoindex_code.settings import (
DEFAULT_EXCLUDED_PATTERNS,
DEFAULT_INCLUDED_PATTERNS,
ChunkerMapping,
EmbeddingSettings,
LanguageOverride,
ProjectSettings,
UserSettings,
_reset_db_path_mapping_cache,
_reset_host_path_mapping_cache,
default_project_settings,
default_user_settings,
find_parent_with_marker,
find_project_root,
format_path_for_display,
get_host_path_mappings,
load_project_settings,
load_user_settings,
normalize_input_path,
resolve_db_dir,
save_project_settings,
save_user_settings,
)
@pytest.fixture()
def _patch_user_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Redirect user_settings_dir() to a temp directory."""
monkeypatch.setattr(
"cocoindex_code.settings.user_settings_dir",
lambda: tmp_path / ".cocoindex_code",
)
monkeypatch.setattr(
"cocoindex_code.settings.user_settings_path",
lambda: tmp_path / ".cocoindex_code" / "global_settings.yml",
)
def test_default_user_settings() -> None:
s = default_user_settings()
assert s.embedding.provider == "sentence-transformers"
assert s.embedding.model == "Snowflake/snowflake-arctic-embed-xs"
assert s.embedding.device is None
assert s.embedding.min_interval_ms is None
assert s.envs == {}
def test_default_project_settings() -> None:
s = default_project_settings()
assert s.include_patterns == DEFAULT_INCLUDED_PATTERNS
assert s.exclude_patterns == DEFAULT_EXCLUDED_PATTERNS
assert s.language_overrides == []
@pytest.mark.usefixtures("_patch_user_dir")
def test_save_and_load_user_settings(tmp_path: Path) -> None:
settings = UserSettings(
embedding=EmbeddingSettings(
provider="litellm",
model="gemini/text-embedding-004",
device="cpu",
min_interval_ms=300,
),
envs={"GEMINI_API_KEY": "test-key"},
)
save_user_settings(settings)
loaded = load_user_settings()
assert loaded.embedding.provider == settings.embedding.provider
assert loaded.embedding.model == settings.embedding.model
assert loaded.embedding.device == settings.embedding.device
assert loaded.embedding.min_interval_ms == settings.embedding.min_interval_ms
assert loaded.envs == settings.envs
def test_save_and_load_project_settings(tmp_path: Path) -> None:
settings = ProjectSettings(
include_patterns=["**/*.py", "**/*.rs"],
exclude_patterns=["**/target"],
language_overrides=[LanguageOverride(ext="inc", lang="php")],
)
save_project_settings(tmp_path, settings)
loaded = load_project_settings(tmp_path)
assert loaded.include_patterns == settings.include_patterns
assert loaded.exclude_patterns == settings.exclude_patterns
assert len(loaded.language_overrides) == 1
assert loaded.language_overrides[0].ext == "inc"
assert loaded.language_overrides[0].lang == "php"
@pytest.mark.usefixtures("_patch_user_dir")
def test_load_user_settings_missing_file_raises() -> None:
with pytest.raises(FileNotFoundError):
load_user_settings()
@pytest.mark.usefixtures("_patch_user_dir")
def test_load_user_settings_empty_file_raises(tmp_path: Path) -> None:
path = tmp_path / ".cocoindex_code" / "global_settings.yml"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("{}\n")
with pytest.raises(ValueError):
load_user_settings()
@pytest.mark.usefixtures("_patch_user_dir")
def test_load_user_settings_missing_model_raises(tmp_path: Path) -> None:
path = tmp_path / ".cocoindex_code" / "global_settings.yml"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("embedding:\n provider: litellm\n")
with pytest.raises(ValueError):
load_user_settings()
@pytest.mark.usefixtures("_patch_user_dir")
def test_from_dict_missing_provider_defaults_to_litellm() -> None:
from cocoindex_code.settings import _user_settings_from_dict
settings = _user_settings_from_dict({"embedding": {"model": "some/model"}})
assert settings.embedding.provider == "litellm"
assert settings.embedding.model == "some/model"
assert settings.embedding.min_interval_ms is None
@pytest.mark.usefixtures("_patch_user_dir")
def test_save_default_settings_writes_explicit_embedding() -> None:
from cocoindex_code.settings import user_settings_path
save_user_settings(default_user_settings())
content = user_settings_path().read_text()
assert "provider:" in content
assert "model:" in content
assert "Snowflake/snowflake-arctic-embed-xs" in content
def test_load_project_settings_missing_file_raises(tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
load_project_settings(tmp_path)
def test_find_project_root_from_subdirectory(tmp_path: Path) -> None:
project = tmp_path / "project"
(project / ".cocoindex_code").mkdir(parents=True)
(project / ".cocoindex_code" / "settings.yml").write_text("include_patterns: []")
subdir = project / "src" / "lib"
subdir.mkdir(parents=True)
assert find_project_root(subdir) == project
def test_find_project_root_from_project_root(tmp_path: Path) -> None:
project = tmp_path / "project"
(project / ".cocoindex_code").mkdir(parents=True)
(project / ".cocoindex_code" / "settings.yml").write_text("include_patterns: []")
assert find_project_root(project) == project
def test_find_project_root_returns_none_when_not_initialized(tmp_path: Path) -> None:
standalone = tmp_path / "standalone"
standalone.mkdir()
assert find_project_root(standalone) is None
def test_find_parent_with_marker_finds_git(tmp_path: Path) -> None:
repo = tmp_path / "repo"
(repo / ".git").mkdir(parents=True)
subdir = repo / "src"
subdir.mkdir()
assert find_parent_with_marker(subdir) == repo
def test_find_parent_with_marker_prefers_cocoindex_code(tmp_path: Path) -> None:
repo = tmp_path / "repo"
(repo / ".git").mkdir(parents=True)
(repo / ".cocoindex_code").mkdir(parents=True)
subdir = repo / "src"
subdir.mkdir()
assert find_parent_with_marker(subdir) == repo
@pytest.mark.usefixtures("_patch_user_dir")
def test_user_settings_litellm_round_trip() -> None:
settings = UserSettings(
embedding=EmbeddingSettings(
provider="litellm",
model="gemini/text-embedding-004",
min_interval_ms=250,
),
envs={"GEMINI_API_KEY": "test"},
)
save_user_settings(settings)
loaded = load_user_settings()
assert loaded.embedding.provider == "litellm"
assert loaded.embedding.model == "gemini/text-embedding-004"
assert loaded.embedding.min_interval_ms == 250
assert loaded.envs == {"GEMINI_API_KEY": "test"}
@pytest.mark.usefixtures("_patch_user_dir")
def test_load_user_settings_with_min_interval_ms(tmp_path: Path) -> None:
path = tmp_path / ".cocoindex_code" / "global_settings.yml"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
"embedding:\n provider: litellm\n model: text-embedding-3-small\n min_interval_ms: 300\n"
)
loaded = load_user_settings()
assert loaded.embedding.provider == "litellm"
assert loaded.embedding.model == "text-embedding-3-small"
assert loaded.embedding.min_interval_ms == 300
def test_project_settings_with_language_overrides(tmp_path: Path) -> None:
settings = ProjectSettings(
language_overrides=[LanguageOverride(ext="inc", lang="php")],
)
save_project_settings(tmp_path, settings)
loaded = load_project_settings(tmp_path)
assert len(loaded.language_overrides) == 1
assert loaded.language_overrides[0].ext == "inc"
assert loaded.language_overrides[0].lang == "php"
class TestResolveDbDir:
"""Tests for COCOINDEX_CODE_DB_PATH_MAPPING and resolve_db_dir()."""
@pytest.fixture(autouse=True)
def _clear_cache(self, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
"""Reset cached mapping before each test."""
_reset_db_path_mapping_cache()
monkeypatch.delenv("COCOINDEX_CODE_DB_PATH_MAPPING", raising=False)
yield
_reset_db_path_mapping_cache()
def test_no_mapping(self, tmp_path: Path) -> None:
project = tmp_path / "myproject"
assert resolve_db_dir(project) == project / ".cocoindex_code"
def test_single_mapping_match(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
src = tmp_path / "workspace"
dst = tmp_path / "db-files"
monkeypatch.setenv("COCOINDEX_CODE_DB_PATH_MAPPING", f"{src}={dst}")
assert resolve_db_dir(src / "myproject") == dst / "myproject"
def test_exact_root_match(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
src = tmp_path / "workspace"
dst = tmp_path / "db-files"
monkeypatch.setenv("COCOINDEX_CODE_DB_PATH_MAPPING", f"{src}={dst}")
assert resolve_db_dir(src) == dst
def test_no_match_falls_back(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
src = tmp_path / "workspace"
dst = tmp_path / "db-files"
other = tmp_path / "other" / "myproject"
monkeypatch.setenv("COCOINDEX_CODE_DB_PATH_MAPPING", f"{src}={dst}")
assert resolve_db_dir(other) == other / ".cocoindex_code"
def test_multiple_mappings_first_wins(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
src = tmp_path / "workspace"
dst1 = tmp_path / "db1"
dst2 = tmp_path / "db2"
monkeypatch.setenv("COCOINDEX_CODE_DB_PATH_MAPPING", f"{src}={dst1},{src / 'sub'}={dst2}")
assert resolve_db_dir(src / "sub" / "proj") == dst1 / "sub" / "proj"
def test_multiple_mappings_second_matches(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
src1 = tmp_path / "workspace"
src2 = tmp_path / "other"
dst1 = tmp_path / "db1"
dst2 = tmp_path / "db2"
monkeypatch.setenv("COCOINDEX_CODE_DB_PATH_MAPPING", f"{src1}={dst1},{src2}={dst2}")
assert resolve_db_dir(src2 / "proj") == dst2 / "proj"
def test_no_partial_component_match(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
src = tmp_path / "workspace"
dst = tmp_path / "db-files"
other = tmp_path / "workspace2" / "proj"
monkeypatch.setenv("COCOINDEX_CODE_DB_PATH_MAPPING", f"{src}={dst}")
assert resolve_db_dir(other) == other / ".cocoindex_code"
def test_rejects_relative_source(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("COCOINDEX_CODE_DB_PATH_MAPPING", "relative/path=/db-files")
with pytest.raises(ValueError, match="source path must be absolute"):
resolve_db_dir(Path("/anything"))
def test_rejects_relative_target(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
src = tmp_path / "workspace"
monkeypatch.setenv("COCOINDEX_CODE_DB_PATH_MAPPING", f"{src}=relative/path")
with pytest.raises(ValueError, match="target path must be absolute"):
resolve_db_dir(tmp_path / "anything")
def test_skips_empty_entries(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
src1 = tmp_path / "workspace"
src2 = tmp_path / "other"
dst1 = tmp_path / "db-files"
dst2 = tmp_path / "db2"
monkeypatch.setenv("COCOINDEX_CODE_DB_PATH_MAPPING", f"{src1}={dst1},,{src2}={dst2},")
assert resolve_db_dir(src2 / "proj") == dst2 / "proj"
def test_nested_project(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
src = tmp_path / "workspace"
dst = tmp_path / "db-files"
monkeypatch.setenv("COCOINDEX_CODE_DB_PATH_MAPPING", f"{src}={dst}")
assert resolve_db_dir(src / "org" / "repo" / "subdir") == dst / "org" / "repo" / "subdir"
def test_project_settings_with_chunkers(tmp_path: Path) -> None:
settings = ProjectSettings(
chunkers=[ChunkerMapping(ext="toml", module="example_toml_chunker:toml_chunker")],
)
save_project_settings(tmp_path, settings)
loaded = load_project_settings(tmp_path)
assert len(loaded.chunkers) == 1
assert loaded.chunkers[0].ext == "toml"
assert loaded.chunkers[0].module == "example_toml_chunker:toml_chunker"
def test_resolve_chunker_registry_missing_colon() -> None:
with pytest.raises(ValueError, match="module.path:callable"):
_resolve_chunker_registry([ChunkerMapping(ext="toml", module="no_colon_here")])
def test_resolve_chunker_registry_not_callable() -> None:
# os.path is a module attribute that is a string — not callable.
with pytest.raises(ValueError, match="not callable"):
_resolve_chunker_registry([ChunkerMapping(ext="toml", module="os:sep")])
@pytest.mark.usefixtures("_patch_user_dir")
def test_save_initial_user_settings_round_trip() -> None:
from cocoindex_code.settings import (
save_initial_user_settings,
user_settings_path,
)
emb = EmbeddingSettings(
provider="sentence-transformers",
model="Snowflake/snowflake-arctic-embed-xs",
)
path = save_initial_user_settings(emb, defaults_applied=False)
content = path.read_text()
# Hint comment and the four commented env-var examples.
assert "ccc doctor" in content
assert "# envs:" in content
for key in ("OPENAI_API_KEY", "GEMINI_API_KEY", "ANTHROPIC_API_KEY", "VOYAGE_API_KEY"):
assert f"# {key}:" in content
# Must round-trip through the normal loader.
loaded = load_user_settings()
assert loaded.embedding.provider == "sentence-transformers"
assert loaded.embedding.model == "Snowflake/snowflake-arctic-embed-xs"
# user_settings_path() is the same path returned by save_initial_user_settings.
assert path == user_settings_path()
@pytest.mark.usefixtures("_patch_user_dir")
def test_save_initial_user_settings_model_with_colon() -> None:
"""Regression: LiteLLM model names can contain `:`; must stay parseable."""
from cocoindex_code.settings import save_initial_user_settings
emb = EmbeddingSettings(
provider="litellm",
model="ollama_chat/llama3:latest",
)
save_initial_user_settings(emb, defaults_applied=False)
loaded = load_user_settings()
assert loaded.embedding.provider == "litellm"
assert loaded.embedding.model == "ollama_chat/llama3:latest"
# ---------------------------------------------------------------------------
# Host path mapping (COCOINDEX_CODE_HOST_PATH_MAPPING)
# ---------------------------------------------------------------------------
class TestHostPathMapping:
"""Tests for format_path_for_display / normalize_input_path and the shared parser."""
@pytest.fixture(autouse=True)
def _clear_cache(self, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
_reset_host_path_mapping_cache()
monkeypatch.delenv("COCOINDEX_CODE_HOST_PATH_MAPPING", raising=False)
yield
_reset_host_path_mapping_cache()
def test_translates_display(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
container = tmp_path / "workspace"
host = tmp_path / "alice"
container.mkdir()
host.mkdir()
monkeypatch.setenv("COCOINDEX_CODE_HOST_PATH_MAPPING", f"{container}={host}")
assert format_path_for_display(container / "proj" / "app.py") == str(
host / "proj" / "app.py"
)
def test_translates_input(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
container = tmp_path / "workspace"
host = tmp_path / "alice"
container.mkdir()
host.mkdir()
monkeypatch.setenv("COCOINDEX_CODE_HOST_PATH_MAPPING", f"{container}={host}")
assert normalize_input_path(host / "proj") == str(container / "proj")
def test_unmatched_absolute_passes_through(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
container = tmp_path / "workspace"
host = tmp_path / "alice"
container.mkdir()
host.mkdir()
monkeypatch.setenv("COCOINDEX_CODE_HOST_PATH_MAPPING", f"{container}={host}")
unrelated = "/etc/hosts"
assert format_path_for_display(unrelated) == unrelated
assert normalize_input_path(unrelated) == unrelated
def test_relative_passes_through(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
container = tmp_path / "workspace"
host = tmp_path / "alice"
container.mkdir()
host.mkdir()
monkeypatch.setenv("COCOINDEX_CODE_HOST_PATH_MAPPING", f"{container}={host}")
assert format_path_for_display("src/app.py") == "src/app.py"
assert normalize_input_path("src/app.py") == "src/app.py"
def test_first_match_wins(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
ws = tmp_path / "workspace"
shared = ws / "shared"
host_ws = tmp_path / "alice"
host_shared = tmp_path / "mnt-shared"
ws.mkdir()
shared.mkdir(parents=True)
host_ws.mkdir()
host_shared.mkdir()
monkeypatch.setenv(
"COCOINDEX_CODE_HOST_PATH_MAPPING",
f"{ws}={host_ws},{shared}={host_shared}",
)
# Path under shared — first mapping wins, not the more-specific one.
assert format_path_for_display(shared / "docs" / "x") == str(
host_ws / "shared" / "docs" / "x"
)
def test_env_unset_is_noop(self) -> None:
# Fixture already clears env var.
assert format_path_for_display("/workspace/x") == "/workspace/x"
assert normalize_input_path("/workspace/x") == "/workspace/x"
assert get_host_path_mappings() == []
def test_invalid_env_raises(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("COCOINDEX_CODE_HOST_PATH_MAPPING", "relative=/abs")
with pytest.raises(ValueError, match="source path must be absolute"):
get_host_path_mappings()
# ---------------------------------------------------------------------------
# find_parent_with_marker — global-only should not match
# ---------------------------------------------------------------------------
def test_find_parent_with_marker_skips_global_only(tmp_path: Path) -> None:
"""A workspace-root ``.cocoindex_code/`` holding only ``global_settings.yml``
should NOT trigger the parent-marker check (it's not a project).
"""
ws = tmp_path / "ws"
(ws / ".cocoindex_code").mkdir(parents=True)
(ws / ".cocoindex_code" / "global_settings.yml").write_text("embedding: {model: x}\n")
subdir = ws / "myproject"
subdir.mkdir()
assert find_parent_with_marker(subdir) is None
def test_find_parent_with_marker_detects_project_settings(tmp_path: Path) -> None:
"""``.cocoindex_code/settings.yml`` at a parent is a real project marker."""
repo = tmp_path / "repo"
(repo / ".cocoindex_code").mkdir(parents=True)
(repo / ".cocoindex_code" / "settings.yml").write_text("include_patterns: []\n")
subdir = repo / "src"
subdir.mkdir()
assert find_parent_with_marker(subdir) == repo
# ---------------------------------------------------------------------------
# daemon_runtime_dir
# ---------------------------------------------------------------------------
def test_daemon_runtime_dir_uses_env_var(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
from cocoindex_code._daemon_paths import daemon_runtime_dir
target = tmp_path / "runtime"
monkeypatch.setenv("COCOINDEX_CODE_RUNTIME_DIR", str(target))
assert daemon_runtime_dir() == target
def test_daemon_runtime_dir_falls_back_to_user_settings_dir(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""When COCOINDEX_CODE_RUNTIME_DIR is unset, falls back to user_settings_dir()."""
from cocoindex_code._daemon_paths import daemon_runtime_dir
settings_dir = tmp_path / "settings"
monkeypatch.delenv("COCOINDEX_CODE_RUNTIME_DIR", raising=False)
monkeypatch.setenv("COCOINDEX_CODE_DIR", str(settings_dir))
assert daemon_runtime_dir() == settings_dir
# ---------------------------------------------------------------------------
# indexing_params / query_params round-trip and templates
# ---------------------------------------------------------------------------
@pytest.mark.usefixtures("_patch_user_dir")
def test_embedding_params_missing_load_as_none(tmp_path: Path) -> None:
path = tmp_path / ".cocoindex_code" / "global_settings.yml"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("embedding:\n provider: litellm\n model: m\n")
loaded = load_user_settings()
assert loaded.embedding.indexing_params is None
assert loaded.embedding.query_params is None
@pytest.mark.usefixtures("_patch_user_dir")
def test_embedding_params_roundtrip_preserves_empty_dict() -> None:
settings = UserSettings(
embedding=EmbeddingSettings(
provider="sentence-transformers",
model="x/y",
indexing_params={"prompt_name": "passage"},
query_params={}, # explicit empty — must round-trip as {} not None
),
)
save_user_settings(settings)
loaded = load_user_settings()
assert loaded.embedding.indexing_params == {"prompt_name": "passage"}
assert loaded.embedding.query_params == {}
@pytest.mark.usefixtures("_patch_user_dir")
def test_embedding_params_omit_when_none() -> None:
settings = UserSettings(
embedding=EmbeddingSettings(
provider="litellm",
model="m",
),
)
save_user_settings(settings)
from cocoindex_code.settings import user_settings_path
content = user_settings_path().read_text()
assert "indexing_params" not in content
assert "query_params" not in content
@pytest.mark.usefixtures("_patch_user_dir")
def test_save_initial_writes_populated_defaults_no_template() -> None:
from cocoindex_code.settings import save_initial_user_settings, user_settings_path
emb = EmbeddingSettings(
provider="sentence-transformers",
model="nomic-ai/CodeRankEmbed",
query_params={"prompt_name": "query"},
)
save_initial_user_settings(emb, defaults_applied=True)
content = user_settings_path().read_text()
# Populated as real YAML keys
assert "query_params:" in content
assert "prompt_name: query" in content
# No commented-out template hint
assert "# indexing_params: {}" not in content
assert "# query_params: {}" not in content
@pytest.mark.usefixtures("_patch_user_dir")
def test_save_initial_writes_comment_template_for_unknown_sentence_transformers() -> None:
from cocoindex_code.settings import save_initial_user_settings, user_settings_path
emb = EmbeddingSettings(
provider="sentence-transformers",
model="unknown/model",
)
save_initial_user_settings(emb, defaults_applied=False)
content = user_settings_path().read_text()
assert "# indexing_params: {}" in content
assert "# query_params: {}" in content
assert "prompt_name" in content
# litellm-only keys should not appear
assert "input_type" not in content
@pytest.mark.usefixtures("_patch_user_dir")
def test_save_initial_writes_comment_template_for_unknown_litellm() -> None:
from cocoindex_code.settings import save_initial_user_settings, user_settings_path
emb = EmbeddingSettings(
provider="litellm",
model="someprovider/unknown",
)
save_initial_user_settings(emb, defaults_applied=False)
content = user_settings_path().read_text()
assert "# indexing_params: {}" in content
assert "# query_params: {}" in content
assert "input_type" in content
# `dimensions` is intentionally NOT in the litellm template — it must be
# the same on both sides, so we don't expose it as a per-side knob.
assert "dimensions" not in content