mirror of
https://github.com/zensical/zensical.git
synced 2026-10-08 05:41:24 +00:00
Also defer llmstxt validation until initial page processing completes. Signed-off-by: Timothée Mazzucotelli <dev@pawamoy.fr>
153 lines
5.1 KiB
Python
Vendored
153 lines
5.1 KiB
Python
Vendored
# Copyright (c) 2025-2026 Zensical and contributors
|
|
|
|
# SPDX-License-Identifier: MIT
|
|
# All contributions are certified under the DCO
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to
|
|
# deal in the Software without restriction, including without limitation the
|
|
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
# sell copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
# IN THE SOFTWARE.
|
|
|
|
"""Configuration compatibility for native LLM text output."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from zensical.config import ConfigurationError, _convert_plugins
|
|
|
|
|
|
def convert(
|
|
value: Any, site_url: str | None = "https://example.org/docs/"
|
|
) -> dict[str, Any]:
|
|
"""Normalize plugin settings with the project fields they use."""
|
|
project = {
|
|
"site_url": site_url,
|
|
"extra": {"polyfills": []},
|
|
"root_dir": ".",
|
|
}
|
|
return _convert_plugins(value, project)["llmstxt"]["config"]
|
|
|
|
|
|
def test_plugin_presence_controls_generation() -> None:
|
|
assert convert([])["enabled"] is False
|
|
assert convert(["llmstxt"])["enabled"] is True
|
|
assert (
|
|
convert({"llmstxt": {"enabled": False}}, site_url=None)["enabled"]
|
|
is False
|
|
)
|
|
|
|
|
|
def test_sections_preserve_order_patterns_and_descriptions() -> None:
|
|
settings = convert(
|
|
{
|
|
"llmstxt": {
|
|
"base_url": "https://other.org/v1",
|
|
"markdown_description": "Long **description**.",
|
|
"full_output": "ai/full.txt",
|
|
"sections": {
|
|
"Z first": [{"index.md": "Home"}, "guide/*.md"],
|
|
"A second": ["other.md"],
|
|
},
|
|
}
|
|
}
|
|
)
|
|
|
|
assert settings["autoclean"] is True
|
|
assert settings["base_url"] == "https://other.org/v1"
|
|
assert settings["full_output"] == "ai/full.txt"
|
|
assert [section["title"] for section in settings["sections"]] == [
|
|
"Z first",
|
|
"A second",
|
|
]
|
|
inputs = settings["sections"][0]["inputs"]
|
|
assert inputs[0] == {
|
|
"pattern": "index.md",
|
|
"description": "Home",
|
|
"expression": None,
|
|
}
|
|
assert inputs[1]["pattern"] == "guide/*.md"
|
|
assert inputs[1]["expression"].startswith(r"\A(?s:")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"preprocess",
|
|
[
|
|
None,
|
|
"custom.py",
|
|
False,
|
|
42,
|
|
["custom.py"],
|
|
{"function": "preprocess"},
|
|
object(),
|
|
],
|
|
)
|
|
def test_silently_ignores_preprocess(
|
|
preprocess: Any,
|
|
capsys: pytest.CaptureFixture[str],
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
expected = convert(["llmstxt"])
|
|
|
|
settings = convert({"llmstxt": {"preprocess": preprocess}})
|
|
|
|
assert settings == expected
|
|
captured = capsys.readouterr()
|
|
assert captured.out == ""
|
|
assert captured.err == ""
|
|
assert not caplog.records
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("settings", "message"),
|
|
[
|
|
({"enabled": "yes"}, "enabled must be a boolean"),
|
|
({"autoclean": 1}, "autoclean must be a boolean"),
|
|
({"base_url": 42}, "base_url must be a string"),
|
|
({"markdown_description": []}, "markdown_description must be a string"),
|
|
({"full_output": True}, "full_output must be a string"),
|
|
({"full_output": "../full.txt"}, "relative file path"),
|
|
({"full_output": "/full.txt"}, "relative file path"),
|
|
({"full_output": "full/../file.txt"}, "relative file path"),
|
|
({"full_output": "llms.txt"}, "must differ from llms.txt"),
|
|
({"sections": []}, "sections must be a mapping"),
|
|
({"sections": {"Pages": "index.md"}}, "map strings to lists"),
|
|
({"sections": {"Pages": [False]}}, "single-entry mappings"),
|
|
(
|
|
{"sections": {"Pages": [{"a.md": "A", "b.md": "B"}]}},
|
|
"single-entry mappings",
|
|
),
|
|
(
|
|
{"sections": {"Pages": [{"a.md": 1}]}},
|
|
"descriptions must be strings",
|
|
),
|
|
({"sections": {"Pages": [""]}}, "non-empty strings"),
|
|
({"unknown": True}, "unknown llmstxt option"),
|
|
],
|
|
)
|
|
def test_invalid_settings_report_configuration_errors(
|
|
settings: dict[str, Any], message: str
|
|
) -> None:
|
|
with pytest.raises(ConfigurationError, match=message):
|
|
convert({"llmstxt": settings})
|
|
|
|
|
|
def test_enabled_plugin_requires_site_url() -> None:
|
|
with pytest.raises(ConfigurationError, match="requires site_url"):
|
|
convert(["llmstxt"], site_url=None)
|