"""Scope precedence (paths -> include -> exclude) and config validation.""" import sys from pathlib import Path import pytest SCRIPTS = Path(__file__).parent.parent / "scripts" sys.path.insert(0, str(SCRIPTS)) import wiki_config from wiki_config import ConfigError, SourceConfig, load_config, source_root WORKSPACE_YAML = """ embedding: endpoint: http://nvidia.hell:11434/ model: qwen3-embedding:0.6b dims: 1024 batch: 32 keep_alive: -1 query_prefix: "Instruct: task\\nQuery: " sources: index: kind: git url: git@git.fnet.cz:lachtan/index.git paths: ["**"] include: ["*.md"] exclude: - "**/node_modules/**" - "**/vendor/**" workspace: kind: workspace paths: - "notes/**" - "develop/**" include: ["*.md"] exclude: - "**/inbox/**" - "develop/history.md" """ def _write(tmp_path, text): path = tmp_path / "config.yaml" path.write_text(text, encoding="utf-8") return path def _source( source_id: str = "ws", kind: str = "workspace", url: str | None = None, paths: tuple[str, ...] = ("**",), include: tuple[str, ...] = ("*.md",), exclude: tuple[str, ...] = (), ) -> SourceConfig: return SourceConfig(source_id=source_id, kind=kind, url=url, paths=paths, include=include, exclude=exclude) def test_loads_embedding_and_sources(tmp_path): config = load_config(_write(tmp_path, WORKSPACE_YAML)) assert config.embedding.endpoint == "http://nvidia.hell:11434" # trailing slash trimmed assert config.embedding.model == "qwen3-embedding:0.6b" assert config.embedding.dims == 1024 assert config.embedding.keep_alive == -1 assert config.embedding.query_prefix.endswith("Query: ") assert [s.source_id for s in config.sources] == ["index", "workspace"] index = config.source("index") assert index is not None and index.kind == "git" assert config.source("nope") is None def test_string_keep_alive_is_rejected(): """Ollama answers HTTP 400 to a string "-1" — catch it in config, not at runtime.""" with pytest.raises(ConfigError, match="keep_alive"): wiki_config._parse_embedding({"endpoint": "http://x", "model": "m", "dims": 1024, "keep_alive": "-1"}) def test_missing_config_file(tmp_path): with pytest.raises(ConfigError, match="missing config"): load_config(tmp_path / "nope.yaml") def test_git_source_needs_url(tmp_path): yaml_text = WORKSPACE_YAML.replace(" url: git@git.fnet.cz:lachtan/index.git\n", "") with pytest.raises(ConfigError, match="need a url"): load_config(_write(tmp_path, yaml_text)) def test_unknown_kind_is_rejected(tmp_path): yaml_text = WORKSPACE_YAML.replace(" kind: workspace", " kind: mirror") with pytest.raises(ConfigError, match="kind must be one of"): load_config(_write(tmp_path, yaml_text)) def test_paths_is_required(tmp_path): yaml_text = WORKSPACE_YAML.replace(' paths: ["**"]\n', "") with pytest.raises(ConfigError, match="`paths` is required"): load_config(_write(tmp_path, yaml_text)) def test_paths_whitelist_gates_everything(): source = _source(paths=("notes/**", "develop/**")) assert source.covers("notes/a.md") assert source.covers("notes/deep/nested/a.md") assert source.covers("develop/knowledge.md") # Not on the whitelist -> does not exist for the index. assert not source.covers("tmp/a.md") assert not source.covers("skills/wiki/SKILL.md") assert not source.covers("AGENTS.md") def test_include_filters_extensions(): source = _source(paths=("**",)) assert source.covers("notes/a.md") assert not source.covers("notes/main.py") assert not source.covers("memory/history.jsonl") assert not source.covers("assets/photo.png") def test_exclude_wins_over_paths_and_include(): source = _source(paths=("notes/**", "develop/**"), exclude=("**/inbox/**", "develop/history.md")) assert not source.covers("notes/inbox/raw.md") assert not source.covers("develop/history.md") assert source.covers("develop/knowledge.md") assert source.covers("notes/notes.md") def test_double_star_matches_whole_repo(): source = _source(paths=("**",), exclude=("**/node_modules/**", "**/vendor/**")) assert source.covers("README.md") assert source.covers("japan/tokyo/metro.md") assert not source.covers("node_modules/pkg/README.md") assert not source.covers("web/vendor/lib/CHANGELOG.md") def test_glob_star_does_not_cross_a_slash(): source = _source(paths=("notes/*",)) assert source.covers("notes/a.md") assert not source.covers("notes/deep/a.md") def test_covers_dir_prunes_the_walk(): source = _source(paths=("notes/**", "develop/**"), exclude=("**/inbox/**",)) assert source.covers_dir("") assert source.covers_dir("notes") assert source.covers_dir("notes/deep") assert source.covers_dir("develop") assert not source.covers_dir("tmp") assert not source.covers_dir("notes/inbox") def test_covers_dir_never_prunes_a_double_star_source(): source = _source(paths=("**",), exclude=("**/node_modules/**",)) assert source.covers_dir("anything/deep") assert not source.covers_dir("app/node_modules") def test_source_root_derives_clone_path_from_id(): assert source_root(_source(source_id="travel", kind="git", url="git@x:y.git")) == ( wiki_config.REMOTE_DIR / "travel" ) assert source_root(_source(kind="workspace")) == wiki_config.WORKSPACE