diff --git a/pyproject.toml b/pyproject.toml index 9025227..7df513a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ dev = [ [project.scripts] cocoindex-code = "cocoindex_code:main" -ccc = "cocoindex_code.cli:app" +ccc = "cocoindex_code.cli:main" [project.urls] Homepage = "https://github.com/cocoindex-io/cocoindex-code" diff --git a/src/cocoindex_code/cli.py b/src/cocoindex_code/cli.py index 7128c47..3ac521c 100644 --- a/src/cocoindex_code/cli.py +++ b/src/cocoindex_code/cli.py @@ -1164,6 +1164,11 @@ def run_daemon_cmd() -> None: run_daemon() +def main() -> None: + """Run the CLI without expanding literal search patterns on Windows.""" + app(windows_expand_args=False) + + # Allow running as module: python -m cocoindex_code.cli if __name__ == "__main__": - app() + main() diff --git a/tests/test_cli_entrypoint.py b/tests/test_cli_entrypoint.py new file mode 100644 index 0000000..3ac6a8b --- /dev/null +++ b/tests/test_cli_entrypoint.py @@ -0,0 +1,61 @@ +"""Exercise argv parsing through both user-facing CLI entry points.""" + +import os +import runpy +import sys +from importlib.metadata import distribution +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest +import typer.core + +from cocoindex_code import client +from cocoindex_code.protocol import SearchResponse + + +@pytest.mark.parametrize("entrypoint", ["console", "module"]) +@pytest.mark.parametrize("query", ["some query", "docs/*"]) +def test_windows_search_preserves_globs( + entrypoint: str, query: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Matching local files must not replace search terms or path filters.""" + docs = tmp_path / "docs" + docs.mkdir() + (docs / "a.md").write_text("first document", encoding="utf-8") + (docs / "b.md").write_text("second document", encoding="utf-8") + project_settings = tmp_path / ".cocoindex_code" + project_settings.mkdir() + (project_settings / "settings.yml").write_text("include_patterns: []", encoding="utf-8") + user_settings = tmp_path / "ccc_home" + user_settings.mkdir() + (user_settings / "global_settings.yml").write_text( + "embedding:\n model: test\n provider: litellm\n", encoding="utf-8" + ) + monkeypatch.setenv("COCOINDEX_CODE_DIR", str(user_settings)) + monkeypatch.delenv("COCOINDEX_CODE_HOST_CWD", raising=False) + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(sys, "argv", ["ccc", "search", query, "--path", "docs/*"]) + # Only select Typer's Windows parsing branch; leave pathlib's OS unchanged. + monkeypatch.setattr(typer.core, "os", SimpleNamespace(**(vars(os) | {"name": "nt"}))) + search = Mock(return_value=SearchResponse(success=True)) + monkeypatch.setattr(client, "search", search) + + with pytest.raises(SystemExit) as exc_info: + if entrypoint == "console": + entry = next( + entry + for entry in distribution("cocoindex-code").entry_points + if entry.group == "console_scripts" and entry.name == "ccc" + ) + entry.load()() + else: + # Other tests import cli during collection; runpy needs a fresh module. + monkeypatch.delitem(sys.modules, "cocoindex_code.cli", raising=False) + runpy.run_module("cocoindex_code.cli", run_name="__main__", alter_sys=True) + + assert exc_info.value.code == 0 + search.assert_called_once() + assert search.call_args.kwargs["query"] == query + assert search.call_args.kwargs["paths"] == ["docs/*"]